70
import cPickle as pickle
63
import cPickle as pickle
73
64
import multiprocessing
83
67
import dbus.service
85
from gi.repository import GLib
86
70
from dbus.mainloop.glib import DBusGMainLoop
89
73
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
77
SO_BINDTODEVICE = socket.SO_BINDTODEVICE
100
78
except AttributeError:
102
# This is where SO_BINDTODEVICE was up to and including Python
104
80
from IN import SO_BINDTODEVICE
105
81
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
82
SO_BINDTODEVICE = None
87
#logger = logging.getLogger('mandos')
88
logger = logging.Logger('mandos')
89
syslogger = (logging.handlers.SysLogHandler
90
(facility = logging.handlers.SysLogHandler.LOG_DAEMON,
91
address = str("/dev/log")))
92
syslogger.setFormatter(logging.Formatter
93
('Mandos [%(process)d]: %(levelname)s:'
95
logger.addHandler(syslogger)
97
console = logging.StreamHandler()
98
console.setFormatter(logging.Formatter('%(name)s [%(process)d]:'
101
logger.addHandler(console)
311
103
class AvahiError(Exception):
312
104
def __init__(self, value, *args, **kwargs):
313
105
self.value = value
314
return super(AvahiError, self).__init__(value, *args,
106
super(AvahiError, self).__init__(value, *args, **kwargs)
107
def __unicode__(self):
108
return unicode(repr(self.value))
318
110
class AvahiServiceError(AvahiError):
322
113
class AvahiGroupError(AvahiError):
117
class AvahiService(object):
327
118
"""An Avahi (Zeroconf) service.
330
121
interface: integer; avahi.IF_UNSPEC or an interface index.
331
122
Used to optionally bind to the specified interface.
332
123
name: string; Example: 'Mandos'
333
124
type: string; Example: '_mandos._tcp'.
334
See <https://www.iana.org/assignments/service-names-port-numbers>
125
See <http://www.dns-sd.org/ServiceTypes.html>
335
126
port: integer; what port to announce
336
127
TXT: list of strings; TXT record for the service
337
128
domain: string; Domain to publish on, default to .local if empty.
439
210
elif state == avahi.ENTRY_GROUP_FAILURE:
440
211
logger.critical("Avahi: Error in group state changed %s",
442
raise AvahiGroupError("State changed: {!s}".format(error))
213
raise AvahiGroupError("State changed: %s"
444
215
def cleanup(self):
445
216
"""Derived from the Avahi example code"""
446
217
if self.group is not None:
449
except (dbus.exceptions.UnknownMethodException,
450
dbus.exceptions.DBusException):
452
219
self.group = None
455
def server_state_changed(self, state, error=None):
220
def server_state_changed(self, state):
456
221
"""Derived from the Avahi example code"""
457
222
logger.debug("Avahi server state change: %i", state)
459
avahi.SERVER_INVALID: "Zeroconf server invalid",
460
avahi.SERVER_REGISTERING: None,
461
avahi.SERVER_COLLISION: "Zeroconf server name collision",
462
avahi.SERVER_FAILURE: "Zeroconf server failure",
464
if state in bad_states:
465
if bad_states[state] is not None:
467
logger.error(bad_states[state])
469
logger.error(bad_states[state] + ": %r", error)
223
if state == avahi.SERVER_COLLISION:
224
logger.error("Zeroconf server name collision")
471
226
elif state == avahi.SERVER_RUNNING:
474
except dbus.exceptions.DBusException as error:
475
if (error.get_dbus_name()
476
== "org.freedesktop.Avahi.CollisionError"):
477
logger.info("Local Zeroconf service name"
479
return self.rename(remove=False)
481
logger.critical("D-Bus Exception", exc_info=error)
486
logger.debug("Unknown state: %r", state)
488
logger.debug("Unknown state: %r: %r", state, error)
490
228
def activate(self):
491
229
"""Derived from the Avahi example code"""
492
230
if self.server is None:
493
231
self.server = dbus.Interface(
494
232
self.bus.get_object(avahi.DBUS_NAME,
495
avahi.DBUS_PATH_SERVER,
496
follow_name_owner_changes=True),
233
avahi.DBUS_PATH_SERVER),
497
234
avahi.DBUS_INTERFACE_SERVER)
498
235
self.server.connect_to_signal("StateChanged",
499
self.server_state_changed)
236
self.server_state_changed)
500
237
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))
240
class Client(object):
826
241
"""A representation of a client host served by this server.
829
approved: bool(); 'None' if not yet approved/disapproved
244
_approved: bool(); 'None' if not yet approved/disapproved
830
245
approval_delay: datetime.timedelta(); Time to wait for approval
831
246
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
247
checker: subprocess.Popen(); a running checker process used
248
to see if the client lives.
249
'None' if no process is running.
250
checker_callback_tag: a gobject event source tag, or None
836
251
checker_command: string; External command which is run to check
837
252
if client lives. %() expansions are done at
838
253
runtime with vars(self) as dict, so that for
839
254
instance %(name)s can be used in the command.
840
checker_initiator_tag: a GLib event source tag, or None
255
checker_initiator_tag: a gobject event source tag, or None
841
256
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
257
current_checker_command: string; current running checker_command
845
disable_initiator_tag: a GLib event source tag, or None
258
disable_hook: If set, called by disable() as disable_hook(self)
259
disable_initiator_tag: a gobject event source tag, or None
847
261
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
262
uniquely identify the client
851
263
host: string; available for use by the checker command
852
264
interval: datetime.timedelta(); How often to start a new checker
853
265
last_approval_request: datetime.datetime(); (UTC) or None
854
266
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
267
last_enabled: datetime.datetime(); (UTC)
861
268
name: string; from the config file, used in log messages and
862
269
D-Bus identifiers
863
270
secret: bytestring; sent verbatim (over TLS) to client
864
271
timeout: datetime.timedelta(); How long from last_checked_ok
865
272
until this client is disabled
866
extended_timeout: extra long timeout when secret has been sent
867
273
runtime_expansions: Allowed attributes for runtime expansion.
868
expires: datetime.datetime(); time (UTC) when a client will be
870
server_settings: The server_settings dict from main()
873
276
runtime_expansions = ("approval_delay", "approval_duration",
874
"created", "enabled", "expires", "key_id",
875
"fingerprint", "host", "interval",
876
"last_approval_request", "last_checked_ok",
277
"created", "enabled", "fingerprint",
278
"host", "interval", "last_checked_ok",
877
279
"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):
282
def _timedelta_to_milliseconds(td):
283
"Convert a datetime.timedelta() to milliseconds"
284
return ((td.days * 24 * 60 * 60 * 1000)
285
+ (td.seconds * 1000)
286
+ (td.microseconds // 1000))
288
def timeout_milliseconds(self):
289
"Return the 'timeout' attribute in milliseconds"
290
return self._timedelta_to_milliseconds(self.timeout)
292
def interval_milliseconds(self):
293
"Return the 'interval' attribute in milliseconds"
294
return self._timedelta_to_milliseconds(self.interval)
296
def approval_delay_milliseconds(self):
297
return self._timedelta_to_milliseconds(self.approval_delay)
299
def __init__(self, name = None, disable_hook=None, config=None):
300
"""Note: the 'checker' key in 'config' sets the
301
'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
306
logger.debug("Creating client %r", self.name)
964
logger.debug(" Key ID: %s", self.key_id)
307
# Uppercase and remove spaces from fingerprint for later
308
# comparison purposes with return value from the fingerprint()
310
self.fingerprint = (config["fingerprint"].upper()
965
312
logger.debug(" Fingerprint: %s", self.fingerprint)
966
self.created = settings.get("created",
967
datetime.datetime.utcnow())
969
# attributes specific for this server instance
313
if "secret" in config:
314
self.secret = config["secret"].decode("base64")
315
elif "secfile" in config:
316
with open(os.path.expanduser(os.path.expandvars
317
(config["secfile"])),
319
self.secret = secfile.read()
321
raise TypeError("No secret or secfile for client %s"
323
self.host = config.get("host", "")
324
self.created = datetime.datetime.utcnow()
326
self.last_approval_request = None
327
self.last_enabled = None
328
self.last_checked_ok = None
329
self.timeout = string_to_delta(config["timeout"])
330
self.interval = string_to_delta(config["interval"])
331
self.disable_hook = disable_hook
970
332
self.checker = None
971
333
self.checker_initiator_tag = None
972
334
self.disable_initiator_tag = None
973
335
self.checker_callback_tag = None
336
self.checker_command = config["checker"]
974
337
self.current_checker_command = None
338
self.last_connect = None
339
self._approved = None
340
self.approved_by_default = config.get("approved_by_default",
976
342
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
343
self.approval_delay = string_to_delta(
344
config["approval_delay"])
345
self.approval_duration = string_to_delta(
346
config["approval_duration"])
347
self.changedstate = multiprocessing_manager.Condition(multiprocessing_manager.Lock())
990
349
def send_changedstate(self):
991
with self.changedstate:
992
self.changedstate.notify_all()
350
self.changedstate.acquire()
351
self.changedstate.notify_all()
352
self.changedstate.release()
994
354
def enable(self):
995
355
"""Start this client's checker and timeout hooks"""
996
356
if getattr(self, "enabled", False):
997
357
# Already enabled
999
self.expires = datetime.datetime.utcnow() + self.timeout
359
self.send_changedstate()
1001
360
self.last_enabled = datetime.datetime.utcnow()
1003
self.send_changedstate()
361
# Schedule a new checker to be started an 'interval' from now,
362
# and every interval from then on.
363
self.checker_initiator_tag = (gobject.timeout_add
364
(self.interval_milliseconds(),
366
# Schedule a disable() when 'timeout' has passed
367
self.disable_initiator_tag = (gobject.timeout_add
368
(self.timeout_milliseconds(),
371
# Also start a new checker *right now*.
1005
374
def disable(self, quiet=True):
1006
375
"""Disable this client."""
1007
376
if not getattr(self, "enabled", False):
379
self.send_changedstate()
1010
381
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)
382
if getattr(self, "disable_initiator_tag", False):
383
gobject.source_remove(self.disable_initiator_tag)
1013
384
self.disable_initiator_tag = None
1015
if getattr(self, "checker_initiator_tag", None) is not None:
1016
GLib.source_remove(self.checker_initiator_tag)
385
if getattr(self, "checker_initiator_tag", False):
386
gobject.source_remove(self.checker_initiator_tag)
1017
387
self.checker_initiator_tag = None
1018
388
self.stop_checker()
389
if self.disable_hook:
390
self.disable_hook(self)
1019
391
self.enabled = False
1021
self.send_changedstate()
1022
# Do not run this again if called by a GLib.timeout_add
392
# Do not run this again if called by a gobject.timeout_add
1025
395
def __del__(self):
396
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,
399
def checker_callback(self, pid, condition, command):
1046
400
"""The checker has completed, so take appropriate actions."""
1047
# Read return code from connection (see call_pipe)
1048
returncode = connection.recv()
1051
401
self.checker_callback_tag = None
1052
402
self.checker = None
1055
self.last_checker_status = returncode
1056
self.last_checker_signal = None
1057
if self.last_checker_status == 0:
403
if os.WIFEXITED(condition):
404
exitstatus = os.WEXITSTATUS(condition)
1058
406
logger.info("Checker for %(name)s succeeded",
1060
408
self.checked_ok()
1062
logger.info("Checker for %(name)s failed", vars(self))
410
logger.info("Checker for %(name)s failed",
1064
self.last_checker_status = -1
1065
self.last_checker_signal = -returncode
1066
413
logger.warning("Checker for %(name)s crashed?",
1070
416
def checked_ok(self):
1071
"""Assert that the client has been seen, alive and well."""
417
"""Bump up the timeout for this client.
419
This should only be called when the client has been seen,
1072
422
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."""
1080
timeout = self.timeout
1081
if self.disable_initiator_tag is not None:
1082
GLib.source_remove(self.disable_initiator_tag)
1083
self.disable_initiator_tag = None
1084
if getattr(self, "enabled", False):
1085
self.disable_initiator_tag = GLib.timeout_add(
1086
int(timeout.total_seconds() * 1000), self.disable)
1087
self.expires = datetime.datetime.utcnow() + timeout
423
gobject.source_remove(self.disable_initiator_tag)
424
self.disable_initiator_tag = (gobject.timeout_add
425
(self.timeout_milliseconds(),
1089
428
def need_approval(self):
1090
429
self.last_approval_request = datetime.datetime.utcnow()
1092
431
def start_checker(self):
1093
432
"""Start a new checker subprocess if one is not running.
1095
434
If a checker already exists, leave it running and do
1097
436
# 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
437
# did that, then if a checker (for some reason) started
438
# running slowly and taking more than 'interval' time, the
439
# client would inevitably timeout, since no checker would get
440
# a chance to run to completion. If we instead leave running
1102
441
# checkers alone, the checker would have to take more time
1103
442
# 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")
445
# If a checker exists, make sure it is not a zombie
447
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
448
except (AttributeError, OSError), error:
449
if (isinstance(error, OSError)
450
and error.errno != errno.ECHILD):
454
logger.warning("Checker was a zombie")
455
gobject.source_remove(self.checker_callback_tag)
456
self.checker_callback(pid, status,
457
self.current_checker_command)
1110
458
# Start a new checker if needed
1111
459
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
461
# In case checker_command has exactly one % operator
462
command = self.checker_command % self.host
464
# Escape attributes for the shell
465
escaped_attrs = dict(
467
re.escape(unicode(str(getattr(self, attr, "")),
471
self.runtime_expansions)
474
command = self.checker_command % escaped_attrs
475
except TypeError, error:
476
logger.error('Could not format string "%s":'
477
' %s', self.checker_command, error)
478
return True # Try again later
1123
479
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
481
logger.info("Starting checker %r for %s",
483
# We don't need to redirect stdout and stderr, since
484
# in normal mode, that is already done by daemon(),
485
# and in debug mode we don't want to. (Stdin is
486
# always replaced by /dev/null.)
487
self.checker = subprocess.Popen(command,
490
self.checker_callback_tag = (gobject.child_watch_add
492
self.checker_callback,
494
# The checker may have completed before the gobject
495
# watch was added. Check for this.
496
pid, status = os.waitpid(self.checker.pid, os.WNOHANG)
498
gobject.source_remove(self.checker_callback_tag)
499
self.checker_callback(pid, status, command)
500
except OSError, error:
501
logger.error("Failed to start subprocess: %s",
503
# Re-run this periodically if run by gobject.timeout_add
1152
506
def stop_checker(self):
1153
507
"""Force the checker process, if any, to stop."""
1154
508
if self.checker_callback_tag:
1155
GLib.source_remove(self.checker_callback_tag)
509
gobject.source_remove(self.checker_callback_tag)
1156
510
self.checker_callback_tag = None
1157
511
if getattr(self, "checker", None) is None:
1159
513
logger.debug("Stopping checker for %(name)s", vars(self))
1160
self.checker.terminate()
515
os.kill(self.checker.pid, signal.SIGTERM)
517
#if self.checker.poll() is None:
518
# os.kill(self.checker.pid, signal.SIGKILL)
519
except OSError, error:
520
if error.errno != errno.ESRCH: # No such process
1161
522
self.checker = None
1164
def dbus_service_property(dbus_interface,
524
def dbus_service_property(dbus_interface, signature="v",
525
access="readwrite", byte_arrays=False):
1168
526
"""Decorators for marking methods of a DBusObjectWithProperties to
1169
527
become properties on the D-Bus.
1171
529
The decorated method will be called with no arguments by "Get"
1172
530
and with one argument by "Set".
1174
532
The parameters, where they are supported, are the same as
1175
533
dbus.service.method, except there is only "signature", since the
1176
534
type from Get() and the type sent to Set() is the same.
1507
704
xmlstring = document.toxml("utf-8")
1508
705
document.unlink()
1509
706
except (AttributeError, xml.dom.DOMException,
1510
xml.parsers.expat.ExpatError) as error:
1511
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):
1588
"""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).
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):
1625
# Ignore non-D-Bus attributes, and D-Bus attributes
1626
# with the wrong interface name
1627
if (not hasattr(attribute, "_dbus_interface")
1628
or not attribute._dbus_interface.startswith(
1629
orig_interface_name)):
1631
# Create an alternate D-Bus interface name based on
1633
alt_interface = attribute._dbus_interface.replace(
1634
orig_interface_name, alt_interface_name)
1635
interface_names.add(alt_interface)
1636
# Is this a D-Bus signal?
1637
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(
1642
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)
1650
# Create a new, but exactly alike, function
1651
# object, and decorate it to be a new D-Bus signal
1652
# 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:
1664
# 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
1668
def fixscope(func1, func2):
1669
"""This function is a scope container to pass
1670
func1 and func2 to the "call_both" function
1671
outside of its arguments"""
1673
@functools.wraps(func2)
1674
def call_both(*args, **kwargs):
1675
"""This function will emit two D-Bus
1676
signals by calling func1 and func2"""
1677
func1(*args, **kwargs)
1678
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)
1686
# Create the "call_both" function and add it to
1688
attr[attrname] = fixscope(attribute, new_function)
1689
# Is this a D-Bus method?
1690
elif getattr(attribute, "_dbus_is_method", False):
1691
# Create a new, but exactly alike, function
1692
# object. Decorate it to be a new D-Bus method
1693
# 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:
1707
# Is this a D-Bus property?
1708
elif getattr(attribute, "_dbus_is_property", False):
1709
# Create a new, but exactly alike, function
1710
# object, and decorate it to be a new D-Bus
1711
# property with the alternate D-Bus interface
1712
# 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"})
707
xml.parsers.expat.ExpatError), error:
708
logger.error("Failed to override Introspection method",
1765
713
class ClientDBus(Client, DBusObjectWithProperties):
1766
714
"""A Client class using D-Bus
1769
717
dbus_object_path: dbus.ObjectPath
1770
718
bus: dbus.SystemBus()
1773
721
runtime_expansions = (Client.runtime_expansions
1774
+ ("dbus_object_path", ))
1776
_interface = "se.recompile.Mandos.Client"
722
+ ("dbus_object_path",))
1778
724
# dbus.service.Object doesn't use super(), so we can't either.
1780
def __init__(self, bus=None, *args, **kwargs):
726
def __init__(self, bus = None, *args, **kwargs):
727
self._approvals_pending = 0
1782
729
Client.__init__(self, *args, **kwargs)
1783
730
# Only now, when this client is initialized, can it show up on
1785
client_object_name = str(self.name).translate(
732
client_object_name = unicode(self.name).translate(
1786
733
{ord("."): ord("_"),
1787
734
ord("-"): ord("_")})
1788
self.dbus_object_path = dbus.ObjectPath(
1789
"/clients/" + client_object_name)
735
self.dbus_object_path = (dbus.ObjectPath
736
("/clients/" + client_object_name))
1790
737
DBusObjectWithProperties.__init__(self, self.bus,
1791
738
self.dbus_object_path)
1793
def notifychangeproperty(transform_func, dbus_name,
1794
type_func=lambda x: x,
1796
invalidate_only=False,
1797
_interface=_interface):
1798
""" Modify a variable so that it's a property which announces
1799
its changes to DBus.
1801
transform_fun: Function that takes a value and a variant_level
1802
and transforms it to a D-Bus type.
1803
dbus_name: D-Bus name of the variable
1804
type_func: Function that transform the value before sending it
1805
to the D-Bus. Default: no transform
1806
variant_level: D-Bus variant level. Default: 1
1808
attrname = "_{}".format(dbus_name)
1810
def setter(self, value):
1811
if hasattr(self, "dbus_object_path"):
1812
if (not hasattr(self, attrname) or
1813
type_func(getattr(self, attrname, None))
1814
!= type_func(value)):
1816
self.PropertiesChanged(
1817
_interface, dbus.Dictionary(),
1818
dbus.Array((dbus_name, )))
1820
dbus_value = transform_func(
1822
variant_level=variant_level)
1823
self.PropertyChanged(dbus.String(dbus_name),
1825
self.PropertiesChanged(
1827
dbus.Dictionary({dbus.String(dbus_name):
1830
setattr(self, attrname, value)
1832
return property(lambda self: getattr(self, attrname), setter)
1834
expires = notifychangeproperty(datetime_to_dbus, "Expires")
1835
approvals_pending = notifychangeproperty(dbus.Boolean,
1838
enabled = notifychangeproperty(dbus.Boolean, "Enabled")
1839
last_enabled = notifychangeproperty(datetime_to_dbus,
1841
checker = notifychangeproperty(
1842
dbus.Boolean, "CheckerRunning",
1843
type_func=lambda checker: checker is not None)
1844
last_checked_ok = notifychangeproperty(datetime_to_dbus,
1846
last_checker_status = notifychangeproperty(dbus.Int16,
1847
"LastCheckerStatus")
1848
last_approval_request = notifychangeproperty(
1849
datetime_to_dbus, "LastApprovalRequest")
1850
approved_by_default = notifychangeproperty(dbus.Boolean,
1851
"ApprovedByDefault")
1852
approval_delay = notifychangeproperty(
1853
dbus.UInt64, "ApprovalDelay",
1854
type_func=lambda td: td.total_seconds() * 1000)
1855
approval_duration = notifychangeproperty(
1856
dbus.UInt64, "ApprovalDuration",
1857
type_func=lambda td: td.total_seconds() * 1000)
1858
host = notifychangeproperty(dbus.String, "Host")
1859
timeout = notifychangeproperty(
1860
dbus.UInt64, "Timeout",
1861
type_func=lambda td: td.total_seconds() * 1000)
1862
extended_timeout = notifychangeproperty(
1863
dbus.UInt64, "ExtendedTimeout",
1864
type_func=lambda td: td.total_seconds() * 1000)
1865
interval = notifychangeproperty(
1866
dbus.UInt64, "Interval",
1867
type_func=lambda td: td.total_seconds() * 1000)
1868
checker_command = notifychangeproperty(dbus.String, "Checker")
1869
secret = notifychangeproperty(dbus.ByteArray, "Secret",
1870
invalidate_only=True)
1872
del notifychangeproperty
740
def _get_approvals_pending(self):
741
return self._approvals_pending
742
def _set_approvals_pending(self, value):
743
old_value = self._approvals_pending
744
self._approvals_pending = value
746
if (hasattr(self, "dbus_object_path")
747
and bval is not bool(old_value)):
748
dbus_bool = dbus.Boolean(bval, variant_level=1)
749
self.PropertyChanged(dbus.String("ApprovalPending"),
752
approvals_pending = property(_get_approvals_pending,
753
_set_approvals_pending)
754
del _get_approvals_pending, _set_approvals_pending
757
def _datetime_to_dbus(dt, variant_level=0):
758
"""Convert a UTC datetime.datetime() to a D-Bus type."""
759
return dbus.String(dt.isoformat(),
760
variant_level=variant_level)
763
oldstate = getattr(self, "enabled", False)
764
r = Client.enable(self)
765
if oldstate != self.enabled:
767
self.PropertyChanged(dbus.String("Enabled"),
768
dbus.Boolean(True, variant_level=1))
769
self.PropertyChanged(
770
dbus.String("LastEnabled"),
771
self._datetime_to_dbus(self.last_enabled,
775
def disable(self, quiet = False):
776
oldstate = getattr(self, "enabled", False)
777
r = Client.disable(self, quiet=quiet)
778
if not quiet and oldstate != self.enabled:
780
self.PropertyChanged(dbus.String("Enabled"),
781
dbus.Boolean(False, variant_level=1))
1874
784
def __del__(self, *args, **kwargs):
1876
786
self.remove_from_connection()
2375
1289
delay -= time2 - time
2378
session.send(client.secret)
2379
except gnutls.Error as error:
2380
logger.warning("gnutls send failed",
1292
while sent_size < len(client.secret):
1294
sent = session.send(client.secret[sent_size:])
1295
except (gnutls.errors.GNUTLSError), error:
1296
logger.warning("gnutls send failed")
1298
logger.debug("Sent: %d, remaining: %d",
1299
sent, len(client.secret)
1300
- (sent_size + sent))
2384
1303
logger.info("Sending secret to %s", client.name)
2385
# bump the timeout using extended_timeout
2386
client.bump_timeout(client.extended_timeout)
1304
# bump the timeout as if seen
2387
1306
if self.server.use_dbus:
2388
1307
# Emit D-Bus signal
2389
1308
client.GotSecret()
2392
1311
if approval_required:
2393
1312
client.approvals_pending -= 1
2396
except gnutls.Error as error:
2397
logger.warning("GnuTLS bye failed",
1315
except (gnutls.errors.GNUTLSError), error:
1316
logger.warning("GnuTLS bye failed")
2401
1319
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
1320
"Return the peer's OpenPGP certificate as a bytestring"
1321
# If not an OpenPGP certificate...
1322
if (gnutls.library.functions
1323
.gnutls_certificate_type_get(session._c_object)
1324
!= gnutls.library.constants.GNUTLS_CRT_OPENPGP):
1325
# ...do the normal thing
1326
return session.peer_certificate
2418
1327
list_size = ctypes.c_uint(1)
2419
cert_list = (gnutls.certificate_get_peers
1328
cert_list = (gnutls.library.functions
1329
.gnutls_certificate_get_peers
2420
1330
(session._c_object, ctypes.byref(list_size)))
2421
1331
if not bool(cert_list) and list_size.value != 0:
2422
raise gnutls.Error("error getting peer certificate")
1332
raise gnutls.errors.GNUTLSError("error getting peer"
2423
1334
if list_size.value == 0:
2425
1336
cert = cert_list[0]
2426
1337
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
1340
def fingerprint(openpgp):
2464
1341
"Convert an OpenPGP bytestring to a hexdigit fingerprint"
2465
1342
# 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)))
1343
datum = (gnutls.library.types
1344
.gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp),
1347
ctypes.c_uint(len(openpgp))))
2470
1348
# New empty GnuTLS certificate
2471
crt = gnutls.openpgp_crt_t()
2472
gnutls.openpgp_crt_init(ctypes.byref(crt))
1349
crt = gnutls.library.types.gnutls_openpgp_crt_t()
1350
(gnutls.library.functions
1351
.gnutls_openpgp_crt_init(ctypes.byref(crt)))
2473
1352
# Import the OpenPGP public key into the certificate
2474
gnutls.openpgp_crt_import(crt, ctypes.byref(datum),
2475
gnutls.OPENPGP_FMT_RAW)
1353
(gnutls.library.functions
1354
.gnutls_openpgp_crt_import(crt, ctypes.byref(datum),
1355
gnutls.library.constants
1356
.GNUTLS_OPENPGP_FMT_RAW))
2476
1357
# Verify the self signature in the key
2477
1358
crtverify = ctypes.c_uint()
2478
gnutls.openpgp_crt_verify_self(crt, 0,
2479
ctypes.byref(crtverify))
1359
(gnutls.library.functions
1360
.gnutls_openpgp_crt_verify_self(crt, 0,
1361
ctypes.byref(crtverify)))
2480
1362
if crtverify.value != 0:
2481
gnutls.openpgp_crt_deinit(crt)
2482
raise gnutls.CertificateSecurityError(code
1363
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
1364
raise (gnutls.errors.CertificateSecurityError
2484
1366
# New buffer for the fingerprint
2485
1367
buf = ctypes.create_string_buffer(20)
2486
1368
buf_len = ctypes.c_size_t()
2487
1369
# Get the fingerprint from the certificate into the buffer
2488
gnutls.openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
2489
ctypes.byref(buf_len))
1370
(gnutls.library.functions
1371
.gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
1372
ctypes.byref(buf_len)))
2490
1373
# Deinit the certificate
2491
gnutls.openpgp_crt_deinit(crt)
1374
gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
2492
1375
# Convert the buffer to a Python bytestring
2493
1376
fpr = ctypes.string_at(buf, buf_len.value)
2494
1377
# Convert the bytestring to hexadecimal notation
2495
hex_fpr = binascii.hexlify(fpr).upper()
1378
hex_fpr = ''.join("%02X" % ord(char) for char in fpr)
2499
class MultiprocessingMixIn:
1382
class MultiprocessingMixIn(object):
2500
1383
"""Like socketserver.ThreadingMixIn, but with multiprocessing"""
2502
1384
def sub_process_main(self, request, address):
2504
1386
self.finish_request(request, address)
2506
1388
self.handle_error(request, address)
2507
1389
self.close_request(request)
2509
1391
def process_request(self, request, address):
2510
1392
"""Start a new process to process the request."""
2511
proc = multiprocessing.Process(target=self.sub_process_main,
2512
args=(request, address))
2517
class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
1393
multiprocessing.Process(target = self.sub_process_main,
1394
args = (request, address)).start()
1396
class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object):
2518
1397
""" adds a pipe to the MixIn """
2520
1398
def process_request(self, request, client_address):
2521
1399
"""Overrides and wraps the original process_request().
2523
1401
This function creates a new pipe in self.pipe
2525
1403
parent_pipe, self.child_pipe = multiprocessing.Pipe()
2527
proc = MultiprocessingMixIn.process_request(self, request,
1405
super(MultiprocessingMixInWithPipe,
1406
self).process_request(request, client_address)
2529
1407
self.child_pipe.close()
2530
self.add_pipe(parent_pipe, proc)
1408
self.add_pipe(parent_pipe)
2532
def add_pipe(self, parent_pipe, proc):
1410
def add_pipe(self, parent_pipe):
2533
1411
"""Dummy function; override as necessary"""
2534
raise NotImplementedError()
1412
raise NotImplementedError
2537
1414
class IPv6_TCPServer(MultiprocessingMixInWithPipe,
2538
socketserver.TCPServer):
1415
socketserver.TCPServer, object):
2539
1416
"""IPv6-capable TCP server. Accepts 'None' as address and/or port
2542
1419
enabled: Boolean; whether this server is activated yet
2543
1420
interface: None or a network interface name (string)
2544
1421
use_ipv6: Boolean; to use IPv6 or not
2547
1423
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().
1424
interface=None, use_ipv6=True):
2554
1425
self.interface = interface
2556
1427
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
1428
socketserver.TCPServer.__init__(self, server_address,
2585
1429
RequestHandlerClass)
2587
1430
def server_bind(self):
2588
1431
"""This overrides the normal server_bind() function
2589
1432
to bind to an interface if one was specified, and also NOT to
2590
1433
bind to an address or port if they were not specified."""
2591
global SO_BINDTODEVICE
2592
1434
if self.interface is not None:
2593
1435
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)
1436
logger.error("SO_BINDTODEVICE does not exist;"
1437
" cannot bind to interface %s",
1441
self.socket.setsockopt(socket.SOL_SOCKET,
1445
except socket.error, error:
1446
if error[0] == errno.EPERM:
1447
logger.error("No permission to"
1448
" bind to interface %s",
1450
elif error[0] == errno.ENOPROTOOPT:
1451
logger.error("SO_BINDTODEVICE not available;"
1452
" cannot bind to interface %s",
2615
1456
# Only bind(2) the socket if we really need to.
2616
1457
if self.server_address[0] or self.server_address[1]:
2617
if self.server_address[1]:
2618
self.allow_reuse_address = True
2619
1458
if not self.server_address[0]:
2620
1459
if self.address_family == socket.AF_INET6:
2621
any_address = "::" # in6addr_any
1460
any_address = "::" # in6addr_any
2623
any_address = "0.0.0.0" # INADDR_ANY
1462
any_address = socket.INADDR_ANY
2624
1463
self.server_address = (any_address,
2625
1464
self.server_address[1])
2626
1465
elif not self.server_address[1]:
2627
self.server_address = (self.server_address[0], 0)
1466
self.server_address = (self.server_address[0],
2628
1468
# if self.interface:
2629
1469
# self.server_address = (self.server_address[0],
2957
1701
parser.add_argument("--no-dbus", action="store_false",
2958
1702
dest="use_dbus", help="Do not provide D-Bus"
2959
" system bus interface", default=None)
1703
" system bus interface")
2960
1704
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",
1705
dest="use_ipv6", help="Do not use IPv6")
2977
1706
options = parser.parse_args()
2979
1713
# 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",
1714
server_defaults = { "interface": "",
1719
"SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
1720
"servicename": "Mandos",
3003
1726
# Parse config file for server-global settings
3004
server_config = configparser.ConfigParser(server_defaults)
1727
server_config = configparser.SafeConfigParser(server_defaults)
3005
1728
del server_defaults
3006
server_config.read(os.path.join(options.configdir, "mandos.conf"))
3007
# Convert the ConfigParser object to a dict
1729
server_config.read(os.path.join(options.configdir,
1731
# Convert the SafeConfigParser object to a dict
3008
1732
server_settings = server_config.defaults()
3009
1733
# Use the appropriate methods on the non-string config options
3010
for option in ("debug", "use_dbus", "use_ipv6", "restore",
3011
"foreground", "zeroconf"):
1734
for option in ("debug", "use_dbus", "use_ipv6"):
3012
1735
server_settings[option] = server_config.getboolean("DEFAULT",
3014
1737
if server_settings["port"]:
3015
1738
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
1740
del server_config
3028
1742
# Override the settings from the config file with command line
3029
1743
# options, if set.
3030
1744
for option in ("interface", "address", "port", "debug",
3031
"priority", "servicename", "configdir", "use_dbus",
3032
"use_ipv6", "debuglevel", "restore", "statedir",
3033
"socket", "foreground", "zeroconf"):
1745
"priority", "servicename", "configdir",
1746
"use_dbus", "use_ipv6", "debuglevel"):
3034
1747
value = getattr(options, option)
3035
1748
if value is not None:
3036
1749
server_settings[option] = value
3038
1751
# Force all strings to be unicode
3039
1752
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
1753
if type(server_settings[option]) is str:
1754
server_settings[option] = unicode(server_settings[option])
3050
1755
# Now we have our good server settings in "server_settings"
3052
1757
##################################################################
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
1759
# For convenience
3060
1760
debug = server_settings["debug"]
3061
1761
debuglevel = server_settings["debuglevel"]
3062
1762
use_dbus = server_settings["use_dbus"]
3063
1763
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
1765
if server_settings["servicename"] != "Mandos":
3079
syslogger.setFormatter(
3080
logging.Formatter('Mandos ({}) [%(process)d]:'
3081
' %(levelname)s: %(message)s'.format(
3082
server_settings["servicename"])))
1766
syslogger.setFormatter(logging.Formatter
1767
('Mandos (%s) [%%(process)d]:'
1768
' %%(levelname)s: %%(message)s'
1769
% server_settings["servicename"]))
3084
1771
# Parse config file with clients
3085
client_config = configparser.ConfigParser(Client.client_defaults)
1772
client_defaults = { "timeout": "1h",
1774
"checker": "fping -q -- %%(host)s",
1776
"approval_delay": "0s",
1777
"approval_duration": "1s",
1779
client_config = configparser.SafeConfigParser(client_defaults)
3086
1780
client_config.read(os.path.join(server_settings["configdir"],
3087
1781
"clients.conf"))
3089
1783
global mandos_dbus_service
3090
1784
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
1786
tcp_server = MandosServer((server_settings["address"],
1787
server_settings["port"]),
1789
interface=(server_settings["interface"]
1793
server_settings["priority"],
1796
pidfilename = "/var/run/mandos.pid"
1798
pidfile = open(pidfilename, "w")
1800
logger.error("Could not open file %r", pidfilename)
1803
uid = pwd.getpwnam("_mandos").pw_uid
1804
gid = pwd.getpwnam("_mandos").pw_gid
1807
uid = pwd.getpwnam("mandos").pw_uid
1808
gid = pwd.getpwnam("mandos").pw_gid
3121
1809
except KeyError:
1811
uid = pwd.getpwnam("nobody").pw_uid
1812
gid = pwd.getpwnam("nobody").pw_gid
3130
logger.debug("Did setuid/setgid to {}:{}".format(uid,
3132
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:
1819
except OSError, error:
1820
if error[0] != errno.EPERM:
1823
if not debug and not debuglevel:
1824
syslogger.setLevel(logging.WARNING)
1825
console.setLevel(logging.WARNING)
1827
level = getattr(logging, debuglevel.upper())
1828
syslogger.setLevel(level)
1829
console.setLevel(level)
3139
1832
# Enable all possible GnuTLS debugging
3141
1834
# "Use a log level over 10 to enable all debugging options."
3142
1835
# - GnuTLS manual
3143
gnutls.global_set_log_level(11)
1836
gnutls.library.functions.gnutls_global_set_log_level(11)
1838
@gnutls.library.types.gnutls_log_func
3146
1839
def debug_gnutls(level, string):
3147
1840
logger.debug("GnuTLS: %s", string[:-1])
3149
gnutls.global_set_log_function(debug_gnutls)
1842
(gnutls.library.functions
1843
.gnutls_global_set_log_function(debug_gnutls))
3151
1845
# Redirect stdin so all checkers get /dev/null
3152
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
1846
null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
3153
1847
os.dup2(null, sys.stdin.fileno())
1851
# No console logging
1852
logger.removeHandler(console)
3157
1854
# Need to fork before connecting to D-Bus
3159
1856
# 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
1859
global main_loop
3168
1860
# From the Avahi example code
3169
DBusGMainLoop(set_as_default=True)
3170
main_loop = GLib.MainLoop()
1861
DBusGMainLoop(set_as_default=True )
1862
main_loop = gobject.MainLoop()
3171
1863
bus = dbus.SystemBus()
3172
1864
# End of Avahi example code
3175
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)
1867
bus_name = dbus.service.BusName("se.bsnet.fukt.Mandos",
1868
bus, do_not_queue=True)
1869
except dbus.exceptions.NameExistsException, e:
1870
logger.error(unicode(e) + ", disabling D-Bus")
3183
1871
use_dbus = False
3184
1872
server_settings["use_dbus"] = False
3185
1873
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"))
1874
protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
1875
service = AvahiService(name = server_settings["servicename"],
1876
servicetype = "_mandos._tcp",
1877
protocol = protocol, bus = bus)
1878
if server_settings["interface"]:
1879
service.interface = (if_nametoindex
1880
(str(server_settings["interface"])))
3197
1882
global multiprocessing_manager
3198
1883
multiprocessing_manager = multiprocessing.Manager()
3200
1885
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(
1887
client_class = functools.partial(ClientDBus, bus = bus)
1888
def client_config_items(config, section):
1889
special_settings = {
1890
"approved_by_default":
1891
lambda: config.getboolean(section,
1892
"approved_by_default"),
1894
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)
1896
yield (name, special_settings[name]())
1900
tcp_server.clients.update(set(
1901
client_class(name = section,
1902
config= dict(client_config_items(
1903
client_config, section)))
1904
for section in client_config.sections()))
3360
1905
if not tcp_server.clients:
3361
1906
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",
1912
pidfile.write(str(pid) + "\n".encode("utf-8"))
1915
logger.error("Could not write to file %r with PID %d",
1918
# "pidfile" was never created
3373
1920
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)
1922
signal.signal(signal.SIGINT, signal.SIG_IGN)
1924
signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit())
1925
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):
1928
class MandosDBusService(dbus.service.Object):
3384
1929
"""A D-Bus proxy object"""
3386
1930
def __init__(self):
3387
1931
dbus.service.Object.__init__(self, bus, "/")
3389
_interface = "se.recompile.Mandos"
1932
_interface = "se.bsnet.fukt.Mandos"
3391
1934
@dbus.service.signal(_interface, signature="o")
3392
1935
def ClientAdded(self, objpath):
3396
1939
@dbus.service.signal(_interface, signature="ss")
3397
def ClientNotFound(self, key_id, address):
1940
def ClientNotFound(self, fingerprint, address):
3401
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3403
1944
@dbus.service.signal(_interface, signature="os")
3404
1945
def ClientRemoved(self, objpath, name):
3408
@dbus_annotations({"org.freedesktop.DBus.Deprecated":
3410
1949
@dbus.service.method(_interface, out_signature="ao")
3411
1950
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":
1952
return dbus.Array(c.dbus_object_path
1953
for c in tcp_server.clients)
3418
1955
@dbus.service.method(_interface,
3419
1956
out_signature="a{oa{sv}}")
3420
1957
def GetAllClientsWithProperties(self):
3422
1959
return dbus.Dictionary(
3423
{c.dbus_object_path: c.GetAll(
3424
"se.recompile.Mandos.Client")
3425
for c in tcp_server.clients.values()},
1960
((c.dbus_object_path, c.GetAll(""))
1961
for c in tcp_server.clients),
3426
1962
signature="oa{sv}")
3428
1964
@dbus.service.method(_interface, in_signature="o")
3429
1965
def RemoveClient(self, object_path):
3431
for c in tcp_server.clients.values():
1967
for c in tcp_server.clients:
3432
1968
if c.dbus_object_path == object_path:
3433
del tcp_server.clients[c.name]
1969
tcp_server.clients.remove(c)
3434
1970
c.remove_from_connection()
3435
# Don't signal the disabling
1971
# Don't signal anything except ClientRemoved
3436
1972
c.disable(quiet=True)
3437
# Emit D-Bus signal for removal
3438
self.client_removed_signal(c)
1974
self.ClientRemoved(object_path, c.name)
3440
1976
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
1980
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
3488
1983
"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
3551
1986
while tcp_server.clients:
3552
name, client = tcp_server.clients.popitem()
1987
client = tcp_server.clients.pop()
3554
1989
client.remove_from_connection()
3555
# Don't signal the disabling
1990
client.disable_hook = None
1991
# Don't signal anything except ClientRemoved
3556
1992
client.disable(quiet=True)
3557
# Emit D-Bus signal for removal
3559
mandos_dbus_service.client_removed_signal(client)
3560
client_settings.clear()
1995
mandos_dbus_service.ClientRemoved(client.dbus_object_path,
3562
1998
atexit.register(cleanup)
3564
for client in tcp_server.clients.values():
2000
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()
2003
mandos_dbus_service.ClientAdded(client.dbus_object_path)
3572
2006
tcp_server.enable()
3573
2007
tcp_server.server_activate()
3575
2009
# Find out what port we got
3577
service.port = tcp_server.socket.getsockname()[1]
2010
service.port = tcp_server.socket.getsockname()[1]
3579
2012
logger.info("Now listening on address %r, port %d,"
3580
" flowinfo %d, scope_id %d",
3581
*tcp_server.socket.getsockname())
2013
" flowinfo %d, scope_id %d"
2014
% 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]
2016
logger.info("Now listening on address %r, port %d"
2017
% tcp_server.socket.getsockname())
2019
#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))
2022
# From the Avahi example code
2025
except dbus.exceptions.DBusException, error:
2026
logger.critical("DBusException: %s", error)
2029
# End of Avahi example code
2031
gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
2032
lambda *args, **kwargs:
2033
(tcp_server.handle_request
2034
(*args[2:], **kwargs) or True))
3604
2036
logger.debug("Starting main loop")
3605
2037
main_loop.run()
3606
except AvahiError as error:
3607
logger.critical("Avahi Error", exc_info=error)
2038
except AvahiError, error:
2039
logger.critical("AvahiError: %s", error)
3610
2042
except KeyboardInterrupt: