70
import cPickle as pickle
63
import cPickle as pickle
73
64
import multiprocessing
83
68
import dbus.service
85
from gi.repository import GLib
86
71
from dbus.mainloop.glib import DBusGMainLoop
89
74
import xml.dom.minidom
92
if sys.version_info.major == 2:
95
# Try to find the value of SO_BINDTODEVICE:
97
# This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
98
# newer, and it is also the most natural place for it:
99
78
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
100
79
except AttributeError:
102
# This is where SO_BINDTODEVICE was up to and including Python
104
81
from IN import SO_BINDTODEVICE
105
82
except ImportError:
106
# In Python 2.7 it seems to have been removed entirely.
107
# Try running the C preprocessor:
109
cc = subprocess.Popen(["cc", "--language=c", "-E",
111
stdin=subprocess.PIPE,
112
stdout=subprocess.PIPE)
113
stdout = cc.communicate(
114
"#include <sys/socket.h>\nSO_BINDTODEVICE\n")[0]
115
SO_BINDTODEVICE = int(stdout.splitlines()[-1])
116
except (OSError, ValueError, IndexError):
118
SO_BINDTODEVICE = None
120
if sys.version_info.major == 2:
123
if sys.version_info < (3, 2):
124
configparser.Configparser = configparser.SafeConfigParser
127
stored_state_file = "clients.pickle"
129
logger = logging.getLogger()
133
if_nametoindex = ctypes.cdll.LoadLibrary(
134
ctypes.util.find_library("c")).if_nametoindex
135
except (OSError, AttributeError):
137
def if_nametoindex(interface):
138
"Get an interface index the hard way, i.e. using fcntl()"
139
SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
140
with contextlib.closing(socket.socket()) as s:
141
ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
142
struct.pack(b"16s16x", interface))
143
interface_index = struct.unpack("I", ifreq[16:20])[0]
144
return interface_index
147
def copy_function(func):
148
"""Make a copy of a function"""
149
if sys.version_info.major == 2:
150
return types.FunctionType(func.func_code,
156
return types.FunctionType(func.__code__,
163
def initlogger(debug, level=logging.WARNING):
164
"""init logger and add loglevel"""
167
syslogger = (logging.handlers.SysLogHandler(
168
facility=logging.handlers.SysLogHandler.LOG_DAEMON,
170
syslogger.setFormatter(logging.Formatter
171
('Mandos [%(process)d]: %(levelname)s:'
173
logger.addHandler(syslogger)
176
console = logging.StreamHandler()
177
console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
181
logger.addHandler(console)
182
logger.setLevel(level)
185
class PGPError(Exception):
186
"""Exception if encryption/decryption fails"""
191
"""A simple class for OpenPGP symmetric encryption & decryption"""
194
self.tempdir = tempfile.mkdtemp(prefix="mandos-")
197
output = subprocess.check_output(["gpgconf"])
198
for line in output.splitlines():
199
name, text, path = line.split(b":")
204
if e.errno != errno.ENOENT:
206
self.gnupgargs = ['--batch',
207
'--homedir', self.tempdir,
210
# Only GPG version 1 has the --no-use-agent option.
211
if self.gpg == "gpg" or self.gpg.endswith("/gpg"):
212
self.gnupgargs.append("--no-use-agent")
217
def __exit__(self, exc_type, exc_value, traceback):
225
if self.tempdir is not None:
226
# Delete contents of tempdir
227
for root, dirs, files in os.walk(self.tempdir,
229
for filename in files:
230
os.remove(os.path.join(root, filename))
232
os.rmdir(os.path.join(root, dirname))
234
os.rmdir(self.tempdir)
237
def password_encode(self, password):
238
# Passphrase can not be empty and can not contain newlines or
239
# NUL bytes. So we prefix it and hex encode it.
240
encoded = b"mandos" + binascii.hexlify(password)
241
if len(encoded) > 2048:
242
# GnuPG can't handle long passwords, so encode differently
243
encoded = (b"mandos" + password.replace(b"\\", b"\\\\")
244
.replace(b"\n", b"\\n")
245
.replace(b"\0", b"\\x00"))
248
def encrypt(self, data, password):
249
passphrase = self.password_encode(password)
250
with tempfile.NamedTemporaryFile(
251
dir=self.tempdir) as passfile:
252
passfile.write(passphrase)
254
proc = subprocess.Popen([self.gpg, '--symmetric',
258
stdin=subprocess.PIPE,
259
stdout=subprocess.PIPE,
260
stderr=subprocess.PIPE)
261
ciphertext, err = proc.communicate(input=data)
262
if proc.returncode != 0:
266
def decrypt(self, data, password):
267
passphrase = self.password_encode(password)
268
with tempfile.NamedTemporaryFile(
269
dir=self.tempdir) as passfile:
270
passfile.write(passphrase)
272
proc = subprocess.Popen([self.gpg, '--decrypt',
276
stdin=subprocess.PIPE,
277
stdout=subprocess.PIPE,
278
stderr=subprocess.PIPE)
279
decrypted_plaintext, err = proc.communicate(input=data)
280
if proc.returncode != 0:
282
return decrypted_plaintext
285
# Pretend that we have an Avahi module
287
"""This isn't so much a class as it is a module-like namespace."""
288
IF_UNSPEC = -1 # avahi-common/address.h
289
PROTO_UNSPEC = -1 # avahi-common/address.h
290
PROTO_INET = 0 # avahi-common/address.h
291
PROTO_INET6 = 1 # avahi-common/address.h
292
DBUS_NAME = "org.freedesktop.Avahi"
293
DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
294
DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
295
DBUS_PATH_SERVER = "/"
298
def string_array_to_txt_array(t):
299
return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
300
for s in t), signature="ay")
301
ENTRY_GROUP_ESTABLISHED = 2 # avahi-common/defs.h
302
ENTRY_GROUP_COLLISION = 3 # avahi-common/defs.h
303
ENTRY_GROUP_FAILURE = 4 # avahi-common/defs.h
304
SERVER_INVALID = 0 # avahi-common/defs.h
305
SERVER_REGISTERING = 1 # avahi-common/defs.h
306
SERVER_RUNNING = 2 # avahi-common/defs.h
307
SERVER_COLLISION = 3 # avahi-common/defs.h
308
SERVER_FAILURE = 4 # avahi-common/defs.h
83
SO_BINDTODEVICE = None
88
#logger = logging.getLogger('mandos')
89
logger = logging.Logger('mandos')
90
syslogger = (logging.handlers.SysLogHandler
91
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
92
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)
311
104
class AvahiError(Exception):
312
105
def __init__(self, value, *args, **kwargs):
313
106
self.value = value
314
return super(AvahiError, self).__init__(value, *args,
107
super(AvahiError, self).__init__(value, *args, **kwargs)
108
def __unicode__(self):
109
return unicode(repr(self.value))
318
111
class AvahiServiceError(AvahiError):
322
114
class AvahiGroupError(AvahiError):
118
class AvahiService(object):
327
119
"""An Avahi (Zeroconf) service.
330
122
interface: integer; avahi.IF_UNSPEC or an interface index.
331
123
Used to optionally bind to the specified interface.
332
124
name: string; Example: 'Mandos'
333
125
type: string; Example: '_mandos._tcp'.
334
See <https://www.iana.org/assignments/service-names-port-numbers>
126
See <http://www.dns-sd.org/ServiceTypes.html>
335
127
port: integer; what port to announce
336
128
TXT: list of strings; TXT record for the service
337
129
domain: string; Domain to publish on, default to .local if empty.
496
262
follow_name_owner_changes=True),
497
263
avahi.DBUS_INTERFACE_SERVER)
498
264
self.server.connect_to_signal("StateChanged",
499
self.server_state_changed)
265
self.server_state_changed)
500
266
self.server_state_changed(self.server.GetState())
503
class AvahiServiceToSyslog(AvahiService):
504
def rename(self, *args, **kwargs):
505
"""Add the new name to the syslog messages"""
506
ret = super(AvahiServiceToSyslog, self).rename(*args, **kwargs)
507
syslogger.setFormatter(logging.Formatter(
508
'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
513
# Pretend that we have a GnuTLS module
515
"""This isn't so much a class as it is a module-like namespace."""
517
library = ctypes.util.find_library("gnutls")
519
library = ctypes.util.find_library("gnutls-deb0")
520
_library = ctypes.cdll.LoadLibrary(library)
523
# Unless otherwise indicated, the constants and types below are
524
# all from the gnutls/gnutls.h C header file.
535
E_NO_CERTIFICATE_FOUND = -49
540
KEYID_USE_SHA256 = 1 # gnutls/x509.h
541
OPENPGP_FMT_RAW = 0 # gnutls/openpgp.h
544
class session_int(ctypes.Structure):
546
session_t = ctypes.POINTER(session_int)
548
class certificate_credentials_st(ctypes.Structure):
550
certificate_credentials_t = ctypes.POINTER(
551
certificate_credentials_st)
552
certificate_type_t = ctypes.c_int
554
class datum_t(ctypes.Structure):
555
_fields_ = [('data', ctypes.POINTER(ctypes.c_ubyte)),
556
('size', ctypes.c_uint)]
558
class openpgp_crt_int(ctypes.Structure):
560
openpgp_crt_t = ctypes.POINTER(openpgp_crt_int)
561
openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
562
log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
563
credentials_type_t = ctypes.c_int
564
transport_ptr_t = ctypes.c_void_p
565
close_request_t = ctypes.c_int
568
class Error(Exception):
569
def __init__(self, message=None, code=None, args=()):
570
# Default usage is by a message string, but if a return
571
# code is passed, convert it to a string with
574
if message is None and code is not None:
575
message = gnutls.strerror(code)
576
return super(gnutls.Error, self).__init__(
579
class CertificateSecurityError(Error):
585
self._c_object = gnutls.certificate_credentials_t()
586
gnutls.certificate_allocate_credentials(
587
ctypes.byref(self._c_object))
588
self.type = gnutls.CRD_CERTIFICATE
591
gnutls.certificate_free_credentials(self._c_object)
594
def __init__(self, socket, credentials=None):
595
self._c_object = gnutls.session_t()
596
gnutls_flags = gnutls.CLIENT
597
if gnutls.check_version(b"3.5.6"):
598
gnutls_flags |= gnutls.NO_TICKETS
600
gnutls_flags |= gnutls.ENABLE_RAWPK
601
gnutls.init(ctypes.byref(self._c_object), gnutls_flags)
603
gnutls.set_default_priority(self._c_object)
604
gnutls.transport_set_ptr(self._c_object, socket.fileno())
605
gnutls.handshake_set_private_extensions(self._c_object,
608
if credentials is None:
609
credentials = gnutls.Credentials()
610
gnutls.credentials_set(self._c_object, credentials.type,
611
ctypes.cast(credentials._c_object,
613
self.credentials = credentials
616
gnutls.deinit(self._c_object)
619
return gnutls.handshake(self._c_object)
621
def send(self, data):
625
data_len -= gnutls.record_send(self._c_object,
630
return gnutls.bye(self._c_object, gnutls.SHUT_RDWR)
632
# Error handling functions
633
def _error_code(result):
634
"""A function to raise exceptions on errors, suitable
635
for the 'restype' attribute on ctypes functions"""
638
if result == gnutls.E_NO_CERTIFICATE_FOUND:
639
raise gnutls.CertificateSecurityError(code=result)
640
raise gnutls.Error(code=result)
642
def _retry_on_error(result, func, arguments):
643
"""A function to retry on some errors, suitable
644
for the 'errcheck' attribute on ctypes functions"""
646
if result not in (gnutls.E_INTERRUPTED, gnutls.E_AGAIN):
647
return _error_code(result)
648
result = func(*arguments)
651
# Unless otherwise indicated, the function declarations below are
652
# all from the gnutls/gnutls.h C header file.
655
priority_set_direct = _library.gnutls_priority_set_direct
656
priority_set_direct.argtypes = [session_t, ctypes.c_char_p,
657
ctypes.POINTER(ctypes.c_char_p)]
658
priority_set_direct.restype = _error_code
660
init = _library.gnutls_init
661
init.argtypes = [ctypes.POINTER(session_t), ctypes.c_int]
662
init.restype = _error_code
664
set_default_priority = _library.gnutls_set_default_priority
665
set_default_priority.argtypes = [session_t]
666
set_default_priority.restype = _error_code
668
record_send = _library.gnutls_record_send
669
record_send.argtypes = [session_t, ctypes.c_void_p,
671
record_send.restype = ctypes.c_ssize_t
672
record_send.errcheck = _retry_on_error
674
certificate_allocate_credentials = (
675
_library.gnutls_certificate_allocate_credentials)
676
certificate_allocate_credentials.argtypes = [
677
ctypes.POINTER(certificate_credentials_t)]
678
certificate_allocate_credentials.restype = _error_code
680
certificate_free_credentials = (
681
_library.gnutls_certificate_free_credentials)
682
certificate_free_credentials.argtypes = [
683
certificate_credentials_t]
684
certificate_free_credentials.restype = None
686
handshake_set_private_extensions = (
687
_library.gnutls_handshake_set_private_extensions)
688
handshake_set_private_extensions.argtypes = [session_t,
690
handshake_set_private_extensions.restype = None
692
credentials_set = _library.gnutls_credentials_set
693
credentials_set.argtypes = [session_t, credentials_type_t,
695
credentials_set.restype = _error_code
697
strerror = _library.gnutls_strerror
698
strerror.argtypes = [ctypes.c_int]
699
strerror.restype = ctypes.c_char_p
701
certificate_type_get = _library.gnutls_certificate_type_get
702
certificate_type_get.argtypes = [session_t]
703
certificate_type_get.restype = _error_code
705
certificate_get_peers = _library.gnutls_certificate_get_peers
706
certificate_get_peers.argtypes = [session_t,
707
ctypes.POINTER(ctypes.c_uint)]
708
certificate_get_peers.restype = ctypes.POINTER(datum_t)
710
global_set_log_level = _library.gnutls_global_set_log_level
711
global_set_log_level.argtypes = [ctypes.c_int]
712
global_set_log_level.restype = None
714
global_set_log_function = _library.gnutls_global_set_log_function
715
global_set_log_function.argtypes = [log_func]
716
global_set_log_function.restype = None
718
deinit = _library.gnutls_deinit
719
deinit.argtypes = [session_t]
720
deinit.restype = None
722
handshake = _library.gnutls_handshake
723
handshake.argtypes = [session_t]
724
handshake.restype = _error_code
725
handshake.errcheck = _retry_on_error
727
transport_set_ptr = _library.gnutls_transport_set_ptr
728
transport_set_ptr.argtypes = [session_t, transport_ptr_t]
729
transport_set_ptr.restype = None
731
bye = _library.gnutls_bye
732
bye.argtypes = [session_t, close_request_t]
733
bye.restype = _error_code
734
bye.errcheck = _retry_on_error
736
check_version = _library.gnutls_check_version
737
check_version.argtypes = [ctypes.c_char_p]
738
check_version.restype = ctypes.c_char_p
740
_need_version = b"3.3.0"
741
if check_version(_need_version) is None:
742
raise self.Error("Needs GnuTLS {} or later"
743
.format(_need_version))
745
_tls_rawpk_version = b"3.6.6"
746
has_rawpk = bool(check_version(_tls_rawpk_version))
750
class pubkey_st(ctypes.Structure):
752
pubkey_t = ctypes.POINTER(pubkey_st)
754
x509_crt_fmt_t = ctypes.c_int
756
# All the function declarations below are from gnutls/abstract.h
757
pubkey_init = _library.gnutls_pubkey_init
758
pubkey_init.argtypes = [ctypes.POINTER(pubkey_t)]
759
pubkey_init.restype = _error_code
761
pubkey_import = _library.gnutls_pubkey_import
762
pubkey_import.argtypes = [pubkey_t, ctypes.POINTER(datum_t),
764
pubkey_import.restype = _error_code
766
pubkey_get_key_id = _library.gnutls_pubkey_get_key_id
767
pubkey_get_key_id.argtypes = [pubkey_t, ctypes.c_int,
768
ctypes.POINTER(ctypes.c_ubyte),
769
ctypes.POINTER(ctypes.c_size_t)]
770
pubkey_get_key_id.restype = _error_code
772
pubkey_deinit = _library.gnutls_pubkey_deinit
773
pubkey_deinit.argtypes = [pubkey_t]
774
pubkey_deinit.restype = None
776
# All the function declarations below are from gnutls/openpgp.h
778
openpgp_crt_init = _library.gnutls_openpgp_crt_init
779
openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
780
openpgp_crt_init.restype = _error_code
782
openpgp_crt_import = _library.gnutls_openpgp_crt_import
783
openpgp_crt_import.argtypes = [openpgp_crt_t,
784
ctypes.POINTER(datum_t),
786
openpgp_crt_import.restype = _error_code
788
openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
789
openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
790
ctypes.POINTER(ctypes.c_uint)]
791
openpgp_crt_verify_self.restype = _error_code
793
openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
794
openpgp_crt_deinit.argtypes = [openpgp_crt_t]
795
openpgp_crt_deinit.restype = None
797
openpgp_crt_get_fingerprint = (
798
_library.gnutls_openpgp_crt_get_fingerprint)
799
openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
803
openpgp_crt_get_fingerprint.restype = _error_code
805
if check_version(b"3.6.4"):
806
certificate_type_get2 = _library.gnutls_certificate_type_get2
807
certificate_type_get2.argtypes = [session_t, ctypes.c_int]
808
certificate_type_get2.restype = _error_code
810
# Remove non-public functions
811
del _error_code, _retry_on_error
814
def call_pipe(connection, # : multiprocessing.Connection
815
func, *args, **kwargs):
816
"""This function is meant to be called by multiprocessing.Process
818
This function runs func(*args, **kwargs), and writes the resulting
819
return value on the provided multiprocessing.Connection.
821
connection.send(func(*args, **kwargs))
269
def _timedelta_to_milliseconds(td):
270
"Convert a datetime.timedelta() to milliseconds"
271
return ((td.days * 24 * 60 * 60 * 1000)
272
+ (td.seconds * 1000)
273
+ (td.microseconds // 1000))
275
class Client(object):
826
276
"""A representation of a client host served by this server.
829
approved: bool(); 'None' if not yet approved/disapproved
279
_approved: bool(); 'None' if not yet approved/disapproved
830
280
approval_delay: datetime.timedelta(); Time to wait for approval
831
281
approval_duration: datetime.timedelta(); Duration of one approval
832
checker: multiprocessing.Process(); a running checker process used
833
to see if the client lives. 'None' if no process is
835
checker_callback_tag: a GLib event source tag, or None
282
checker: subprocess.Popen(); a running checker process used
283
to see if the client lives.
284
'None' if no process is running.
285
checker_callback_tag: a gobject event source tag, or None
836
286
checker_command: string; External command which is run to check
837
287
if client lives. %() expansions are done at
838
288
runtime with vars(self) as dict, so that for
839
289
instance %(name)s can be used in the command.
840
checker_initiator_tag: a GLib event source tag, or None
290
checker_initiator_tag: a gobject event source tag, or None
841
291
created: datetime.datetime(); (UTC) object creation
842
client_structure: Object describing what attributes a client has
843
and is used for storing the client at exit
844
292
current_checker_command: string; current running checker_command
845
disable_initiator_tag: a GLib event source tag, or None
293
disable_hook: If set, called by disable() as disable_hook(self)
294
disable_initiator_tag: a gobject event source tag, or None
847
296
fingerprint: string (40 or 32 hexadecimal digits); used to
848
uniquely identify an OpenPGP client
849
key_id: string (64 hexadecimal digits); used to uniquely identify
850
a client using raw public keys
297
uniquely identify the client
851
298
host: string; available for use by the checker command
852
299
interval: datetime.timedelta(); How often to start a new checker
853
300
last_approval_request: datetime.datetime(); (UTC) or None
854
301
last_checked_ok: datetime.datetime(); (UTC) or None
855
last_checker_status: integer between 0 and 255 reflecting exit
856
status of last checker. -1 reflects crashed
857
checker, -2 means no checker completed yet.
858
last_checker_signal: The signal which killed the last checker, if
859
last_checker_status is -1
860
last_enabled: datetime.datetime(); (UTC) or None
302
last_enabled: datetime.datetime(); (UTC)
861
303
name: string; from the config file, used in log messages and
862
304
D-Bus identifiers
863
305
secret: bytestring; sent verbatim (over TLS) to client
864
306
timeout: datetime.timedelta(); How long from last_checked_ok
865
307
until this client is disabled
866
extended_timeout: extra long timeout when secret has been sent
308
extended_timeout: extra long timeout when password has been sent
867
309
runtime_expansions: Allowed attributes for runtime expansion.
868
310
expires: datetime.datetime(); time (UTC) when a client will be
869
311
disabled, or None
870
server_settings: The server_settings dict from main()
873
314
runtime_expansions = ("approval_delay", "approval_duration",
874
"created", "enabled", "expires", "key_id",
875
"fingerprint", "host", "interval",
876
"last_approval_request", "last_checked_ok",
315
"created", "enabled", "fingerprint",
316
"host", "interval", "last_checked_ok",
877
317
"last_enabled", "name", "timeout")
880
"extended_timeout": "PT15M",
882
"checker": "fping -q -- %%(host)s",
884
"approval_delay": "PT0S",
885
"approval_duration": "PT1S",
886
"approved_by_default": "True",
891
def config_parser(config):
892
"""Construct a new dict of client settings of this form:
893
{ client_name: {setting_name: value, ...}, ...}
894
with exceptions for any special settings as defined above.
895
NOTE: Must be a pure function. Must return the same result
896
value given the same arguments.
899
for client_name in config.sections():
900
section = dict(config.items(client_name))
901
client = settings[client_name] = {}
903
client["host"] = section["host"]
904
# Reformat values from string types to Python types
905
client["approved_by_default"] = config.getboolean(
906
client_name, "approved_by_default")
907
client["enabled"] = config.getboolean(client_name,
910
# Uppercase and remove spaces from key_id and fingerprint
911
# for later comparison purposes with return value from the
912
# key_id() and fingerprint() functions
913
client["key_id"] = (section.get("key_id", "").upper()
915
client["fingerprint"] = (section["fingerprint"].upper()
917
if "secret" in section:
918
client["secret"] = codecs.decode(section["secret"]
921
elif "secfile" in section:
922
with open(os.path.expanduser(os.path.expandvars
923
(section["secfile"])),
925
client["secret"] = secfile.read()
927
raise TypeError("No secret or secfile for section {}"
929
client["timeout"] = string_to_delta(section["timeout"])
930
client["extended_timeout"] = string_to_delta(
931
section["extended_timeout"])
932
client["interval"] = string_to_delta(section["interval"])
933
client["approval_delay"] = string_to_delta(
934
section["approval_delay"])
935
client["approval_duration"] = string_to_delta(
936
section["approval_duration"])
937
client["checker_command"] = section["checker"]
938
client["last_approval_request"] = None
939
client["last_checked_ok"] = None
940
client["last_checker_status"] = -2
944
def __init__(self, settings, name=None, server_settings=None):
319
def timeout_milliseconds(self):
320
"Return the 'timeout' attribute in milliseconds"
321
return _timedelta_to_milliseconds(self.timeout)
323
def extended_timeout_milliseconds(self):
324
"Return the 'extended_timeout' attribute in milliseconds"
325
return _timedelta_to_milliseconds(self.extended_timeout)
327
def interval_milliseconds(self):
328
"Return the 'interval' attribute in milliseconds"
329
return _timedelta_to_milliseconds(self.interval)
331
def approval_delay_milliseconds(self):
332
return _timedelta_to_milliseconds(self.approval_delay)
334
def __init__(self, name = None, disable_hook=None, config=None):
335
"""Note: the 'checker' key in 'config' sets the
336
'checker_command' attribute and *not* the 'checker'
946
if server_settings is None:
948
self.server_settings = server_settings
949
# adding all client settings
950
for setting, value in settings.items():
951
setattr(self, setting, value)
954
if not hasattr(self, "last_enabled"):
955
self.last_enabled = datetime.datetime.utcnow()
956
if not hasattr(self, "expires"):
957
self.expires = (datetime.datetime.utcnow()
960
self.last_enabled = None
963
341
logger.debug("Creating client %r", self.name)
964
logger.debug(" Key ID: %s", self.key_id)
342
# Uppercase and remove spaces from fingerprint for later
343
# comparison purposes with return value from the fingerprint()
345
self.fingerprint = (config["fingerprint"].upper()
965
347
logger.debug(" Fingerprint: %s", self.fingerprint)
966
self.created = settings.get("created",
967
datetime.datetime.utcnow())
969
# attributes specific for this server instance
348
if "secret" in config:
349
self.secret = config["secret"].decode("base64")
350
elif "secfile" in config:
351
with open(os.path.expanduser(os.path.expandvars
352
(config["secfile"])),
354
self.secret = secfile.read()
356
raise TypeError("No secret or secfile for client %s"
358
self.host = config.get("host", "")
359
self.created = datetime.datetime.utcnow()
361
self.last_approval_request = None
362
self.last_enabled = None
363
self.last_checked_ok = None
364
self.timeout = string_to_delta(config["timeout"])
365
self.extended_timeout = string_to_delta(config
366
["extended_timeout"])
367
self.interval = string_to_delta(config["interval"])
368
self.disable_hook = disable_hook
970
369
self.checker = None
971
370
self.checker_initiator_tag = None
972
371
self.disable_initiator_tag = None
973
373
self.checker_callback_tag = None
374
self.checker_command = config["checker"]
974
375
self.current_checker_command = None
376
self.last_connect = None
377
self._approved = None
378
self.approved_by_default = config.get("approved_by_default",
976
380
self.approvals_pending = 0
977
self.changedstate = multiprocessing_manager.Condition(
978
multiprocessing_manager.Lock())
979
self.client_structure = [attr
980
for attr in self.__dict__.keys()
981
if not attr.startswith("_")]
982
self.client_structure.append("client_structure")
984
for name, t in inspect.getmembers(
985
type(self), lambda obj: isinstance(obj, property)):
986
if not name.startswith("_"):
987
self.client_structure.append(name)
989
# Send notice to process children that client state has changed
381
self.approval_delay = string_to_delta(
382
config["approval_delay"])
383
self.approval_duration = string_to_delta(
384
config["approval_duration"])
385
self.changedstate = (multiprocessing_manager
386
.Condition(multiprocessing_manager
990
389
def send_changedstate(self):
991
with self.changedstate:
992
self.changedstate.notify_all()
390
self.changedstate.acquire()
391
self.changedstate.notify_all()
392
self.changedstate.release()
994
394
def enable(self):
995
395
"""Start this client's checker and timeout hooks"""
996
396
if getattr(self, "enabled", False):
997
397
# Already enabled
399
self.send_changedstate()
400
# Schedule a new checker to be started an 'interval' from now,
401
# and every interval from then on.
402
self.checker_initiator_tag = (gobject.timeout_add
403
(self.interval_milliseconds(),
405
# Schedule a disable() when 'timeout' has passed
999
406
self.expires = datetime.datetime.utcnow() + self.timeout
407
self.disable_initiator_tag = (gobject.timeout_add
408
(self.timeout_milliseconds(),
1000
410
self.enabled = True
1001
411
self.last_enabled = datetime.datetime.utcnow()
1003
self.send_changedstate()
412
# Also start a new checker *right now*.
1005
415
def disable(self, quiet=True):
1006
416
"""Disable this client."""
1007
417
if not getattr(self, "enabled", False):
420
self.send_changedstate()
1010
422
logger.info("Disabling client %s", self.name)
1011
if getattr(self, "disable_initiator_tag", None) is not None:
1012
GLib.source_remove(self.disable_initiator_tag)
423
if getattr(self, "disable_initiator_tag", False):
424
gobject.source_remove(self.disable_initiator_tag)
1013
425
self.disable_initiator_tag = None
1014
426
self.expires = None
1015
if getattr(self, "checker_initiator_tag", None) is not None:
1016
GLib.source_remove(self.checker_initiator_tag)
427
if getattr(self, "checker_initiator_tag", False):
428
gobject.source_remove(self.checker_initiator_tag)
1017
429
self.checker_initiator_tag = None
1018
430
self.stop_checker()
431
if self.disable_hook:
432
self.disable_hook(self)
1019
433
self.enabled = False
1021
self.send_changedstate()
1022
# Do not run this again if called by a GLib.timeout_add
434
# Do not run this again if called by a gobject.timeout_add
1025
437
def __del__(self):
438
self.disable_hook = None
1028
def init_checker(self):
1029
# Schedule a new checker to be started an 'interval' from now,
1030
# and every interval from then on.
1031
if self.checker_initiator_tag is not None:
1032
GLib.source_remove(self.checker_initiator_tag)
1033
self.checker_initiator_tag = GLib.timeout_add(
1034
int(self.interval.total_seconds() * 1000),
1036
# Schedule a disable() when 'timeout' has passed
1037
if self.disable_initiator_tag is not None:
1038
GLib.source_remove(self.disable_initiator_tag)
1039
self.disable_initiator_tag = GLib.timeout_add(
1040
int(self.timeout.total_seconds() * 1000), self.disable)
1041
# Also start a new checker *right now*.
1042
self.start_checker()
1044
def checker_callback(self, source, condition, connection,
441
def checker_callback(self, pid, condition, command):
1046
442
"""The checker has completed, so take appropriate actions."""
1047
# Read return code from connection (see call_pipe)
1048
returncode = connection.recv()
1051
443
self.checker_callback_tag = None
1052
444
self.checker = None
1055
self.last_checker_status = returncode
1056
self.last_checker_signal = None
1057
if self.last_checker_status == 0:
445
if os.WIFEXITED(condition):
446
exitstatus = os.WEXITSTATUS(condition)
1058
448
logger.info("Checker for %(name)s succeeded",
1060
450
self.checked_ok()
1062
logger.info("Checker for %(name)s failed", vars(self))
452
logger.info("Checker for %(name)s failed",
1064
self.last_checker_status = -1
1065
self.last_checker_signal = -returncode
1066
455
logger.warning("Checker for %(name)s crashed?",
1070
def checked_ok(self):
1071
"""Assert that the client has been seen, alive and well."""
1072
self.last_checked_ok = datetime.datetime.utcnow()
1073
self.last_checker_status = 0
1074
self.last_checker_signal = None
1077
def bump_timeout(self, timeout=None):
1078
"""Bump up the timeout for this client."""
458
def checked_ok(self, timeout=None):
459
"""Bump up the timeout for this client.
461
This should only be called when the client has been seen,
1079
464
if timeout is None:
1080
465
timeout = self.timeout
466
self.last_checked_ok = datetime.datetime.utcnow()
1081
467
if self.disable_initiator_tag is not None:
1082
GLib.source_remove(self.disable_initiator_tag)
1083
self.disable_initiator_tag = None
468
gobject.source_remove(self.disable_initiator_tag)
1084
469
if getattr(self, "enabled", False):
1085
self.disable_initiator_tag = GLib.timeout_add(
1086
int(timeout.total_seconds() * 1000), self.disable)
470
self.disable_initiator_tag = (gobject.timeout_add
471
(_timedelta_to_milliseconds
472
(timeout), self.disable))
1087
473
self.expires = datetime.datetime.utcnow() + timeout
1089
475
def need_approval(self):
1090
476
self.last_approval_request = datetime.datetime.utcnow()
1092
478
def start_checker(self):
1093
479
"""Start a new checker subprocess if one is not running.
1095
481
If a checker already exists, leave it running and do
1097
483
# The reason for not killing a running checker is that if we
1098
# did that, and if a checker (for some reason) started running
1099
# slowly and taking more than 'interval' time, then the client
1100
# would inevitably timeout, since no checker would get a
1101
# chance to run to completion. If we instead leave running
484
# did that, then if a checker (for some reason) started
485
# running slowly and taking more than 'interval' time, the
486
# client would inevitably timeout, since no checker would get
487
# a chance to run to completion. If we instead leave running
1102
488
# checkers alone, the checker would have to take more time
1103
489
# than 'timeout' for the client to be disabled, which is as it
1106
if self.checker is not None and not self.checker.is_alive():
1107
logger.warning("Checker was not alive; joining")
492
# If a checker exists, make sure it is not a zombie
494
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
495
except (AttributeError, OSError) as error:
496
if (isinstance(error, OSError)
497
and error.errno != errno.ECHILD):
501
logger.warning("Checker was a zombie")
502
gobject.source_remove(self.checker_callback_tag)
503
self.checker_callback(pid, status,
504
self.current_checker_command)
1110
505
# Start a new checker if needed
1111
506
if self.checker is None:
1112
# Escape attributes for the shell
1114
attr: re.escape(str(getattr(self, attr)))
1115
for attr in self.runtime_expansions}
1117
command = self.checker_command % escaped_attrs
1118
except TypeError as error:
1119
logger.error('Could not format string "%s"',
1120
self.checker_command,
1122
return True # Try again later
508
# In case checker_command has exactly one % operator
509
command = self.checker_command % self.host
511
# Escape attributes for the shell
512
escaped_attrs = dict(
514
re.escape(unicode(str(getattr(self, attr, "")),
518
self.runtime_expansions)
521
command = self.checker_command % escaped_attrs
522
except TypeError as error:
523
logger.error('Could not format string "%s":'
524
' %s', self.checker_command, error)
525
return True # Try again later
1123
526
self.current_checker_command = command
1124
logger.info("Starting checker %r for %s", command,
1126
# We don't need to redirect stdout and stderr, since
1127
# in normal mode, that is already done by daemon(),
1128
# and in debug mode we don't want to. (Stdin is
1129
# always replaced by /dev/null.)
1130
# The exception is when not debugging but nevertheless
1131
# running in the foreground; use the previously
1133
popen_args = {"close_fds": True,
1136
if (not self.server_settings["debug"]
1137
and self.server_settings["foreground"]):
1138
popen_args.update({"stdout": wnull,
1140
pipe = multiprocessing.Pipe(duplex=False)
1141
self.checker = multiprocessing.Process(
1143
args=(pipe[1], subprocess.call, command),
1145
self.checker.start()
1146
self.checker_callback_tag = GLib.io_add_watch(
1147
pipe[0].fileno(), GLib.IO_IN,
1148
self.checker_callback, pipe[0], command)
1149
# Re-run this periodically if run by GLib.timeout_add
528
logger.info("Starting checker %r for %s",
530
# We don't need to redirect stdout and stderr, since
531
# in normal mode, that is already done by daemon(),
532
# and in debug mode we don't want to. (Stdin is
533
# always replaced by /dev/null.)
534
self.checker = subprocess.Popen(command,
537
self.checker_callback_tag = (gobject.child_watch_add
539
self.checker_callback,
541
# The checker may have completed before the gobject
542
# watch was added. Check for this.
543
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
545
gobject.source_remove(self.checker_callback_tag)
546
self.checker_callback(pid, status, command)
547
except OSError as error:
548
logger.error("Failed to start subprocess: %s",
550
# Re-run this periodically if run by gobject.timeout_add
1152
553
def stop_checker(self):
1153
554
"""Force the checker process, if any, to stop."""
1154
555
if self.checker_callback_tag:
1155
GLib.source_remove(self.checker_callback_tag)
556
gobject.source_remove(self.checker_callback_tag)
1156
557
self.checker_callback_tag = None
1157
558
if getattr(self, "checker", None) is None:
1159
560
logger.debug("Stopping checker for %(name)s", vars(self))
1160
self.checker.terminate()
562
os.kill(self.checker.pid, signal.SIGTERM)
564
#if self.checker.poll() is None:
565
# os.kill(self.checker.pid, signal.SIGKILL)
566
except OSError as error:
567
if error.errno != errno.ESRCH: # No such process
1161
569
self.checker = None
1164
def dbus_service_property(dbus_interface,
572
def dbus_service_property(dbus_interface, signature="v",
573
access="readwrite", byte_arrays=False):
1168
574
"""Decorators for marking methods of a DBusObjectWithProperties to
1169
575
become properties on the D-Bus.
1171
577
The decorated method will be called with no arguments by "Get"
1172
578
and with one argument by "Set".
1174
580
The parameters, where they are supported, are the same as
1175
581
dbus.service.method, except there is only "signature", since the
1176
582
type from Get() and the type sent to Set() is the same.
1509
752
except (AttributeError, xml.dom.DOMException,
1510
753
xml.parsers.expat.ExpatError) as error:
1511
754
logger.error("Failed to override Introspection method",
1517
dbus.OBJECT_MANAGER_IFACE
1518
except AttributeError:
1519
dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
1522
class DBusObjectWithObjectManager(DBusObjectWithAnnotations):
1523
"""A D-Bus object with an ObjectManager.
1525
Classes inheriting from this exposes the standard
1526
GetManagedObjects call and the InterfacesAdded and
1527
InterfacesRemoved signals on the standard
1528
"org.freedesktop.DBus.ObjectManager" interface.
1530
Note: No signals are sent automatically; they must be sent
1533
@dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
1534
out_signature="a{oa{sa{sv}}}")
1535
def GetManagedObjects(self):
1536
"""This function must be overridden"""
1537
raise NotImplementedError()
1539
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE,
1540
signature="oa{sa{sv}}")
1541
def InterfacesAdded(self, object_path, interfaces_and_properties):
1544
@dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature="oas")
1545
def InterfacesRemoved(self, object_path, interfaces):
1548
@dbus.service.method(dbus.INTROSPECTABLE_IFACE,
1550
path_keyword='object_path',
1551
connection_keyword='connection')
1552
def Introspect(self, object_path, connection):
1553
"""Overloading of standard D-Bus method.
1555
Override return argument name of GetManagedObjects to be
1556
"objpath_interfaces_and_properties"
1558
xmlstring = DBusObjectWithAnnotations.Introspect(self,
1562
document = xml.dom.minidom.parseString(xmlstring)
1564
for if_tag in document.getElementsByTagName("interface"):
1565
# Fix argument name for the GetManagedObjects method
1566
if (if_tag.getAttribute("name")
1567
== dbus.OBJECT_MANAGER_IFACE):
1568
for cn in if_tag.getElementsByTagName("method"):
1569
if (cn.getAttribute("name")
1570
== "GetManagedObjects"):
1571
for arg in cn.getElementsByTagName("arg"):
1572
if (arg.getAttribute("direction")
1576
"objpath_interfaces"
1578
xmlstring = document.toxml("utf-8")
1580
except (AttributeError, xml.dom.DOMException,
1581
xml.parsers.expat.ExpatError) as error:
1582
logger.error("Failed to override Introspection method",
1587
def datetime_to_dbus(dt, variant_level=0):
759
def datetime_to_dbus (dt, variant_level=0):
1588
760
"""Convert a UTC datetime.datetime() to a D-Bus type."""
1590
return dbus.String("", variant_level=variant_level)
1591
return dbus.String(dt.isoformat(), variant_level=variant_level)
1594
def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
1595
"""A class decorator; applied to a subclass of
1596
dbus.service.Object, it will add alternate D-Bus attributes with
1597
interface names according to the "alt_interface_names" mapping.
1600
@alternate_dbus_interfaces({"org.example.Interface":
1601
"net.example.AlternateInterface"})
1602
class SampleDBusObject(dbus.service.Object):
1603
@dbus.service.method("org.example.Interface")
1604
def SampleDBusMethod():
1607
The above "SampleDBusMethod" on "SampleDBusObject" will be
1608
reachable via two interfaces: "org.example.Interface" and
1609
"net.example.AlternateInterface", the latter of which will have
1610
its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
1611
"true", unless "deprecate" is passed with a False value.
1613
This works for methods and signals, and also for D-Bus properties
1614
(from DBusObjectWithProperties) and interfaces (from the
1615
dbus_interface_annotations decorator).
762
return dbus.String("", variant_level = variant_level)
763
return dbus.String(dt.isoformat(),
764
variant_level=variant_level)
766
class AlternateDBusNamesMetaclass(DBusObjectWithProperties
768
"""Applied to an empty subclass of a D-Bus object, this metaclass
769
will add additional D-Bus attributes matching a certain pattern.
1619
for orig_interface_name, alt_interface_name in (
1620
alt_interface_names.items()):
1622
interface_names = set()
1623
# Go though all attributes of the class
1624
for attrname, attribute in inspect.getmembers(cls):
771
def __new__(mcs, name, bases, attr):
772
# Go through all the base classes which could have D-Bus
773
# methods, signals, or properties in them
774
for base in (b for b in bases
775
if issubclass(b, dbus.service.Object)):
776
# Go though all attributes of the base class
777
for attrname, attribute in inspect.getmembers(base):
1625
778
# Ignore non-D-Bus attributes, and D-Bus attributes
1626
779
# with the wrong interface name
1627
780
if (not hasattr(attribute, "_dbus_interface")
1628
or not attribute._dbus_interface.startswith(
1629
orig_interface_name)):
781
or not attribute._dbus_interface
782
.startswith("se.recompile.Mandos")):
1631
784
# Create an alternate D-Bus interface name based on
1632
785
# the current name
1633
alt_interface = attribute._dbus_interface.replace(
1634
orig_interface_name, alt_interface_name)
1635
interface_names.add(alt_interface)
786
alt_interface = (attribute._dbus_interface
787
.replace("se.recompile.Mandos",
788
"se.bsnet.fukt.Mandos"))
1636
789
# Is this a D-Bus signal?
1637
790
if getattr(attribute, "_dbus_is_signal", False):
1638
# Extract the original non-method undecorated
1639
# function by black magic
1640
if sys.version_info.major == 2:
1641
nonmethod_func = (dict(
791
# Extract the original non-method function by
793
nonmethod_func = (dict(
1642
794
zip(attribute.func_code.co_freevars,
1643
attribute.__closure__))
1644
["func"].cell_contents)
1646
nonmethod_func = (dict(
1647
zip(attribute.__code__.co_freevars,
1648
attribute.__closure__))
1649
["func"].cell_contents)
795
attribute.__closure__))["func"]
1650
797
# Create a new, but exactly alike, function
1651
798
# object, and decorate it to be a new D-Bus signal
1652
799
# with the alternate D-Bus interface name
1653
new_function = copy_function(nonmethod_func)
1654
new_function = (dbus.service.signal(
1656
attribute._dbus_signature)(new_function))
1657
# Copy annotations, if any
1659
new_function._dbus_annotations = dict(
1660
attribute._dbus_annotations)
1661
except AttributeError:
800
new_function = (dbus.service.signal
802
attribute._dbus_signature)
804
nonmethod_func.func_code,
805
nonmethod_func.func_globals,
806
nonmethod_func.func_name,
807
nonmethod_func.func_defaults,
808
nonmethod_func.func_closure)))
1664
809
# Define a creator of a function to call both the
1665
# original and alternate functions, so both the
1666
# original and alternate signals gets sent when
1667
# the function is called
810
# old and new functions, so both the old and new
811
# signals gets sent when the function is called
1668
812
def fixscope(func1, func2):
1669
813
"""This function is a scope container to pass
1670
814
func1 and func2 to the "call_both" function
1671
815
outside of its arguments"""
1673
@functools.wraps(func2)
1674
816
def call_both(*args, **kwargs):
1675
817
"""This function will emit two D-Bus
1676
818
signals by calling func1 and func2"""
1677
819
func1(*args, **kwargs)
1678
820
func2(*args, **kwargs)
1679
# Make wrapper function look like a D-Bus
1681
for name, attr in inspect.getmembers(func2):
1682
if name.startswith("_dbus_"):
1683
setattr(call_both, name, attr)
1685
821
return call_both
1686
822
# Create the "call_both" function and add it to
1688
attr[attrname] = fixscope(attribute, new_function)
824
attr[attrname] = fixscope(attribute,
1689
826
# Is this a D-Bus method?
1690
827
elif getattr(attribute, "_dbus_is_method", False):
1691
828
# Create a new, but exactly alike, function
1692
829
# object. Decorate it to be a new D-Bus method
1693
830
# with the alternate D-Bus interface name. Add it
1696
dbus.service.method(
1698
attribute._dbus_in_signature,
1699
attribute._dbus_out_signature)
1700
(copy_function(attribute)))
1701
# Copy annotations, if any
1703
attr[attrname]._dbus_annotations = dict(
1704
attribute._dbus_annotations)
1705
except AttributeError:
832
attr[attrname] = (dbus.service.method
834
attribute._dbus_in_signature,
835
attribute._dbus_out_signature)
837
(attribute.func_code,
838
attribute.func_globals,
840
attribute.func_defaults,
841
attribute.func_closure)))
1707
842
# Is this a D-Bus property?
1708
843
elif getattr(attribute, "_dbus_is_property", False):
1709
844
# Create a new, but exactly alike, function
1710
845
# object, and decorate it to be a new D-Bus
1711
846
# property with the alternate D-Bus interface
1712
847
# name. Add it to the class.
1713
attr[attrname] = (dbus_service_property(
1714
alt_interface, attribute._dbus_signature,
1715
attribute._dbus_access,
1716
attribute._dbus_get_args_options
1718
(copy_function(attribute)))
1719
# Copy annotations, if any
1721
attr[attrname]._dbus_annotations = dict(
1722
attribute._dbus_annotations)
1723
except AttributeError:
1725
# Is this a D-Bus interface?
1726
elif getattr(attribute, "_dbus_is_interface", False):
1727
# Create a new, but exactly alike, function
1728
# object. Decorate it to be a new D-Bus interface
1729
# with the alternate D-Bus interface name. Add it
1732
dbus_interface_annotations(alt_interface)
1733
(copy_function(attribute)))
1735
# Deprecate all alternate interfaces
1736
iname = "_AlternateDBusNames_interface_annotation{}"
1737
for interface_name in interface_names:
1739
@dbus_interface_annotations(interface_name)
1741
return {"org.freedesktop.DBus.Deprecated":
1743
# Find an unused name
1744
for aname in (iname.format(i)
1745
for i in itertools.count()):
1746
if aname not in attr:
1750
# Replace the class with a new subclass of it with
1751
# methods, signals, etc. as created above.
1752
if sys.version_info.major == 2:
1753
cls = type(b"{}Alternate".format(cls.__name__),
1756
cls = type("{}Alternate".format(cls.__name__),
1763
@alternate_dbus_interfaces({"se.recompile.Mandos":
1764
"se.bsnet.fukt.Mandos"})
848
attr[attrname] = (dbus_service_property
850
attribute._dbus_signature,
851
attribute._dbus_access,
853
._dbus_get_args_options
856
(attribute.func_code,
857
attribute.func_globals,
859
attribute.func_defaults,
860
attribute.func_closure)))
861
return type.__new__(mcs, name, bases, attr)
1765
863
class ClientDBus(Client, DBusObjectWithProperties):
1766
864
"""A Client class using D-Bus
1769
867
dbus_object_path: dbus.ObjectPath
1770
868
bus: dbus.SystemBus()
1773
871
runtime_expansions = (Client.runtime_expansions
1774
+ ("dbus_object_path", ))
1776
_interface = "se.recompile.Mandos.Client"
872
+ ("dbus_object_path",))
1778
874
# dbus.service.Object doesn't use super(), so we can't either.
1780
def __init__(self, bus=None, *args, **kwargs):
876
def __init__(self, bus = None, *args, **kwargs):
877
self._approvals_pending = 0
1782
879
Client.__init__(self, *args, **kwargs)
1783
880
# Only now, when this client is initialized, can it show up on
1785
client_object_name = str(self.name).translate(
882
client_object_name = unicode(self.name).translate(
1786
883
{ord("."): ord("_"),
1787
884
ord("-"): ord("_")})
1788
self.dbus_object_path = dbus.ObjectPath(
1789
"/clients/" + client_object_name)
885
self.dbus_object_path = (dbus.ObjectPath
886
("/clients/" + client_object_name))
1790
887
DBusObjectWithProperties.__init__(self, self.bus,
1791
888
self.dbus_object_path)
1793
def notifychangeproperty(transform_func, dbus_name,
1794
type_func=lambda x: x,
1796
invalidate_only=False,
1797
_interface=_interface):
890
def notifychangeproperty(transform_func,
891
dbus_name, type_func=lambda x: x,
1798
893
""" Modify a variable so that it's a property which announces
1799
894
its changes to DBus.
2194
1246
self.start_checker()
2196
1248
self.stop_checker()
2198
1250
# ObjectPath - property
2200
{"org.freedesktop.DBus.Property.EmitsChangedSignal": "const",
2201
"org.freedesktop.DBus.Deprecated": "true"})
2202
1251
@dbus_service_property(_interface, signature="o", access="read")
2203
1252
def ObjectPath_dbus_property(self):
2204
return self.dbus_object_path # is already a dbus.ObjectPath
1253
return self.dbus_object_path # is already a dbus.ObjectPath
2206
1255
# Secret = property
2208
{"org.freedesktop.DBus.Property.EmitsChangedSignal":
2210
@dbus_service_property(_interface,
1256
@dbus_service_property(_interface, signature="ay",
1257
access="write", byte_arrays=True)
2214
1258
def Secret_dbus_property(self, value):
2215
self.secret = bytes(value)
1259
self.secret = str(value)
2221
def __init__(self, child_pipe, key_id, fpr, address):
1264
class ProxyClient(object):
1265
def __init__(self, child_pipe, fpr, address):
2222
1266
self._pipe = child_pipe
2223
self._pipe.send(('init', key_id, fpr, address))
1267
self._pipe.send(('init', fpr, address))
2224
1268
if not self._pipe.recv():
2225
raise KeyError(key_id or fpr)
2227
1271
def __getattribute__(self, name):
1272
if(name == '_pipe'):
2229
1273
return super(ProxyClient, self).__getattribute__(name)
2230
1274
self._pipe.send(('getattr', name))
2231
1275
data = self._pipe.recv()
2232
1276
if data[0] == 'data':
2234
1278
if data[0] == 'function':
2236
1279
def func(*args, **kwargs):
2237
1280
self._pipe.send(('funcall', name, args, kwargs))
2238
1281
return self._pipe.recv()[1]
2242
1284
def __setattr__(self, name, value):
1285
if(name == '_pipe'):
2244
1286
return super(ProxyClient, self).__setattr__(name, value)
2245
1287
self._pipe.send(('setattr', name, value))
1289
class ClientDBusTransitional(ClientDBus):
1290
__metaclass__ = AlternateDBusNamesMetaclass
2248
1292
class ClientHandler(socketserver.BaseRequestHandler, object):
2249
1293
"""A class to handle client connections.
2251
1295
Instantiated once for each connection to handle it.
2252
1296
Note: This will run in its own forked process."""
2254
1298
def handle(self):
2255
1299
with contextlib.closing(self.server.child_pipe) as child_pipe:
2256
1300
logger.info("TCP connection from: %s",
2257
str(self.client_address))
1301
unicode(self.client_address))
2258
1302
logger.debug("Pipe FD: %d",
2259
1303
self.server.child_pipe.fileno())
2261
session = gnutls.ClientSession(self.request)
2263
# priority = ':'.join(("NONE", "+VERS-TLS1.1",
2264
# "+AES-256-CBC", "+SHA1",
2265
# "+COMP-NULL", "+CTYPE-OPENPGP",
1305
session = (gnutls.connection
1306
.ClientSession(self.request,
1308
.X509Credentials()))
1310
# Note: gnutls.connection.X509Credentials is really a
1311
# generic GnuTLS certificate credentials object so long as
1312
# no X.509 keys are added to it. Therefore, we can use it
1313
# here despite using OpenPGP certificates.
1315
#priority = ':'.join(("NONE", "+VERS-TLS1.1",
1316
# "+AES-256-CBC", "+SHA1",
1317
# "+COMP-NULL", "+CTYPE-OPENPGP",
2267
1319
# Use a fallback default, since this MUST be set.
2268
1320
priority = self.server.gnutls_priority
2269
1321
if priority is None:
2270
1322
priority = "NORMAL"
2271
gnutls.priority_set_direct(session._c_object,
2272
priority.encode("utf-8"),
1323
(gnutls.library.functions
1324
.gnutls_priority_set_direct(session._c_object,
2275
1327
# Start communication using the Mandos protocol
2276
1328
# Get protocol number
2277
1329
line = self.request.makefile().readline()
2278
1330
logger.debug("Protocol version: %r", line)
2280
1332
if int(line.strip().split()[0]) > 1:
2281
raise RuntimeError(line)
2282
1334
except (ValueError, IndexError, RuntimeError) as error:
2283
1335
logger.error("Unknown protocol version: %s", error)
2286
1338
# Start GnuTLS connection
2288
1340
session.handshake()
2289
except gnutls.Error as error:
1341
except gnutls.errors.GNUTLSError as error:
2290
1342
logger.warning("Handshake failed: %s", error)
2291
1343
# Do not run session.bye() here: the session is not
2292
1344
# established. Just abandon the request.
2294
1346
logger.debug("Handshake succeeded")
2296
1348
approval_required = False
2298
if gnutls.has_rawpk:
2301
key_id = self.key_id(
2302
self.peer_certificate(session))
2303
except (TypeError, gnutls.Error) as error:
2304
logger.warning("Bad certificate: %s", error)
2306
logger.debug("Key ID: %s", key_id)
2311
fpr = self.fingerprint(
2312
self.peer_certificate(session))
2313
except (TypeError, gnutls.Error) as error:
2314
logger.warning("Bad certificate: %s", error)
2316
logger.debug("Fingerprint: %s", fpr)
2319
client = ProxyClient(child_pipe, key_id, fpr,
1351
fpr = self.fingerprint(self.peer_certificate
1354
gnutls.errors.GNUTLSError) as error:
1355
logger.warning("Bad certificate: %s", error)
1357
logger.debug("Fingerprint: %s", fpr)
1360
client = ProxyClient(child_pipe, fpr,
2320
1361
self.client_address)
2321
1362
except KeyError:
2324
1365
if client.approval_delay:
2325
1366
delay = client.approval_delay
2326
1367
client.approvals_pending += 1
2327
1368
approval_required = True
2330
1371
if not client.enabled:
2331
1372
logger.info("Client %s is disabled",
2333
1374
if self.server.use_dbus:
2334
1375
# Emit D-Bus signal
2335
1376
client.Rejected("Disabled")
2338
if client.approved or not client.approval_delay:
2339
# We are approved or approval is disabled
1379
if client._approved or not client.approval_delay:
1380
#We are approved or approval is disabled
2341
elif client.approved is None:
1382
elif client._approved is None:
2342
1383
logger.info("Client %s needs approval",
2344
1385
if self.server.use_dbus:
2345
1386
# Emit D-Bus signal
2346
1387
client.NeedApproval(
2347
client.approval_delay.total_seconds()
2348
* 1000, client.approved_by_default)
1388
client.approval_delay_milliseconds(),
1389
client.approved_by_default)
2350
1391
logger.warning("Client %s was not approved",
2375
1418
delay -= time2 - time
2378
session.send(client.secret)
2379
except gnutls.Error as error:
2380
logger.warning("gnutls send failed",
1421
while sent_size < len(client.secret):
1423
sent = session.send(client.secret[sent_size:])
1424
except gnutls.errors.GNUTLSError as error:
1425
logger.warning("gnutls send failed")
1427
logger.debug("Sent: %d, remaining: %d",
1428
sent, len(client.secret)
1429
- (sent_size + sent))
2384
1432
logger.info("Sending secret to %s", client.name)
2385
1433
# bump the timeout using extended_timeout
2386
client.bump_timeout(client.extended_timeout)
1434
client.checked_ok(client.extended_timeout)
2387
1435
if self.server.use_dbus:
2388
1436
# Emit D-Bus signal
2389
1437
client.GotSecret()
2392
1440
if approval_required:
2393
1441
client.approvals_pending -= 1
2396
except gnutls.Error as error:
2397
logger.warning("GnuTLS bye failed",
1444
except gnutls.errors.GNUTLSError as error:
1445
logger.warning("GnuTLS bye failed")
2401
1448
def peer_certificate(session):
2402
"Return the peer's certificate as a bytestring"
2404
cert_type = gnutls.certificate_type_get2(session._c_object,
2406
except AttributeError:
2407
cert_type = gnutls.certificate_type_get(session._c_object)
2408
if gnutls.has_rawpk:
2409
valid_cert_types = frozenset((gnutls.CRT_RAWPK,))
2411
valid_cert_types = frozenset((gnutls.CRT_OPENPGP,))
2412
# If not a valid certificate type...
2413
if cert_type not in valid_cert_types:
2414
logger.info("Cert type %r not in %r", cert_type,
2416
# ...return invalid data
1449
"Return the peer's OpenPGP certificate as a bytestring"
1450
# If not an OpenPGP certificate...
1451
if (gnutls.library.functions
1452
.gnutls_certificate_type_get(session._c_object)
1453
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1454
# ...do the normal thing
1455
return session.peer_certificate
2418
1456
list_size = ctypes.c_uint(1)
2419
cert_list = (gnutls.certificate_get_peers
1457
cert_list = (gnutls.library.functions
1458
.gnutls_certificate_get_peers
2420
1459
(session._c_object, ctypes.byref(list_size)))
2421
1460
if not bool(cert_list) and list_size.value != 0:
2422
raise gnutls.Error("error getting peer certificate")
1461
raise gnutls.errors.GNUTLSError("error getting peer"
2423
1463
if list_size.value == 0:
2425
1465
cert = cert_list[0]
2426
1466
return ctypes.string_at(cert.data, cert.size)
2429
def key_id(certificate):
2430
"Convert a certificate bytestring to a hexdigit key ID"
2431
# New GnuTLS "datum" with the public key
2432
datum = gnutls.datum_t(
2433
ctypes.cast(ctypes.c_char_p(certificate),
2434
ctypes.POINTER(ctypes.c_ubyte)),
2435
ctypes.c_uint(len(certificate)))
2436
# XXX all these need to be created in the gnutls "module"
2437
# New empty GnuTLS certificate
2438
pubkey = gnutls.pubkey_t()
2439
gnutls.pubkey_init(ctypes.byref(pubkey))
2440
# Import the raw public key into the certificate
2441
gnutls.pubkey_import(pubkey,
2442
ctypes.byref(datum),
2443
gnutls.X509_FMT_DER)
2444
# New buffer for the key ID
2445
buf = ctypes.create_string_buffer(32)
2446
buf_len = ctypes.c_size_t(len(buf))
2447
# Get the key ID from the raw public key into the buffer
2448
gnutls.pubkey_get_key_id(pubkey,
2449
gnutls.KEYID_USE_SHA256,
2450
ctypes.cast(ctypes.byref(buf),
2451
ctypes.POINTER(ctypes.c_ubyte)),
2452
ctypes.byref(buf_len))
2453
# Deinit the certificate
2454
gnutls.pubkey_deinit(pubkey)
2456
# Convert the buffer to a Python bytestring
2457
key_id = ctypes.string_at(buf, buf_len.value)
2458
# Convert the bytestring to hexadecimal notation
2459
hex_key_id = binascii.hexlify(key_id).upper()
2463
1469
def fingerprint(openpgp):
2464
1470
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
2465
1471
# New GnuTLS "datum" with the OpenPGP public key
2466
datum = gnutls.datum_t(
2467
ctypes.cast(ctypes.c_char_p(openpgp),
2468
ctypes.POINTER(ctypes.c_ubyte)),
2469
ctypes.c_uint(len(openpgp)))
1472
datum = (gnutls.library.types
1473
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1476
ctypes.c_uint(len(openpgp))))
2470
1477
# New empty GnuTLS certificate
2471
crt = gnutls.openpgp_crt_t()
2472
gnutls.openpgp_crt_init(ctypes.byref(crt))
1478
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1479
(gnutls.library.functions
1480
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
2473
1481
# Import the OpenPGP public key into the certificate
2474
gnutls.openpgp_crt_import(crt, ctypes.byref(datum),
2475
gnutls.OPENPGP_FMT_RAW)
1482
(gnutls.library.functions
1483
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1484
gnutls.library.constants
1485
.GNUTLS_OPENPGP_FMT_RAW))
2476
1486
# Verify the self signature in the key
2477
1487
crtverify = ctypes.c_uint()
2478
gnutls.openpgp_crt_verify_self(crt, 0,
2479
ctypes.byref(crtverify))
1488
(gnutls.library.functions
1489
.gnutls_openpgp_crt_verify_self(crt, 0,
1490
ctypes.byref(crtverify)))
2480
1491
if crtverify.value != 0:
2481
gnutls.openpgp_crt_deinit(crt)
2482
raise gnutls.CertificateSecurityError(code
1492
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1493
raise (gnutls.errors.CertificateSecurityError
2484
1495
# New buffer for the fingerprint
2485
1496
buf = ctypes.create_string_buffer(20)
2486
1497
buf_len = ctypes.c_size_t()
2487
1498
# Get the fingerprint from the certificate into the buffer
2488
gnutls.openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
2489
ctypes.byref(buf_len))
1499
(gnutls.library.functions
1500
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1501
ctypes.byref(buf_len)))
2490
1502
# Deinit the certificate
2491
gnutls.openpgp_crt_deinit(crt)
1503
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
2492
1504
# Convert the buffer to a Python bytestring
2493
1505
fpr = ctypes.string_at(buf, buf_len.value)
2494
1506
# Convert the bytestring to hexadecimal notation
2495
hex_fpr = binascii.hexlify(fpr).upper()
1507
hex_fpr = ''.join("%02X" % ord(char) for char in fpr)
2499
class MultiprocessingMixIn:
1511
class MultiprocessingMixIn(object):
2500
1512
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
2502
1513
def sub_process_main(self, request, address):
2504
1515
self.finish_request(request, address)
2506
1517
self.handle_error(request, address)
2507
1518
self.close_request(request)
2509
1520
def process_request(self, request, address):
2510
1521
"""Start a new process to process the request."""
2511
proc = multiprocessing.Process(target=self.sub_process_main,
2512
args=(request, address))
1522
proc = multiprocessing.Process(target = self.sub_process_main,
2517
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
1529
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
2518
1530
""" adds a pipe to the MixIn """
2520
1531
def process_request(self, request, client_address):
2521
1532
"""Overrides and wraps the original process_request().
2523
1534
This function creates a new pipe in self.pipe
2525
1536
parent_pipe, self.child_pipe = multiprocessing.Pipe()
2527
1538
proc = MultiprocessingMixIn.process_request(self, request,
2528
1539
client_address)
2529
1540
self.child_pipe.close()
2530
1541
self.add_pipe(parent_pipe, proc)
2532
1543
def add_pipe(self, parent_pipe, proc):
2533
1544
"""Dummy function; override as necessary"""
2534
raise NotImplementedError()
1545
raise NotImplementedError
2537
1548
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2538
socketserver.TCPServer):
1549
socketserver.TCPServer, object):
2539
1550
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
2542
1553
enabled: Boolean; whether this server is activated yet
2543
1554
interface: None or a network interface name (string)
2544
1555
use_ipv6: Boolean; to use IPv6 or not
2547
1557
def __init__(self, server_address, RequestHandlerClass,
2551
"""If socketfd is set, use that file descriptor instead of
2552
creating a new one with socket.socket().
1558
interface=None, use_ipv6=True):
2554
1559
self.interface = interface
2556
1561
self.address_family = socket.AF_INET6
2557
if socketfd is not None:
2558
# Save the file descriptor
2559
self.socketfd = socketfd
2560
# Save the original socket.socket() function
2561
self.socket_socket = socket.socket
2563
# To implement --socket, we monkey patch socket.socket.
2565
# (When socketserver.TCPServer is a new-style class, we
2566
# could make self.socket into a property instead of monkey
2567
# patching socket.socket.)
2569
# Create a one-time-only replacement for socket.socket()
2570
@functools.wraps(socket.socket)
2571
def socket_wrapper(*args, **kwargs):
2572
# Restore original function so subsequent calls are
2574
socket.socket = self.socket_socket
2575
del self.socket_socket
2576
# This time only, return a new socket object from the
2577
# saved file descriptor.
2578
return socket.fromfd(self.socketfd, *args, **kwargs)
2579
# Replace socket.socket() function with wrapper
2580
socket.socket = socket_wrapper
2581
# The socketserver.TCPServer.__init__ will call
2582
# socket.socket(), which might be our replacement,
2583
# socket_wrapper(), if socketfd was set.
2584
1562
socketserver.TCPServer.__init__(self, server_address,
2585
1563
RequestHandlerClass)
2587
1564
def server_bind(self):
2588
1565
"""This overrides the normal server_bind() function
2589
1566
to bind to an interface if one was specified, and also NOT to
2590
1567
bind to an address or port if they were not specified."""
2591
global SO_BINDTODEVICE
2592
1568
if self.interface is not None:
2593
1569
if SO_BINDTODEVICE is None:
2594
# Fall back to a hard-coded value which seems to be
2596
logger.warning("SO_BINDTODEVICE not found, trying 25")
2597
SO_BINDTODEVICE = 25
2599
self.socket.setsockopt(
2600
socket.SOL_SOCKET, SO_BINDTODEVICE,
2601
(self.interface + "\0").encode("utf-8"))
2602
except socket.error as error:
2603
if error.errno == errno.EPERM:
2604
logger.error("No permission to bind to"
2605
" interface %s", self.interface)
2606
elif error.errno == errno.ENOPROTOOPT:
2607
logger.error("SO_BINDTODEVICE not available;"
2608
" cannot bind to interface %s",
2610
elif error.errno == errno.ENODEV:
2611
logger.error("Interface %s does not exist,"
2612
" cannot bind", self.interface)
1570
logger.error("SO_BINDTODEVICE does not exist;"
1571
" cannot bind to interface %s",
1575
self.socket.setsockopt(socket.SOL_SOCKET,
1579
except socket.error as error:
1580
if error[0] == errno.EPERM:
1581
logger.error("No permission to"
1582
" bind to interface %s",
1584
elif error[0] == errno.ENOPROTOOPT:
1585
logger.error("SO_BINDTODEVICE not available;"
1586
" cannot bind to interface %s",
2615
1590
# Only bind(2) the socket if we really need to.
2616
1591
if self.server_address[0] or self.server_address[1]:
2617
if self.server_address[1]:
2618
self.allow_reuse_address = True
2619
1592
if not self.server_address[0]:
2620
1593
if self.address_family == socket.AF_INET6:
2621
any_address = "::" # in6addr_any
1594
any_address = "::" # in6addr_any
2623
any_address = "0.0.0.0" # INADDR_ANY
1596
any_address = socket.INADDR_ANY
2624
1597
self.server_address = (any_address,
2625
1598
self.server_address[1])
2626
1599
elif not self.server_address[1]:
2627
self.server_address = (self.server_address[0], 0)
1600
self.server_address = (self.server_address[0],
2628
1602
# if self.interface:
2629
1603
# self.server_address = (self.server_address[0],
2957
1849
parser.add_argument("--no-dbus", action="store_false",
2958
1850
dest="use_dbus", help="Do not provide D-Bus"
2959
" system bus interface", default=None)
1851
" system bus interface")
2960
1852
parser.add_argument("--no-ipv6", action="store_false",
2961
dest="use_ipv6", help="Do not use IPv6",
2963
parser.add_argument("--no-restore", action="store_false",
2964
dest="restore", help="Do not restore stored"
2965
" state", default=None)
2966
parser.add_argument("--socket", type=int,
2967
help="Specify a file descriptor to a network"
2968
" socket to use instead of creating one")
2969
parser.add_argument("--statedir", metavar="DIR",
2970
help="Directory to save/restore state in")
2971
parser.add_argument("--foreground", action="store_true",
2972
help="Run in foreground", default=None)
2973
parser.add_argument("--no-zeroconf", action="store_false",
2974
dest="zeroconf", help="Do not use Zeroconf",
1853
dest="use_ipv6", help="Do not use IPv6")
2977
1854
options = parser.parse_args()
2979
1861
# Default values for config file for server-global settings
2980
if gnutls.has_rawpk:
2981
priority = ("SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA"
2982
":!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA")
2984
priority = ("SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
2985
":+SIGN-DSA-SHA256")
2986
server_defaults = {"interface": "",
2990
"priority": priority,
2991
"servicename": "Mandos",
2997
"statedir": "/var/lib/mandos",
2998
"foreground": "False",
1862
server_defaults = { "interface": "",
1867
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1868
"servicename": "Mandos",
3003
1874
# Parse config file for server-global settings
3004
server_config = configparser.ConfigParser(server_defaults)
1875
server_config = configparser.SafeConfigParser(server_defaults)
3005
1876
del server_defaults
3006
server_config.read(os.path.join(options.configdir, "mandos.conf"))
3007
# Convert the ConfigParser object to a dict
1877
server_config.read(os.path.join(options.configdir,
1879
# Convert the SafeConfigParser object to a dict
3008
1880
server_settings = server_config.defaults()
3009
1881
# Use the appropriate methods on the non-string config options
3010
for option in ("debug", "use_dbus", "use_ipv6", "restore",
3011
"foreground", "zeroconf"):
1882
for option in ("debug", "use_dbus", "use_ipv6"):
3012
1883
server_settings[option] = server_config.getboolean("DEFAULT",
3014
1885
if server_settings["port"]:
3015
1886
server_settings["port"] = server_config.getint("DEFAULT",
3017
if server_settings["socket"]:
3018
server_settings["socket"] = server_config.getint("DEFAULT",
3020
# Later, stdin will, and stdout and stderr might, be dup'ed
3021
# over with an opened os.devnull. But we don't want this to
3022
# happen with a supplied network socket.
3023
if 0 <= server_settings["socket"] <= 2:
3024
server_settings["socket"] = os.dup(server_settings
3026
1888
del server_config
3028
1890
# Override the settings from the config file with command line
3029
1891
# options, if set.
3030
1892
for option in ("interface", "address", "port", "debug",
3031
"priority", "servicename", "configdir", "use_dbus",
3032
"use_ipv6", "debuglevel", "restore", "statedir",
3033
"socket", "foreground", "zeroconf"):
1893
"priority", "servicename", "configdir",
1894
"use_dbus", "use_ipv6", "debuglevel"):
3034
1895
value = getattr(options, option)
3035
1896
if value is not None:
3036
1897
server_settings[option] = value
3038
1899
# Force all strings to be unicode
3039
1900
for option in server_settings.keys():
3040
if isinstance(server_settings[option], bytes):
3041
server_settings[option] = (server_settings[option]
3043
# Force all boolean options to be boolean
3044
for option in ("debug", "use_dbus", "use_ipv6", "restore",
3045
"foreground", "zeroconf"):
3046
server_settings[option] = bool(server_settings[option])
3047
# Debug implies foreground
3048
if server_settings["debug"]:
3049
server_settings["foreground"] = True
1901
if type(server_settings[option]) is str:
1902
server_settings[option] = unicode(server_settings[option])
3050
1903
# Now we have our good server settings in "server_settings"
3052
1905
##################################################################
3054
if (not server_settings["zeroconf"]
3055
and not (server_settings["port"]
3056
or server_settings["socket"] != "")):
3057
parser.error("Needs port or socket to work without Zeroconf")
3059
1907
# For convenience
3060
1908
debug = server_settings["debug"]
3061
1909
debuglevel = server_settings["debuglevel"]
3062
1910
use_dbus = server_settings["use_dbus"]
3063
1911
use_ipv6 = server_settings["use_ipv6"]
3064
stored_state_path = os.path.join(server_settings["statedir"],
3066
foreground = server_settings["foreground"]
3067
zeroconf = server_settings["zeroconf"]
3070
initlogger(debug, logging.DEBUG)
3075
level = getattr(logging, debuglevel.upper())
3076
initlogger(debug, level)
3078
1913
if server_settings["servicename"] != "Mandos":
3079
syslogger.setFormatter(
3080
logging.Formatter('Mandos ({}) [%(process)d]:'
3081
' %(levelname)s: %(message)s'.format(
3082
server_settings["servicename"])))
1914
syslogger.setFormatter(logging.Formatter
1915
('Mandos (%s) [%%(process)d]:'
1916
' %%(levelname)s: %%(message)s'
1917
% server_settings["servicename"]))
3084
1919
# Parse config file with clients
3085
client_config = configparser.ConfigParser(Client.client_defaults)
1920
client_defaults = { "timeout": "5m",
1921
"extended_timeout": "15m",
1923
"checker": "fping -q -- %%(host)s",
1925
"approval_delay": "0s",
1926
"approval_duration": "1s",
1928
client_config = configparser.SafeConfigParser(client_defaults)
3086
1929
client_config.read(os.path.join(server_settings["configdir"],
3087
1930
"clients.conf"))
3089
1932
global mandos_dbus_service
3090
1933
mandos_dbus_service = None
3093
if server_settings["socket"] != "":
3094
socketfd = server_settings["socket"]
3095
tcp_server = MandosServer(
3096
(server_settings["address"], server_settings["port"]),
3098
interface=(server_settings["interface"] or None),
3100
gnutls_priority=server_settings["priority"],
3104
pidfilename = "/run/mandos.pid"
3105
if not os.path.isdir("/run/."):
3106
pidfilename = "/var/run/mandos.pid"
3109
pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
3110
except IOError as e:
3111
logger.error("Could not open file %r", pidfilename,
3114
for name, group in (("_mandos", "_mandos"),
3115
("mandos", "mandos"),
3116
("nobody", "nogroup")):
3118
uid = pwd.getpwnam(name).pw_uid
3119
gid = pwd.getpwnam(group).pw_gid
1935
tcp_server = MandosServer((server_settings["address"],
1936
server_settings["port"]),
1938
interface=(server_settings["interface"]
1942
server_settings["priority"],
1945
pidfilename = "/var/run/mandos.pid"
1947
pidfile = open(pidfilename, "w")
1949
logger.error("Could not open file %r", pidfilename)
1952
uid = pwd.getpwnam("_mandos").pw_uid
1953
gid = pwd.getpwnam("_mandos").pw_gid
1956
uid = pwd.getpwnam("mandos").pw_uid
1957
gid = pwd.getpwnam("mandos").pw_gid
3121
1958
except KeyError:
1960
uid = pwd.getpwnam("nobody").pw_uid
1961
gid = pwd.getpwnam("nobody").pw_gid
3130
logger.debug("Did setuid/setgid to {}:{}".format(uid,
3132
1968
except OSError as error:
3133
logger.warning("Failed to setuid/setgid to {}:{}: {}"
3134
.format(uid, gid, os.strerror(error.errno)))
3135
if error.errno != errno.EPERM:
1969
if error[0] != errno.EPERM:
1972
if not debug and not debuglevel:
1973
syslogger.setLevel(logging.WARNING)
1974
console.setLevel(logging.WARNING)
1976
level = getattr(logging, debuglevel.upper())
1977
syslogger.setLevel(level)
1978
console.setLevel(level)
3139
1981
# Enable all possible GnuTLS debugging
3141
1983
# "Use a log level over 10 to enable all debugging options."
3142
1984
# - GnuTLS manual
3143
gnutls.global_set_log_level(11)
1985
gnutls.library.functions.gnutls_global_set_log_level(11)
1987
@gnutls.library.types.gnutls_log_func
3146
1988
def debug_gnutls(level, string):
3147
1989
logger.debug("GnuTLS: %s", string[:-1])
3149
gnutls.global_set_log_function(debug_gnutls)
1991
(gnutls.library.functions
1992
.gnutls_global_set_log_function(debug_gnutls))
3151
1994
# Redirect stdin so all checkers get /dev/null
3152
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
1995
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
3153
1996
os.dup2(null, sys.stdin.fileno())
2000
# No console logging
2001
logger.removeHandler(console)
3157
2003
# Need to fork before connecting to D-Bus
3159
2005
# Close all input and output, do double fork, etc.
3162
if gi.version_info < (3, 10, 2):
3163
# multiprocessing will use threads, so before we use GLib we
3164
# need to inform GLib that threads will be used.
3167
2008
global main_loop
3168
2009
# From the Avahi example code
3169
DBusGMainLoop(set_as_default=True)
3170
main_loop = GLib.MainLoop()
2010
DBusGMainLoop(set_as_default=True )
2011
main_loop = gobject.MainLoop()
3171
2012
bus = dbus.SystemBus()
3172
2013
# End of Avahi example code
3175
2016
bus_name = dbus.service.BusName("se.recompile.Mandos",
3178
old_bus_name = dbus.service.BusName(
3179
"se.bsnet.fukt.Mandos", bus,
3181
except dbus.exceptions.DBusException as e:
3182
logger.error("Disabling D-Bus:", exc_info=e)
2017
bus, do_not_queue=True)
2018
old_bus_name = (dbus.service.BusName
2019
("se.bsnet.fukt.Mandos", bus,
2021
except dbus.exceptions.NameExistsException as e:
2022
logger.error(unicode(e) + ", disabling D-Bus")
3183
2023
use_dbus = False
3184
2024
server_settings["use_dbus"] = False
3185
2025
tcp_server.use_dbus = False
3187
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
3188
service = AvahiServiceToSyslog(
3189
name=server_settings["servicename"],
3190
servicetype="_mandos._tcp",
3193
if server_settings["interface"]:
3194
service.interface = if_nametoindex(
3195
server_settings["interface"].encode("utf-8"))
2026
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
2027
service = AvahiService(name = server_settings["servicename"],
2028
servicetype = "_mandos._tcp",
2029
protocol = protocol, bus = bus)
2030
if server_settings["interface"]:
2031
service.interface = (if_nametoindex
2032
(str(server_settings["interface"])))
3197
2034
global multiprocessing_manager
3198
2035
multiprocessing_manager = multiprocessing.Manager()
3200
2037
client_class = Client
3202
client_class = functools.partial(ClientDBus, bus=bus)
3204
client_settings = Client.config_parser(client_config)
3205
old_client_settings = {}
3208
# This is used to redirect stdout and stderr for checker processes
3210
wnull = open(os.devnull, "w") # A writable /dev/null
3211
# Only used if server is running in foreground but not in debug
3213
if debug or not foreground:
3216
# Get client data and settings from last running state.
3217
if server_settings["restore"]:
3219
with open(stored_state_path, "rb") as stored_state:
3220
if sys.version_info.major == 2:
3221
clients_data, old_client_settings = pickle.load(
3224
bytes_clients_data, bytes_old_client_settings = (
3225
pickle.load(stored_state, encoding="bytes"))
3226
# Fix bytes to strings
3229
clients_data = {(key.decode("utf-8")
3230
if isinstance(key, bytes)
3233
bytes_clients_data.items()}
3234
del bytes_clients_data
3235
for key in clients_data:
3236
value = {(k.decode("utf-8")
3237
if isinstance(k, bytes) else k): v
3239
clients_data[key].items()}
3240
clients_data[key] = value
3242
value["client_structure"] = [
3244
if isinstance(s, bytes)
3246
value["client_structure"]]
3248
for k in ("name", "host"):
3249
if isinstance(value[k], bytes):
3250
value[k] = value[k].decode("utf-8")
3251
if "key_id" not in value:
3252
value["key_id"] = ""
3253
elif "fingerprint" not in value:
3254
value["fingerprint"] = ""
3255
# old_client_settings
3257
old_client_settings = {
3258
(key.decode("utf-8")
3259
if isinstance(key, bytes)
3262
bytes_old_client_settings.items()}
3263
del bytes_old_client_settings
3265
for value in old_client_settings.values():
3266
if isinstance(value["host"], bytes):
3267
value["host"] = (value["host"]
3269
os.remove(stored_state_path)
3270
except IOError as e:
3271
if e.errno == errno.ENOENT:
3272
logger.warning("Could not load persistent state:"
3273
" {}".format(os.strerror(e.errno)))
3275
logger.critical("Could not load persistent state:",
3278
except EOFError as e:
3279
logger.warning("Could not load persistent state: "
3283
with PGPEngine() as pgp:
3284
for client_name, client in clients_data.items():
3285
# Skip removed clients
3286
if client_name not in client_settings:
3289
# Decide which value to use after restoring saved state.
3290
# We have three different values: Old config file,
3291
# new config file, and saved state.
3292
# New config value takes precedence if it differs from old
3293
# config value, otherwise use saved state.
3294
for name, value in client_settings[client_name].items():
3296
# For each value in new config, check if it
3297
# differs from the old config value (Except for
3298
# the "secret" attribute)
3299
if (name != "secret"
3301
old_client_settings[client_name][name])):
3302
client[name] = value
3306
# Clients who has passed its expire date can still be
3307
# enabled if its last checker was successful. A Client
3308
# whose checker succeeded before we stored its state is
3309
# assumed to have successfully run all checkers during
3311
if client["enabled"]:
3312
if datetime.datetime.utcnow() >= client["expires"]:
3313
if not client["last_checked_ok"]:
3315
"disabling client {} - Client never "
3316
"performed a successful checker".format(
3318
client["enabled"] = False
3319
elif client["last_checker_status"] != 0:
3321
"disabling client {} - Client last"
3322
" checker failed with error code"
3325
client["last_checker_status"]))
3326
client["enabled"] = False
3328
client["expires"] = (
3329
datetime.datetime.utcnow()
3330
+ client["timeout"])
3331
logger.debug("Last checker succeeded,"
3332
" keeping {} enabled".format(
2039
client_class = functools.partial(ClientDBusTransitional,
2041
def client_config_items(config, section):
2042
special_settings = {
2043
"approved_by_default":
2044
lambda: config.getboolean(section,
2045
"approved_by_default"),
2047
for name, value in config.items(section):
3335
client["secret"] = pgp.decrypt(
3336
client["encrypted_secret"],
3337
client_settings[client_name]["secret"])
3339
# If decryption fails, we use secret from new settings
3340
logger.debug("Failed to decrypt {} old secret".format(
3342
client["secret"] = (client_settings[client_name]
3345
# Add/remove clients based on new changes made to config
3346
for client_name in (set(old_client_settings)
3347
- set(client_settings)):
3348
del clients_data[client_name]
3349
for client_name in (set(client_settings)
3350
- set(old_client_settings)):
3351
clients_data[client_name] = client_settings[client_name]
3353
# Create all client objects
3354
for client_name, client in clients_data.items():
3355
tcp_server.clients[client_name] = client_class(
3358
server_settings=server_settings)
2049
yield (name, special_settings[name]())
2053
tcp_server.clients.update(set(
2054
client_class(name = section,
2055
config= dict(client_config_items(
2056
client_config, section)))
2057
for section in client_config.sections()))
3360
2058
if not tcp_server.clients:
3361
2059
logger.warning("No clients defined")
3364
if pidfile is not None:
3368
print(pid, file=pidfile)
3370
logger.error("Could not write to file %r with PID %d",
2065
pidfile.write(str(pid) + "\n".encode("utf-8"))
2068
logger.error("Could not write to file %r with PID %d",
2071
# "pidfile" was never created
3373
2073
del pidfilename
3375
for termsig in (signal.SIGHUP, signal.SIGTERM):
3376
GLib.unix_signal_add(GLib.PRIORITY_HIGH, termsig,
3377
lambda: main_loop.quit() and False)
2075
signal.signal(signal.SIGINT, signal.SIG_IGN)
2077
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
2078
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit())
3381
@alternate_dbus_interfaces(
3382
{"se.recompile.Mandos": "se.bsnet.fukt.Mandos"})
3383
class MandosDBusService(DBusObjectWithObjectManager):
2081
class MandosDBusService(dbus.service.Object):
3384
2082
"""A D-Bus proxy object"""
3386
2083
def __init__(self):
3387
2084
dbus.service.Object.__init__(self, bus, "/")
3389
2085
_interface = "se.recompile.Mandos"
3391
2087
@dbus.service.signal(_interface, signature="o")
3392
2088
def ClientAdded(self, objpath):
3396
2092
@dbus.service.signal(_interface, signature="ss")
3397
def ClientNotFound(self, key_id, address):
2093
def ClientNotFound(self, fingerprint, address):
3401
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3403
2097
@dbus.service.signal(_interface, signature="os")
3404
2098
def ClientRemoved(self, objpath, name):
3408
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3410
2102
@dbus.service.method(_interface, out_signature="ao")
3411
2103
def GetAllClients(self):
3413
return dbus.Array(c.dbus_object_path for c in
3414
tcp_server.clients.values())
3416
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
2105
return dbus.Array(c.dbus_object_path
2106
for c in tcp_server.clients)
3418
2108
@dbus.service.method(_interface,
3419
2109
out_signature="a{oa{sv}}")
3420
2110
def GetAllClientsWithProperties(self):
3422
2112
return dbus.Dictionary(
3423
{c.dbus_object_path: c.GetAll(
3424
"se.recompile.Mandos.Client")
3425
for c in tcp_server.clients.values()},
2113
((c.dbus_object_path, c.GetAll(""))
2114
for c in tcp_server.clients),
3426
2115
signature="oa{sv}")
3428
2117
@dbus.service.method(_interface, in_signature="o")
3429
2118
def RemoveClient(self, object_path):
3431
for c in tcp_server.clients.values():
2120
for c in tcp_server.clients:
3432
2121
if c.dbus_object_path == object_path:
3433
del tcp_server.clients[c.name]
2122
tcp_server.clients.remove(c)
3434
2123
c.remove_from_connection()
3435
# Don't signal the disabling
2124
# Don't signal anything except ClientRemoved
3436
2125
c.disable(quiet=True)
3437
# Emit D-Bus signal for removal
3438
self.client_removed_signal(c)
2127
self.ClientRemoved(object_path, c.name)
3440
2129
raise KeyError(object_path)
3444
@dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
3445
out_signature="a{oa{sa{sv}}}")
3446
def GetManagedObjects(self):
3448
return dbus.Dictionary(
3449
{client.dbus_object_path:
3451
{interface: client.GetAll(interface)
3453
client._get_all_interface_names()})
3454
for client in tcp_server.clients.values()})
3456
def client_added_signal(self, client):
3457
"""Send the new standard signal and the old signal"""
3459
# New standard signal
3460
self.InterfacesAdded(
3461
client.dbus_object_path,
3463
{interface: client.GetAll(interface)
3465
client._get_all_interface_names()}))
3467
self.ClientAdded(client.dbus_object_path)
3469
def client_removed_signal(self, client):
3470
"""Send the new standard signal and the old signal"""
3472
# New standard signal
3473
self.InterfacesRemoved(
3474
client.dbus_object_path,
3475
client._get_all_interface_names())
3477
self.ClientRemoved(client.dbus_object_path,
3480
mandos_dbus_service = MandosDBusService()
3482
# Save modules to variables to exempt the modules from being
3483
# unloaded before the function registered with atexit() is run.
3484
mp = multiprocessing
2133
class MandosDBusServiceTransitional(MandosDBusService):
2134
__metaclass__ = AlternateDBusNamesMetaclass
2135
mandos_dbus_service = MandosDBusServiceTransitional()
3488
2138
"Cleanup function; run on exit"
3492
mp.active_children()
3494
if not (tcp_server.clients or client_settings):
3497
# Store client before exiting. Secrets are encrypted with key
3498
# based on what config file has. If config file is
3499
# removed/edited, old secret will thus be unrecovable.
3501
with PGPEngine() as pgp:
3502
for client in tcp_server.clients.values():
3503
key = client_settings[client.name]["secret"]
3504
client.encrypted_secret = pgp.encrypt(client.secret,
3508
# A list of attributes that can not be pickled
3510
exclude = {"bus", "changedstate", "secret",
3511
"checker", "server_settings"}
3512
for name, typ in inspect.getmembers(dbus.service
3516
client_dict["encrypted_secret"] = (client
3518
for attr in client.client_structure:
3519
if attr not in exclude:
3520
client_dict[attr] = getattr(client, attr)
3522
clients[client.name] = client_dict
3523
del client_settings[client.name]["secret"]
3526
with tempfile.NamedTemporaryFile(
3530
dir=os.path.dirname(stored_state_path),
3531
delete=False) as stored_state:
3532
pickle.dump((clients, client_settings), stored_state,
3534
tempname = stored_state.name
3535
os.rename(tempname, stored_state_path)
3536
except (IOError, OSError) as e:
3542
if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
3543
logger.warning("Could not save persistent state: {}"
3544
.format(os.strerror(e.errno)))
3546
logger.warning("Could not save persistent state:",
3550
# Delete all clients, and settings from config
2141
multiprocessing.active_children()
3551
2142
while tcp_server.clients:
3552
name, client = tcp_server.clients.popitem()
2143
client = tcp_server.clients.pop()
3554
2145
client.remove_from_connection()
3555
# Don't signal the disabling
2146
client.disable_hook = None
2147
# Don't signal anything except ClientRemoved
3556
2148
client.disable(quiet=True)
3557
# Emit D-Bus signal for removal
3559
mandos_dbus_service.client_removed_signal(client)
3560
client_settings.clear()
2151
mandos_dbus_service.ClientRemoved(client
3562
2155
atexit.register(cleanup)
3564
for client in tcp_server.clients.values():
2157
for client in tcp_server.clients:
3566
# Emit D-Bus signal for adding
3567
mandos_dbus_service.client_added_signal(client)
3568
# Need to initiate checking of clients
3570
client.init_checker()
2160
mandos_dbus_service.ClientAdded(client.dbus_object_path)
3572
2163
tcp_server.enable()
3573
2164
tcp_server.server_activate()
3575
2166
# Find out what port we got
3577
service.port = tcp_server.socket.getsockname()[1]
2167
service.port = tcp_server.socket.getsockname()[1]
3579
2169
logger.info("Now listening on address %r, port %d,"
3580
" flowinfo %d, scope_id %d",
3581
*tcp_server.socket.getsockname())
2170
" flowinfo %d, scope_id %d"
2171
% tcp_server.socket.getsockname())
3583
logger.info("Now listening on address %r, port %d",
3584
*tcp_server.socket.getsockname())
3586
# service.interface = tcp_server.socket.getsockname()[3]
2173
logger.info("Now listening on address %r, port %d"
2174
% tcp_server.socket.getsockname())
2176
#service.interface = tcp_server.socket.getsockname()[3]
3590
# From the Avahi example code
3593
except dbus.exceptions.DBusException as error:
3594
logger.critical("D-Bus Exception", exc_info=error)
3597
# End of Avahi example code
3599
GLib.io_add_watch(tcp_server.fileno(), GLib.IO_IN,
3600
lambda *args, **kwargs:
3601
(tcp_server.handle_request
3602
(*args[2:], **kwargs) or True))
2179
# From the Avahi example code
2182
except dbus.exceptions.DBusException as error:
2183
logger.critical("DBusException: %s", error)
2186
# End of Avahi example code
2188
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2189
lambda *args, **kwargs:
2190
(tcp_server.handle_request
2191
(*args[2:], **kwargs) or True))
3604
2193
logger.debug("Starting main loop")
3605
2194
main_loop.run()
3606
2195
except AvahiError as error:
3607
logger.critical("Avahi Error", exc_info=error)
2196
logger.critical("AvahiError: %s", error)
3610
2199
except KeyboardInterrupt: