=== modified file 'DBUS-API' --- DBUS-API 2014-08-10 14:13:02 +0000 +++ DBUS-API 2015-08-10 09:00:23 +0000 @@ -13,34 +13,25 @@ | Path | Object | |-----------------------+-------------------| | "/" | The Mandos Server | - | "/clients/CLIENTNAME" | Mandos Client | - - + + (To get a list of paths to client objects, use the standard D-Bus + org.freedesktop.DBus.ObjectManager interface, which the server + object supports.) + + * Mandos Server Interface: Interface name: "se.recompile.Mandos" ** Methods: -*** GetAllClients() → (ao: Clients) - Returns an array of all client D-Bus object paths - -*** GetAllClientsWithProperties() → (a{oa{sv}}: ClientProperties) - Returns an array of all clients and all their properties - *** RemoveClient(o: ObjectPath) → nothing Removes a client ** Signals: -*** ClientAdded(o: ObjectPath) - A new client was added. - *** ClientNotFound(s: Fingerprint, s: Address) A client connected from Address using Fingerprint, but was rejected because it was not found in the server. The fingerprint is represented as a string of hexadecimal digits. The address is an IPv4 or IPv6 address in its normal string format. - -*** ClientRemoved(o: ObjectPath, s: Name) - A client named Name on ObjectPath was removed. * Mandos Client Interface: @@ -55,20 +46,6 @@ Assert that this client has been checked and found to be alive. This will restart the timeout before disabling this client. See also the "LastCheckedOK" property. - -*** Disable() → nothing - Disable this client. See also the "Enabled" property. - -*** Enable() → nothing - Enable this client. See also the "Enabled" property. - -*** StartChecker() → nothing - Start a new checker for this client, if none is currently - running. See also the "CheckerRunning" property. - -*** StopChecker() → nothing - Abort a running checker process for this client, if any. See also - the "CheckerRunning" property. ** Properties @@ -96,7 +73,6 @@ | LastCheckerStatus (i) | n | Read | N/A | | LastEnabled (j) | s | Read | N/A | | Name | s | Read | (Section name) | - | ObjectPath | o | Read | N/A | | Secret (k) | ay | Write | secret (or secfile) | | Timeout (a) | t | Read/Write | timeout | @@ -104,13 +80,12 @@ b) An approval is currently pending. - c) Setting this property is equivalent to calling StartChecker() or - StopChecker(). + c) Changing this property can either start a new checker or abort a + running one. d) The creation time of this client object, as an RFC 3339 string. - e) Setting this property is equivalent to calling Enable() or - Disable(). + e) Changing this property enables or disables a client. f) The date and time this client will be disabled, as an RFC 3339 string, or an empty string if this is not scheduled. @@ -155,8 +130,8 @@ * Copyright - Copyright © 2010-2012 Teddy Hogeborn - Copyright © 2010-2012 Björn Påhlsson + Copyright © 2010-2015 Teddy Hogeborn + Copyright © 2010-2015 Björn Påhlsson ** License: === modified file 'Makefile' --- Makefile 2014-10-05 20:08:58 +0000 +++ Makefile 2015-07-16 20:30:35 +0000 @@ -24,7 +24,7 @@ endif #COVERAGE=--coverage OPTIMIZE=-Os -fno-strict-aliasing -LANGUAGE=-std=gnu99 +LANGUAGE=-std=gnu11 htmldir=man version=1.6.9 SED=sed @@ -69,6 +69,8 @@ GPGME_CFLAGS=$(shell gpgme-config --cflags; getconf LFS_CFLAGS) GPGME_LIBS=$(shell gpgme-config --libs; getconf LFS_LIBS; \ getconf LFS_LDFLAGS) +LIBNL3_CFLAGS=$(shell pkg-config --cflags-only-I libnl-route-3.0) +LIBNL3_LIBS=$(shell pkg-config --libs libnl-route-3.0) # Do not change these two CFLAGS+=$(WARN) $(DEBUG) $(FORTIFY) $(COVERAGE) $(OPTIMIZE) \ @@ -106,7 +108,8 @@ PLUGINS=plugins.d/password-prompt plugins.d/mandos-client \ plugins.d/usplash plugins.d/splashy plugins.d/askpass-fifo \ plugins.d/plymouth -CPROGS=plugin-runner $(PLUGINS) +PLUGIN_HELPERS=plugin-helpers/mandos-client-iprouteadddel +CPROGS=plugin-runner $(PLUGINS) $(PLUGIN_HELPERS) PROGS=mandos mandos-keygen mandos-ctl mandos-monitor $(CPROGS) DOCS=mandos.8 mandos-keygen.8 mandos-monitor.8 mandos-ctl.8 \ mandos.conf.5 mandos-clients.conf.5 plugin-runner.8mandos \ @@ -239,6 +242,10 @@ $(LINK.c) $^ -lrt $(GNUTLS_LIBS) $(AVAHI_LIBS) $(strip\ ) $(GPGME_LIBS) $(LOADLIBES) $(LDLIBS) -o $@ +plugin-helpers/mandos-client-iprouteadddel: plugin-helpers/mandos-client-iprouteadddel.c + $(LINK.c) $(LIBNL3_CFLAGS) $^ $(LIBNL3_LIBS) $(strip\ + ) $(LOADLIBES) $(LDLIBS) -o $@ + .PHONY : all doc html clean distclean mostlyclean maintainer-clean \ check run-client run-server install install-html \ install-server install-client-nokey install-client uninstall \ @@ -273,6 +280,7 @@ @echo "###################################################################" # We set GNOME_KEYRING_CONTROL to block pam_gnome_keyring ./plugin-runner --plugin-dir=plugins.d \ + --plugin-helper-dir=plugin-helpers \ --config-file=plugin-runner.conf \ --options-for=mandos-client:--seckey=keydir/seckey.txt,--pubkey=keydir/pubkey.txt,--network-hook-dir=network-hooks.d \ --env-for=mandos-client:GNOME_KEYRING_CONTROL= \ @@ -352,10 +360,12 @@ install-client-nokey: all doc install --directory $(LIBDIR)/mandos $(CONFDIR) install --directory --mode=u=rwx $(KEYDIR) \ - $(LIBDIR)/mandos/plugins.d + $(LIBDIR)/mandos/plugins.d \ + $(LIBDIR)/mandos/plugin-helpers if [ "$(CONFDIR)" != "$(LIBDIR)/mandos" ]; then \ install --mode=u=rwx \ --directory "$(CONFDIR)/plugins.d"; \ + install --directory "$(CONFDIR)/plugin-helpers"; \ fi install --mode=u=rwx,go=rx --directory \ "$(CONFDIR)/network-hooks.d" @@ -381,6 +391,9 @@ install --mode=u=rwxs,go=rx \ --target-directory=$(LIBDIR)/mandos/plugins.d \ plugins.d/plymouth + install --mode=u=rwxs,go=rx \ + --target-directory=$(LIBDIR)/mandos/plugin-helpers \ + plugin-helpers/mandos-client-iprouteadddel install initramfs-tools-hook \ $(INITRAMFSTOOLS)/hooks/mandos install --mode=u=rw,go=r initramfs-tools-hook-conf \ === modified file 'TODO' --- TODO 2014-10-05 19:39:25 +0000 +++ TODO 2015-08-10 09:00:23 +0000 @@ -1,7 +1,7 @@ -*- org -*- * GIT -** General: [[https://www.atlassian.com/git/workflows][Git Workflows]], [[http://gitimmersion.com/][Git Immersion]], [[https://news.ycombinator.com/item?id=7036628][Simple git workflow is simple]] +** General: [[https://www.atlassian.com/git/workflows][Git Workflows]], [[http://gitimmersion.com/][Git Immersion]], [[https://news.ycombinator.com/item?id=7036628][Simple git workflow is simple]] [[https://news.ycombinator.com/item?id=9661349][On undoing, fixing, or removing commits in git]] ** Intro: [[http://www.eyrie.org/~eagle/notes/debian/git.html#combine][Using Git for Debian Packaging]] ** Use: [[https://honk.sigxcpu.org/piki/projects/git-buildpackage/][git-buildpackage]] ** Migration @@ -21,15 +21,17 @@ * mandos-client ** TODO [#B] Use capabilities instead of seteuid(). + https://forums.grsecurity.net/viewtopic.php?f=7&t=2522 ** TODO [#B] Use getaddrinfo(hints=AI_NUMERICHOST) instead of inet_pton() ** TODO [#C] Make start_mandos_communication() take "struct server". ** TODO [#C] --interfaces=regex,eth*,noregex (bridge-utils-interfaces(5)) +** TODO [#C] Remove code for GNU libc < 2.15 * splashy ** TODO [#B] use scandir(3) instead of readdir(3) * usplash (Deprecated) -** TODO [#A] Make it work again +** TODO [#B] Make it work again ** TODO [#B] use scandir(3) instead of readdir(3) * askpass-fifo @@ -46,11 +48,12 @@ *** Hook up stderr of plugins, buffer them, and prepend "Mandos Plugin [plugin name]" ** TODO [#C] use same file name rules as run-parts(8) ** kernel command line option for debug info +** TODO [#C] Remove code for GNU libc < 2.15 * mandos (server) -** TODO [#B] Work around Avahi issue - Avahi does not announce link-local addresses if any global - addresses exist: http://lists.freedesktop.org/archives/avahi/2010-March/001863.html +** TODO [#B] --notify-command + This would allow the mandos.service to use + --notify-command="systemd-notify --pid READY=1" ** TODO [#B] Log level :BUGS: *** TODO /etc/mandos/clients.d/*.conf Watch this directory and add/remove/update clients? @@ -65,16 +68,13 @@ + Approve(False) -> Close client connection immediately ** TODO [#C] python-parsedatetime ** TODO Separate logging logic to own object -** TODO [#A] Limit approval_delay to max gnutls/tls timeout value +** TODO [#B] Limit approval_delay to max gnutls/tls timeout value ** TODO [#B] break the wait on approval_delay if connection dies ** TODO Generate Client.runtime_expansions from client options + extra ** TODO Allow %%(checker)s as a runtime expansion ** TODO Use python-tlslite? ** TODO D-Bus AddClient() method on server object ** TODO Use org.freedesktop.DBus.Method.NoReply annotation on async methods. :2: -** TODO Support [[http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager][org.freedesktop.DBus.ObjectManager]] interface on server object :2: - Deprecate methods GetAllClients(), GetAllClientsWithProperties() - and signals ClientAdded and ClientRemoved. ** TODO Save state periodically to recover better from hard shutdowns ** TODO CheckerCompleted method, deprecate CheckedOK ** TODO Secret Service API? @@ -87,6 +87,7 @@ ** TODO Error handling on error parsing config files ** TODO init.d script error handling ** TODO D-Bus server properties; address, port, interface, etc. :2: +** TODO [#C] In Python 3.3, use shlex.quote() instead of re.escape() * mandos.xml ** Add mandos contact info in manual pages @@ -119,7 +120,7 @@ *** TODO [#C] use same file name rules as run-parts(8) *** TODO [#C] Do not install in initrd.img if configured not to. Use "/etc/initramfs-tools/hooksconf.d/mandos"? -** TODO [#C] /etc/bash_completion.d/mandos +** TODO [#C] $(pkg-config --variable=completionsdir bash-completion) From XML sources directly? * Side Stuff === modified file 'debian/control' --- debian/control 2014-10-05 19:46:11 +0000 +++ debian/control 2015-07-09 20:32:52 +0000 @@ -6,10 +6,10 @@ Björn Påhlsson Build-Depends: debhelper (>= 9), docbook-xml, docbook-xsl, libavahi-core-dev, libgpgme11-dev, libgnutls28-dev - | gnutls-dev, xsltproc, pkg-config + | gnutls-dev, xsltproc, pkg-config, libnl-route-3-dev Build-Depends-Indep: systemd, python2.7, python2.7-gnutls, python2.7-dbus, python2.7-avahi, python2.7-gobject -Standards-Version: 3.9.5 +Standards-Version: 3.9.6 Vcs-Bzr: http://ftp.recompile.se/pub/mandos/trunk Vcs-Browser: http://bzr.recompile.se/loggerhead/mandos/trunk/files Homepage: http://www.recompile.se/mandos @@ -41,7 +41,7 @@ Architecture: linux-any Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, cryptsetup, gnupg (<< 2), initramfs-tools, dpkg-dev (>=1.16.0) -Recommends: ssh +Recommends: ssh, gnutls-bin | openssl Breaks: dropbear (<= 0.53.1-1) Enhances: cryptsetup Description: do unattended reboots with an encrypted root file system === modified file 'debian/copyright' --- debian/copyright 2014-03-01 09:39:25 +0000 +++ debian/copyright 2015-07-20 04:03:32 +0000 @@ -4,8 +4,8 @@ Source: Files: * -Copyright: Copyright © 2008-2014 Teddy Hogeborn - Copyright © 2008-2014 Björn Påhlsson +Copyright: Copyright © 2008-2015 Teddy Hogeborn + Copyright © 2008-2015 Björn Påhlsson License: GPL-3+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as === modified file 'debian/mandos-client.README.Debian' --- debian/mandos-client.README.Debian 2013-10-28 10:04:05 +0000 +++ debian/mandos-client.README.Debian 2015-07-12 01:25:14 +0000 @@ -1,3 +1,7 @@ +This file documents the next steps to take after installation of the +Debian package, and also contain some notes specific to the Debian +packaging which are not also in the manual. + * Adding a Client Password to the Server The server must be given a password to give back to the client on @@ -16,6 +20,8 @@ is possible to verify that the correct password will be received by this client by running the command, on the client: + MANDOSPLUGINHELPERDIR=/usr/lib/$(dpkg-architecture \ + -qDEB_HOST_MULTIARCH)/mandos/plugin-helpers \ /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH \ )/mandos/plugins.d/mandos-client \ --pubkey=/etc/keys/mandos/pubkey.txt \ @@ -91,4 +97,13 @@ work, "--options-for=mandos-client:--connect=
:" needs to be manually added to the file "/etc/mandos/plugin-runner.conf". - -- Teddy Hogeborn , Mon, 28 Oct 2013 11:02:26 +0100 +* Diffie-Hellman Parameters + + On installation, a file with Diffie-Hellman parameters, + /etc/keys/mandos/dhparams.pem, will be generated and automatically + installed into the initital RAM disk image and also used by the + Mandos Client on boot. If different parameters are needed for + policy or other reasons, simply replace the existing dhparams.pem + file and update the initital RAM disk image. + + -- Teddy Hogeborn , Sun, 12 Jul 2015 03:24:24 +0200 === modified file 'debian/mandos-client.postinst' --- debian/mandos-client.postinst 2011-10-10 20:29:58 +0000 +++ debian/mandos-client.postinst 2015-07-12 02:08:25 +0000 @@ -20,9 +20,7 @@ # Update the initial RAM file system image update_initramfs() { - if [ -x /usr/sbin/update-initramfs ]; then - update-initramfs -u -k all - fi + update-initramfs -u -k all if dpkg --compare-versions "$2" lt-nl "1.0.10-1"; then # Make old initrd.img files unreadable too, in case they were @@ -58,15 +56,39 @@ -a -r /etc/keys/mandos/seckey.txt ]; then return 0 fi - if [ -x /usr/sbin/mandos-keygen ]; then - mandos-keygen - fi + mandos-keygen +} + +create_dh_params(){ + if [ -r /etc/keys/mandos/dhparams.pem ]; then + return 0 + fi + # Create a Diffe-Hellman parameters file + DHFILE="`mktemp -t mandos-client-dh-parameters.XXXXXXXXXX.pem`" + # First try certtool from GnuTLS + if ! certtool --generate-dh-params --sec-param high \ + --outfile "$DHFILE"; then + # Otherwise try OpenSSL + if ! openssl genpkey -genparam -algorithm DH -out "$DHFILE" \ + -pkeyopt dh_paramgen_prime_len:3072; then + # None of the commands succeded; give up + rm -- "$DHFILE" + return 1 + fi + fi + sed --in-place --expression='0,/^-----BEGIN DH PARAMETERS-----$/d' \ + "$DHFILE" + sed --in-place --expression='1i-----BEGIN DH PARAMETERS-----' \ + "$DHFILE" + cp --archive "$DHFILE" /etc/keys/mandos/dhparams.pem + rm -- "$DHFILE" } case "$1" in configure) add_mandos_user "$@" create_key "$@" + create_dh_params "$@" || : update_initramfs "$@" ;; abort-upgrade|abort-deconfigure|abort-remove) === modified file 'debian/mandos-client.postrm' --- debian/mandos-client.postrm 2011-10-10 20:29:58 +0000 +++ debian/mandos-client.postrm 2015-07-27 09:23:56 +0000 @@ -31,9 +31,7 @@ # Update the initial RAM file system image update_initramfs() { - if [ -x /usr/sbin/update-initramfs ]; then - update-initramfs -u -k all - fi + update-initramfs -u -k all || : } case "$1" in @@ -45,7 +43,8 @@ shred --remove /etc/keys/mandos/seckey.txt 2>/dev/null || : rm --force /etc/mandos/plugin-runner.conf \ /etc/keys/mandos/pubkey.txt \ - /etc/keys/mandos/seckey.txt 2>/dev/null + /etc/keys/mandos/seckey.txt \ + /etc/keys/mandos/dhparams.pem 2>/dev/null update_initramfs ;; upgrade|failed-upgrade|disappear|abort-install|abort-upgrade) === modified file 'debian/mandos.prerm' --- debian/mandos.prerm 2011-10-10 20:29:58 +0000 +++ debian/mandos.prerm 2015-07-12 01:57:54 +0000 @@ -17,13 +17,7 @@ case "$1" in remove|deconfigure) - if [ -x /etc/init.d/mandos ]; then - if [ -x /usr/sbin/invoke-rc.d ]; then - invoke-rc.d mandos stop - else - /etc/init.d/mandos stop - fi - fi + invoke-rc.d mandos stop || : ;; upgrade|failed-upgrade) ;; === modified file 'initramfs-tools-hook' --- initramfs-tools-hook 2014-07-16 23:44:54 +0000 +++ initramfs-tools-hook 2015-07-09 20:36:29 +0000 @@ -70,11 +70,13 @@ CONFDIR="/conf/conf.d/mandos" MANDOSDIR="/lib/mandos" PLUGINDIR="${MANDOSDIR}/plugins.d" +PLUGINHELPERDIR="${MANDOSDIR}/plugin-helpers" HOOKDIR="${MANDOSDIR}/network-hooks.d" # Make directories install --directory --mode=u=rwx,go=rx "${DESTDIR}${CONFDIR}" \ - "${DESTDIR}${MANDOSDIR}" "${DESTDIR}${HOOKDIR}" + "${DESTDIR}${MANDOSDIR}" "${DESTDIR}${HOOKDIR}" \ + "${DESTDIR}${PLUGINHELPERDIR}" install --owner=${mandos_user} --group=${mandos_group} --directory \ --mode=u=rwx "${DESTDIR}${PLUGINDIR}" @@ -98,6 +100,21 @@ esac done +# Copy the packaged plugin helpers +for file in "$libdir"/mandos/plugin-helpers/*; do + base="`basename \"$file\"`" + # Is this plugin overridden? + if [ -e "/etc/mandos/plugin-helpers/$base" ]; then + continue + fi + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) copy_exec "$file" "${PLUGINHELPERDIR}" ;; + esac +done + # Copy any user-supplied plugins for file in /etc/mandos/plugins.d/*; do base="`basename \"$file\"`" @@ -109,6 +126,17 @@ esac done +# Copy any user-supplied plugin helpers +for file in /etc/mandos/plugin-helpers/*; do + base="`basename \"$file\"`" + case "$base" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) copy_exec "$file" "${PLUGINHELPERDIR}" ;; + esac +done + # Get DEVICE from initramfs.conf and other files . /etc/initramfs-tools/initramfs.conf for conf in /etc/initramfs-tools/conf.d/*; do @@ -191,10 +219,24 @@ if [ -d "$file" ]; then continue fi - cp --archive --sparse=always "$file" "${DESTDIR}${CONFDIR}" - chown ${mandos_user}:${mandos_group} \ - "${DESTDIR}${CONFDIR}/`basename \"$file\"`" + case "$file" in + *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert) + : ;; + "*") : ;; + *) + cp --archive --sparse=always "$file" \ + "${DESTDIR}${CONFDIR}" + chown ${mandos_user}:${mandos_group} \ + "${DESTDIR}${CONFDIR}/`basename \"$file\"`" + ;; + esac done +# Use Diffie-Hellman parameters file if available +if [ -e "${DESTDIR}${CONFDIR}"/dhparams.pem ]; then + sed --in-place \ + --expression="1i--options-for=mandos-client:--dh-params=${CONFDIR}/dhparams.pem" \ + "${DESTDIR}/${CONFDIR}/plugin-runner.conf" +fi # /lib/mandos/plugin-runner will drop priviliges, but needs access to # its plugin directory and its config file. However, since almost all === modified file 'intro.xml' --- intro.xml 2014-06-22 02:19:30 +0000 +++ intro.xml 2015-07-20 04:03:32 +0000 @@ -1,7 +1,7 @@ + %common; ]> @@ -32,6 +32,9 @@ 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson @@ -197,6 +200,16 @@ + + How about sniffing the network traffic and decrypting it + later by physically grabbing the Mandos client and using its + key? + + We only use PFS (Perfect Forward Security) + key exchange algorithms in TLS, which protects against this. + + + Physically grabbing the Mandos server computer? === modified file 'mandos' --- mandos 2014-10-05 20:08:58 +0000 +++ mandos 2015-08-10 10:42:52 +0000 @@ -11,8 +11,8 @@ # "AvahiService" class, and some lines in "main". # # Everything else is -# Copyright © 2008-2014 Teddy Hogeborn -# Copyright © 2008-2014 Björn Påhlsson +# Copyright © 2008-2015 Teddy Hogeborn +# Copyright © 2008-2015 Björn Påhlsson # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -36,7 +36,10 @@ from future_builtins import * -import SocketServer as socketserver +try: + import SocketServer as socketserver +except ImportError: + import socketserver import socket import argparse import datetime @@ -47,7 +50,10 @@ import gnutls.library.functions import gnutls.library.constants import gnutls.library.types -import ConfigParser as configparser +try: + import ConfigParser as configparser +except ImportError: + import configparser import sys import re import os @@ -62,17 +68,24 @@ import struct import fcntl import functools -import cPickle as pickle +try: + import cPickle as pickle +except ImportError: + import pickle import multiprocessing import types import binascii import tempfile import itertools import collections +import codecs import dbus import dbus.service -import gobject +try: + import gobject +except ImportError: + from gi.repository import GObject as gobject import avahi from dbus.mainloop.glib import DBusGMainLoop import ctypes @@ -98,10 +111,10 @@ syslogger = None try: - if_nametoindex = (ctypes.cdll.LoadLibrary - (ctypes.util.find_library("c")) - .if_nametoindex) + if_nametoindex = ctypes.cdll.LoadLibrary( + ctypes.util.find_library("c")).if_nametoindex except (OSError, AttributeError): + def if_nametoindex(interface): "Get an interface index the hard way, i.e. using fcntl()" SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h @@ -116,10 +129,9 @@ """init logger and add loglevel""" global syslogger - syslogger = (logging.handlers.SysLogHandler - (facility = - logging.handlers.SysLogHandler.LOG_DAEMON, - address = "/dev/log")) + syslogger = (logging.handlers.SysLogHandler( + facility = logging.handlers.SysLogHandler.LOG_DAEMON, + address = "/dev/log")) syslogger.setFormatter(logging.Formatter ('Mandos [%(process)d]: %(levelname)s:' ' %(message)s')) @@ -142,6 +154,7 @@ class PGPEngine(object): """A simple class for OpenPGP symmetric encryption & decryption""" + def __init__(self): self.tempdir = tempfile.mkdtemp(prefix="mandos-") self.gnupgargs = ['--batch', @@ -186,8 +199,8 @@ def encrypt(self, data, password): passphrase = self.password_encode(password) - with tempfile.NamedTemporaryFile(dir=self.tempdir - ) as passfile: + with tempfile.NamedTemporaryFile( + dir=self.tempdir) as passfile: passfile.write(passphrase) passfile.flush() proc = subprocess.Popen(['gpg', '--symmetric', @@ -204,8 +217,8 @@ def decrypt(self, data, password): passphrase = self.password_encode(password) - with tempfile.NamedTemporaryFile(dir = self.tempdir - ) as passfile: + with tempfile.NamedTemporaryFile( + dir = self.tempdir) as passfile: passfile.write(passphrase) passfile.flush() proc = subprocess.Popen(['gpg', '--decrypt', @@ -215,8 +228,7 @@ stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE) - decrypted_plaintext, err = proc.communicate(input - = data) + decrypted_plaintext, err = proc.communicate(input = data) if proc.returncode != 0: raise PGPError(err) return decrypted_plaintext @@ -228,9 +240,11 @@ return super(AvahiError, self).__init__(value, *args, **kwargs) + class AvahiServiceError(AvahiError): pass + class AvahiGroupError(AvahiError): pass @@ -256,10 +270,17 @@ bus: dbus.SystemBus() """ - def __init__(self, interface = avahi.IF_UNSPEC, name = None, - servicetype = None, port = None, TXT = None, - domain = "", host = "", max_renames = 32768, - protocol = avahi.PROTO_UNSPEC, bus = None): + def __init__(self, + interface = avahi.IF_UNSPEC, + name = None, + servicetype = None, + port = None, + TXT = None, + domain = "", + host = "", + max_renames = 32768, + protocol = avahi.PROTO_UNSPEC, + bus = None): self.interface = interface self.name = name self.type = servicetype @@ -275,25 +296,31 @@ self.bus = bus self.entry_group_state_changed_match = None - def rename(self): + def rename(self, remove=True): """Derived from the Avahi example code""" if self.rename_count >= self.max_renames: logger.critical("No suitable Zeroconf service name found" " after %i retries, exiting.", self.rename_count) raise AvahiServiceError("Too many renames") - self.name = str(self.server - .GetAlternativeServiceName(self.name)) + self.name = str( + self.server.GetAlternativeServiceName(self.name)) + self.rename_count += 1 logger.info("Changing Zeroconf service name to %r ...", self.name) - self.remove() + if remove: + self.remove() try: self.add() except dbus.exceptions.DBusException as error: - logger.critical("D-Bus Exception", exc_info=error) - self.cleanup() - os._exit(1) - self.rename_count += 1 + if (error.get_dbus_name() + == "org.freedesktop.Avahi.CollisionError"): + logger.info("Local Zeroconf service name collision.") + return self.rename(remove=False) + else: + logger.critical("D-Bus Exception", exc_info=error) + self.cleanup() + os._exit(1) def remove(self): """Derived from the Avahi example code""" @@ -338,8 +365,7 @@ elif state == avahi.ENTRY_GROUP_FAILURE: logger.critical("Avahi: Error in group state changed %s", str(error)) - raise AvahiGroupError("State changed: {!s}" - .format(error)) + raise AvahiGroupError("State changed: {!s}".format(error)) def cleanup(self): """Derived from the Avahi example code""" @@ -355,13 +381,12 @@ def server_state_changed(self, state, error=None): """Derived from the Avahi example code""" logger.debug("Avahi server state change: %i", state) - bad_states = { avahi.SERVER_INVALID: - "Zeroconf server invalid", - avahi.SERVER_REGISTERING: None, - avahi.SERVER_COLLISION: - "Zeroconf server name collision", - avahi.SERVER_FAILURE: - "Zeroconf server failure" } + bad_states = { + avahi.SERVER_INVALID: "Zeroconf server invalid", + avahi.SERVER_REGISTERING: None, + avahi.SERVER_COLLISION: "Zeroconf server name collision", + avahi.SERVER_FAILURE: "Zeroconf server failure", + } if state in bad_states: if bad_states[state] is not None: if error is None: @@ -370,7 +395,18 @@ logger.error(bad_states[state] + ": %r", error) self.cleanup() elif state == avahi.SERVER_RUNNING: - self.add() + try: + self.add() + except dbus.exceptions.DBusException as error: + if (error.get_dbus_name() + == "org.freedesktop.Avahi.CollisionError"): + logger.info("Local Zeroconf service name" + " collision.") + return self.rename(remove=False) + else: + logger.critical("D-Bus Exception", exc_info=error) + self.cleanup() + os._exit(1) else: if error is None: logger.debug("Unknown state: %r", state) @@ -386,20 +422,28 @@ follow_name_owner_changes=True), avahi.DBUS_INTERFACE_SERVER) self.server.connect_to_signal("StateChanged", - self.server_state_changed) + self.server_state_changed) self.server_state_changed(self.server.GetState()) class AvahiServiceToSyslog(AvahiService): - def rename(self): + def rename(self, *args, **kwargs): """Add the new name to the syslog messages""" - ret = AvahiService.rename(self) - syslogger.setFormatter(logging.Formatter - ('Mandos ({}) [%(process)d]:' - ' %(levelname)s: %(message)s' - .format(self.name))) + ret = AvahiService.rename(self, *args, **kwargs) + syslogger.setFormatter(logging.Formatter( + 'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s' + .format(self.name))) return ret +def call_pipe(connection, # : multiprocessing.Connection + func, *args, **kwargs): + """This function is meant to be called by multiprocessing.Process + + This function runs func(*args, **kwargs), and writes the resulting + return value on the provided multiprocessing.Connection. + """ + connection.send(func(*args, **kwargs)) + connection.close() class Client(object): """A representation of a client host served by this server. @@ -432,6 +476,8 @@ last_checker_status: integer between 0 and 255 reflecting exit status of last checker. -1 reflects crashed checker, -2 means no checker completed yet. + last_checker_signal: The signal which killed the last checker, if + last_checker_status is -1 last_enabled: datetime.datetime(); (UTC) or None name: string; from the config file, used in log messages and D-Bus identifiers @@ -450,16 +496,17 @@ "fingerprint", "host", "interval", "last_approval_request", "last_checked_ok", "last_enabled", "name", "timeout") - client_defaults = { "timeout": "PT5M", - "extended_timeout": "PT15M", - "interval": "PT2M", - "checker": "fping -q -- %%(host)s", - "host": "", - "approval_delay": "PT0S", - "approval_duration": "PT1S", - "approved_by_default": "True", - "enabled": "True", - } + client_defaults = { + "timeout": "PT5M", + "extended_timeout": "PT15M", + "interval": "PT2M", + "checker": "fping -q -- %%(host)s", + "host": "", + "approval_delay": "PT0S", + "approval_duration": "PT1S", + "approved_by_default": "True", + "enabled": "True", + } @staticmethod def config_parser(config): @@ -481,6 +528,9 @@ client["enabled"] = config.getboolean(client_name, "enabled") + # Uppercase and remove spaces from fingerprint for later + # comparison purposes with return value from the + # fingerprint() function client["fingerprint"] = (section["fingerprint"].upper() .replace(" ", "")) if "secret" in section: @@ -528,9 +578,6 @@ self.expires = None logger.debug("Creating client %r", self.name) - # Uppercase and remove spaces from fingerprint for later - # comparison purposes with return value from the fingerprint() - # function logger.debug(" Fingerprint: %s", self.fingerprint) self.created = settings.get("created", datetime.datetime.utcnow()) @@ -543,18 +590,15 @@ self.current_checker_command = None self.approved = None self.approvals_pending = 0 - self.changedstate = (multiprocessing_manager - .Condition(multiprocessing_manager - .Lock())) - self.client_structure = [attr for attr in - self.__dict__.iterkeys() + self.changedstate = multiprocessing_manager.Condition( + multiprocessing_manager.Lock()) + self.client_structure = [attr + for attr in self.__dict__.iterkeys() if not attr.startswith("_")] self.client_structure.append("client_structure") - for name, t in inspect.getmembers(type(self), - lambda obj: - isinstance(obj, - property)): + for name, t in inspect.getmembers( + type(self), lambda obj: isinstance(obj, property)): if not name.startswith("_"): self.client_structure.append(name) @@ -602,42 +646,47 @@ # and every interval from then on. if self.checker_initiator_tag is not None: gobject.source_remove(self.checker_initiator_tag) - self.checker_initiator_tag = (gobject.timeout_add - (int(self.interval - .total_seconds() * 1000), - self.start_checker)) + self.checker_initiator_tag = gobject.timeout_add( + int(self.interval.total_seconds() * 1000), + self.start_checker) # Schedule a disable() when 'timeout' has passed if self.disable_initiator_tag is not None: gobject.source_remove(self.disable_initiator_tag) - self.disable_initiator_tag = (gobject.timeout_add - (int(self.timeout - .total_seconds() * 1000), - self.disable)) + self.disable_initiator_tag = gobject.timeout_add( + int(self.timeout.total_seconds() * 1000), self.disable) # Also start a new checker *right now*. self.start_checker() - def checker_callback(self, pid, condition, command): + def checker_callback(self, source, condition, connection, + command): """The checker has completed, so take appropriate actions.""" self.checker_callback_tag = None self.checker = None - if os.WIFEXITED(condition): - self.last_checker_status = os.WEXITSTATUS(condition) + # Read return code from connection (see call_pipe) + returncode = connection.recv() + connection.close() + + if returncode >= 0: + self.last_checker_status = returncode + self.last_checker_signal = None if self.last_checker_status == 0: logger.info("Checker for %(name)s succeeded", vars(self)) self.checked_ok() else: - logger.info("Checker for %(name)s failed", - vars(self)) + logger.info("Checker for %(name)s failed", vars(self)) else: self.last_checker_status = -1 + self.last_checker_signal = -returncode logger.warning("Checker for %(name)s crashed?", vars(self)) + return False def checked_ok(self): """Assert that the client has been seen, alive and well.""" self.last_checked_ok = datetime.datetime.utcnow() self.last_checker_status = 0 + self.last_checker_signal = None self.bump_timeout() def bump_timeout(self, timeout=None): @@ -648,9 +697,8 @@ gobject.source_remove(self.disable_initiator_tag) self.disable_initiator_tag = None if getattr(self, "enabled", False): - self.disable_initiator_tag = (gobject.timeout_add - (int(timeout.total_seconds() - * 1000), self.disable)) + self.disable_initiator_tag = gobject.timeout_add( + int(timeout.total_seconds() * 1000), self.disable) self.expires = datetime.datetime.utcnow() + timeout def need_approval(self): @@ -670,74 +718,49 @@ # than 'timeout' for the client to be disabled, which is as it # should be. - # If a checker exists, make sure it is not a zombie - try: - pid, status = os.waitpid(self.checker.pid, os.WNOHANG) - except AttributeError: - pass - except OSError as error: - if error.errno != errno.ECHILD: - raise - else: - if pid: - logger.warning("Checker was a zombie") - gobject.source_remove(self.checker_callback_tag) - self.checker_callback(pid, status, - self.current_checker_command) + if self.checker is not None and not self.checker.is_alive(): + logger.warning("Checker was not alive; joining") + self.checker.join() + self.checker = None # Start a new checker if needed if self.checker is None: # Escape attributes for the shell - escaped_attrs = { attr: - re.escape(str(getattr(self, attr))) - for attr in self.runtime_expansions } + escaped_attrs = { + attr: re.escape(str(getattr(self, attr))) + for attr in self.runtime_expansions } try: command = self.checker_command % escaped_attrs except TypeError as error: logger.error('Could not format string "%s"', - self.checker_command, exc_info=error) - return True # Try again later + self.checker_command, + exc_info=error) + return True # Try again later self.current_checker_command = command - try: - logger.info("Starting checker %r for %s", - command, self.name) - # We don't need to redirect stdout and stderr, since - # in normal mode, that is already done by daemon(), - # and in debug mode we don't want to. (Stdin is - # always replaced by /dev/null.) - # The exception is when not debugging but nevertheless - # running in the foreground; use the previously - # created wnull. - popen_args = {} - if (not self.server_settings["debug"] - and self.server_settings["foreground"]): - popen_args.update({"stdout": wnull, - "stderr": wnull }) - self.checker = subprocess.Popen(command, - close_fds=True, - shell=True, cwd="/", - **popen_args) - except OSError as error: - logger.error("Failed to start subprocess", - exc_info=error) - return True - self.checker_callback_tag = (gobject.child_watch_add - (self.checker.pid, - self.checker_callback, - data=command)) - # The checker may have completed before the gobject - # watch was added. Check for this. - try: - pid, status = os.waitpid(self.checker.pid, os.WNOHANG) - except OSError as error: - if error.errno == errno.ECHILD: - # This should never happen - logger.error("Child process vanished", - exc_info=error) - return True - raise - if pid: - gobject.source_remove(self.checker_callback_tag) - self.checker_callback(pid, status, command) + logger.info("Starting checker %r for %s", command, + self.name) + # We don't need to redirect stdout and stderr, since + # in normal mode, that is already done by daemon(), + # and in debug mode we don't want to. (Stdin is + # always replaced by /dev/null.) + # The exception is when not debugging but nevertheless + # running in the foreground; use the previously + # created wnull. + popen_args = { "close_fds": True, + "shell": True, + "cwd": "/" } + if (not self.server_settings["debug"] + and self.server_settings["foreground"]): + popen_args.update({"stdout": wnull, + "stderr": wnull }) + pipe = multiprocessing.Pipe(duplex = False) + self.checker = multiprocessing.Process( + target = call_pipe, + args = (pipe[1], subprocess.call, command), + kwargs = popen_args) + self.checker.start() + self.checker_callback_tag = gobject.io_add_watch( + pipe[0].fileno(), gobject.IO_IN, + self.checker_callback, pipe[0], command) # Re-run this periodically if run by gobject.timeout_add return True @@ -749,19 +772,14 @@ if getattr(self, "checker", None) is None: return logger.debug("Stopping checker for %(name)s", vars(self)) - try: - self.checker.terminate() - #time.sleep(0.5) - #if self.checker.poll() is None: - # self.checker.kill() - except OSError as error: - if error.errno != errno.ESRCH: # No such process - raise + self.checker.terminate() self.checker = None -def dbus_service_property(dbus_interface, signature="v", - access="readwrite", byte_arrays=False): +def dbus_service_property(dbus_interface, + signature="v", + access="readwrite", + byte_arrays=False): """Decorators for marking methods of a DBusObjectWithProperties to become properties on the D-Bus. @@ -777,6 +795,7 @@ if byte_arrays and signature != "ay": raise ValueError("Byte arrays not supported for non-'ay'" " signature {!r}".format(signature)) + def decorator(func): func._dbus_is_property = True func._dbus_interface = dbus_interface @@ -787,6 +806,7 @@ func._dbus_name = func._dbus_name[:-14] func._dbus_get_args_options = {'byte_arrays': byte_arrays } return func + return decorator @@ -801,11 +821,13 @@ "org.freedesktop.DBus.Property.EmitsChangedSignal": "false"} """ + def decorator(func): func._dbus_is_interface = True func._dbus_interface = dbus_interface func._dbus_name = dbus_interface return func + return decorator @@ -820,10 +842,14 @@ access="r") def Property_dbus_property(self): return dbus.Boolean(False) + + See also the DBusObjectWithAnnotations class. """ + def decorator(func): func._dbus_annotations = annotations return func + return decorator @@ -832,6 +858,7 @@ """ pass + class DBusPropertyAccessException(DBusPropertyException): """A property's access permissions disallows an operation. """ @@ -844,12 +871,11 @@ pass -class DBusObjectWithProperties(dbus.service.Object): - """A D-Bus object with properties. +class DBusObjectWithAnnotations(dbus.service.Object): + """A D-Bus object with annotations. - Classes inheriting from this can use the dbus_service_property - decorator to expose methods as D-Bus properties. It exposes the - standard Get(), Set(), and GetAll() methods on the D-Bus. + Classes inheriting from this can use the dbus_annotations + decorator to add annotations to methods or signals. """ @staticmethod @@ -865,32 +891,109 @@ def _get_all_dbus_things(self, thing): """Returns a generator of (name, attribute) pairs """ - return ((getattr(athing.__get__(self), "_dbus_name", - name), + return ((getattr(athing.__get__(self), "_dbus_name", name), athing.__get__(self)) for cls in self.__class__.__mro__ for name, athing in - inspect.getmembers(cls, - self._is_dbus_thing(thing))) + inspect.getmembers(cls, self._is_dbus_thing(thing))) + + @dbus.service.method(dbus.INTROSPECTABLE_IFACE, + out_signature = "s", + path_keyword = 'object_path', + connection_keyword = 'connection') + def Introspect(self, object_path, connection): + """Overloading of standard D-Bus method. + + Inserts annotation tags on methods and signals. + """ + xmlstring = dbus.service.Object.Introspect(self, object_path, + connection) + try: + document = xml.dom.minidom.parseString(xmlstring) + + for if_tag in document.getElementsByTagName("interface"): + # Add annotation tags + for typ in ("method", "signal"): + for tag in if_tag.getElementsByTagName(typ): + annots = dict() + for name, prop in (self. + _get_all_dbus_things(typ)): + if (name == tag.getAttribute("name") + and prop._dbus_interface + == if_tag.getAttribute("name")): + annots.update(getattr( + prop, "_dbus_annotations", {})) + for name, value in annots.items(): + ann_tag = document.createElement( + "annotation") + ann_tag.setAttribute("name", name) + ann_tag.setAttribute("value", value) + tag.appendChild(ann_tag) + # Add interface annotation tags + for annotation, value in dict( + itertools.chain.from_iterable( + annotations().items() + for name, annotations + in self._get_all_dbus_things("interface") + if name == if_tag.getAttribute("name") + )).items(): + ann_tag = document.createElement("annotation") + ann_tag.setAttribute("name", annotation) + ann_tag.setAttribute("value", value) + if_tag.appendChild(ann_tag) + # Fix argument name for the Introspect method itself + if (if_tag.getAttribute("name") + == dbus.INTROSPECTABLE_IFACE): + for cn in if_tag.getElementsByTagName("method"): + if cn.getAttribute("name") == "Introspect": + for arg in cn.getElementsByTagName("arg"): + if (arg.getAttribute("direction") + == "out"): + arg.setAttribute("name", + "xml_data") + xmlstring = document.toxml("utf-8") + document.unlink() + except (AttributeError, xml.dom.DOMException, + xml.parsers.expat.ExpatError) as error: + logger.error("Failed to override Introspection method", + exc_info=error) + return xmlstring + + +class DBusObjectWithProperties(DBusObjectWithAnnotations): + """A D-Bus object with properties. + + Classes inheriting from this can use the dbus_service_property + decorator to expose methods as D-Bus properties. It exposes the + standard Get(), Set(), and GetAll() methods on the D-Bus. + """ def _get_dbus_property(self, interface_name, property_name): """Returns a bound method if one exists which is a D-Bus property with the specified name and interface. """ - for cls in self.__class__.__mro__: - for name, value in (inspect.getmembers - (cls, - self._is_dbus_thing("property"))): + for cls in self.__class__.__mro__: + for name, value in inspect.getmembers( + cls, self._is_dbus_thing("property")): if (value._dbus_name == property_name and value._dbus_interface == interface_name): return value.__get__(self) # No such property - raise DBusPropertyNotFound(self.dbus_object_path + ":" - + interface_name + "." - + property_name) - - @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss", + raise DBusPropertyNotFound("{}:{}.{}".format( + self.dbus_object_path, interface_name, property_name)) + + @classmethod + def _get_all_interface_names(cls): + """Get a sequence of all interfaces supported by an object""" + return (name for name in set(getattr(getattr(x, attr), + "_dbus_interface", None) + for x in (inspect.getmro(cls)) + for attr in dir(x)) + if name is not None) + + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature="ss", out_signature="v") def Get(self, interface_name, property_name): """Standard D-Bus property Get() method, see D-Bus standard. @@ -921,7 +1024,8 @@ for byte in value)) prop(value) - @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s", + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature="s", out_signature="a{sv}") def GetAll(self, interface_name): """Standard D-Bus property GetAll() method, see D-Bus @@ -942,8 +1046,8 @@ if not hasattr(value, "variant_level"): properties[name] = value continue - properties[name] = type(value)(value, variant_level= - value.variant_level+1) + properties[name] = type(value)( + value, variant_level = value.variant_level + 1) return dbus.Dictionary(properties, signature="sv") @dbus.service.signal(dbus.PROPERTIES_IFACE, signature="sa{sv}as") @@ -963,16 +1067,19 @@ Inserts property tags and interface annotation tags. """ - xmlstring = dbus.service.Object.Introspect(self, object_path, - connection) + xmlstring = DBusObjectWithAnnotations.Introspect(self, + object_path, + connection) try: document = xml.dom.minidom.parseString(xmlstring) + def make_tag(document, name, prop): e = document.createElement("property") e.setAttribute("name", name) e.setAttribute("type", prop._dbus_signature) e.setAttribute("access", prop._dbus_access) return e + for if_tag in document.getElementsByTagName("interface"): # Add property tags for tag in (make_tag(document, name, prop) @@ -981,37 +1088,22 @@ if prop._dbus_interface == if_tag.getAttribute("name")): if_tag.appendChild(tag) - # Add annotation tags - for typ in ("method", "signal", "property"): - for tag in if_tag.getElementsByTagName(typ): - annots = dict() - for name, prop in (self. - _get_all_dbus_things(typ)): - if (name == tag.getAttribute("name") - and prop._dbus_interface - == if_tag.getAttribute("name")): - annots.update(getattr - (prop, - "_dbus_annotations", - {})) - for name, value in annots.items(): - ann_tag = document.createElement( - "annotation") - ann_tag.setAttribute("name", name) - ann_tag.setAttribute("value", value) - tag.appendChild(ann_tag) - # Add interface annotation tags - for annotation, value in dict( - itertools.chain.from_iterable( - annotations().items() - for name, annotations in - self._get_all_dbus_things("interface") - if name == if_tag.getAttribute("name") - )).items(): - ann_tag = document.createElement("annotation") - ann_tag.setAttribute("name", annotation) - ann_tag.setAttribute("value", value) - if_tag.appendChild(ann_tag) + # Add annotation tags for properties + for tag in if_tag.getElementsByTagName("property"): + annots = dict() + for name, prop in self._get_all_dbus_things( + "property"): + if (name == tag.getAttribute("name") + and prop._dbus_interface + == if_tag.getAttribute("name")): + annots.update(getattr( + prop, "_dbus_annotations", {})) + for name, value in annots.items(): + ann_tag = document.createElement( + "annotation") + ann_tag.setAttribute("name", name) + ann_tag.setAttribute("value", value) + tag.appendChild(ann_tag) # Add the names to the return values for the # "org.freedesktop.DBus.Properties" methods if (if_tag.getAttribute("name") @@ -1035,13 +1127,80 @@ exc_info=error) return xmlstring +try: + dbus.OBJECT_MANAGER_IFACE +except AttributeError: + dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager" + +class DBusObjectWithObjectManager(DBusObjectWithAnnotations): + """A D-Bus object with an ObjectManager. + + Classes inheriting from this exposes the standard + GetManagedObjects call and the InterfacesAdded and + InterfacesRemoved signals on the standard + "org.freedesktop.DBus.ObjectManager" interface. + + Note: No signals are sent automatically; they must be sent + manually. + """ + @dbus.service.method(dbus.OBJECT_MANAGER_IFACE, + out_signature = "a{oa{sa{sv}}}") + def GetManagedObjects(self): + """This function must be overridden""" + raise NotImplementedError() + + @dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, + signature = "oa{sa{sv}}") + def InterfacesAdded(self, object_path, interfaces_and_properties): + pass + + @dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature = "oas") + def InterfacesRemoved(self, object_path, interfaces): + pass + + @dbus.service.method(dbus.INTROSPECTABLE_IFACE, + out_signature = "s", + path_keyword = 'object_path', + connection_keyword = 'connection') + def Introspect(self, object_path, connection): + """Overloading of standard D-Bus method. + + Override return argument name of GetManagedObjects to be + "objpath_interfaces_and_properties" + """ + xmlstring = DBusObjectWithAnnotations.Introspect(self, + object_path, + connection) + try: + document = xml.dom.minidom.parseString(xmlstring) + + for if_tag in document.getElementsByTagName("interface"): + # Fix argument name for the GetManagedObjects method + if (if_tag.getAttribute("name") + == dbus.OBJECT_MANAGER_IFACE): + for cn in if_tag.getElementsByTagName("method"): + if (cn.getAttribute("name") + == "GetManagedObjects"): + for arg in cn.getElementsByTagName("arg"): + if (arg.getAttribute("direction") + == "out"): + arg.setAttribute( + "name", + "objpath_interfaces" + "_and_properties") + xmlstring = document.toxml("utf-8") + document.unlink() + except (AttributeError, xml.dom.DOMException, + xml.parsers.expat.ExpatError) as error: + logger.error("Failed to override Introspection method", + exc_info = error) + return xmlstring def datetime_to_dbus(dt, variant_level=0): """Convert a UTC datetime.datetime() to a D-Bus type.""" if dt is None: return dbus.String("", variant_level = variant_level) - return dbus.String(dt.isoformat(), - variant_level=variant_level) + return dbus.String(dt.isoformat(), variant_level=variant_level) def alternate_dbus_interfaces(alt_interface_names, deprecate=True): @@ -1067,9 +1226,10 @@ (from DBusObjectWithProperties) and interfaces (from the dbus_interface_annotations decorator). """ + def wrapper(cls): for orig_interface_name, alt_interface_name in ( - alt_interface_names.items()): + alt_interface_names.items()): attr = {} interface_names = set() # Go though all attributes of the class @@ -1077,39 +1237,49 @@ # Ignore non-D-Bus attributes, and D-Bus attributes # with the wrong interface name if (not hasattr(attribute, "_dbus_interface") - or not attribute._dbus_interface - .startswith(orig_interface_name)): + or not attribute._dbus_interface.startswith( + orig_interface_name)): continue # Create an alternate D-Bus interface name based on # the current name - alt_interface = (attribute._dbus_interface - .replace(orig_interface_name, - alt_interface_name)) + alt_interface = attribute._dbus_interface.replace( + orig_interface_name, alt_interface_name) interface_names.add(alt_interface) # Is this a D-Bus signal? if getattr(attribute, "_dbus_is_signal", False): - # Extract the original non-method undecorated - # function by black magic - nonmethod_func = (dict( + if sys.version_info.major == 2: + # Extract the original non-method undecorated + # function by black magic + nonmethod_func = (dict( zip(attribute.func_code.co_freevars, - attribute.__closure__))["func"] - .cell_contents) + attribute.__closure__)) + ["func"].cell_contents) + else: + nonmethod_func = attribute # Create a new, but exactly alike, function # object, and decorate it to be a new D-Bus signal # with the alternate D-Bus interface name - new_function = (dbus.service.signal - (alt_interface, - attribute._dbus_signature) - (types.FunctionType( - nonmethod_func.func_code, - nonmethod_func.func_globals, - nonmethod_func.func_name, - nonmethod_func.func_defaults, - nonmethod_func.func_closure))) + if sys.version_info.major == 2: + new_function = types.FunctionType( + nonmethod_func.func_code, + nonmethod_func.func_globals, + nonmethod_func.func_name, + nonmethod_func.func_defaults, + nonmethod_func.func_closure) + else: + new_function = types.FunctionType( + nonmethod_func.__code__, + nonmethod_func.__globals__, + nonmethod_func.__name__, + nonmethod_func.__defaults__, + nonmethod_func.__closure__) + new_function = (dbus.service.signal( + alt_interface, + attribute._dbus_signature)(new_function)) # Copy annotations, if any try: - new_function._dbus_annotations = ( - dict(attribute._dbus_annotations)) + new_function._dbus_annotations = dict( + attribute._dbus_annotations) except AttributeError: pass # Define a creator of a function to call both the @@ -1120,11 +1290,18 @@ """This function is a scope container to pass func1 and func2 to the "call_both" function outside of its arguments""" + + @functools.wraps(func2) def call_both(*args, **kwargs): """This function will emit two D-Bus signals by calling func1 and func2""" func1(*args, **kwargs) func2(*args, **kwargs) + # Make wrapper function look like a D-Bus signal + for name, attr in inspect.getmembers(func2): + if name.startswith("_dbus_"): + setattr(call_both, name, attr) + return call_both # Create the "call_both" function and add it to # the class @@ -1135,20 +1312,20 @@ # object. Decorate it to be a new D-Bus method # with the alternate D-Bus interface name. Add it # to the class. - attr[attrname] = (dbus.service.method - (alt_interface, - attribute._dbus_in_signature, - attribute._dbus_out_signature) - (types.FunctionType - (attribute.func_code, - attribute.func_globals, - attribute.func_name, - attribute.func_defaults, - attribute.func_closure))) + attr[attrname] = ( + dbus.service.method( + alt_interface, + attribute._dbus_in_signature, + attribute._dbus_out_signature) + (types.FunctionType(attribute.func_code, + attribute.func_globals, + attribute.func_name, + attribute.func_defaults, + attribute.func_closure))) # Copy annotations, if any try: - attr[attrname]._dbus_annotations = ( - dict(attribute._dbus_annotations)) + attr[attrname]._dbus_annotations = dict( + attribute._dbus_annotations) except AttributeError: pass # Is this a D-Bus property? @@ -1157,23 +1334,21 @@ # object, and decorate it to be a new D-Bus # property with the alternate D-Bus interface # name. Add it to the class. - attr[attrname] = (dbus_service_property - (alt_interface, - attribute._dbus_signature, - attribute._dbus_access, - attribute - ._dbus_get_args_options - ["byte_arrays"]) - (types.FunctionType - (attribute.func_code, - attribute.func_globals, - attribute.func_name, - attribute.func_defaults, - attribute.func_closure))) + attr[attrname] = (dbus_service_property( + alt_interface, attribute._dbus_signature, + attribute._dbus_access, + attribute._dbus_get_args_options + ["byte_arrays"]) + (types.FunctionType( + attribute.func_code, + attribute.func_globals, + attribute.func_name, + attribute.func_defaults, + attribute.func_closure))) # Copy annotations, if any try: - attr[attrname]._dbus_annotations = ( - dict(attribute._dbus_annotations)) + attr[attrname]._dbus_annotations = dict( + attribute._dbus_annotations) except AttributeError: pass # Is this a D-Bus interface? @@ -1182,22 +1357,22 @@ # object. Decorate it to be a new D-Bus interface # with the alternate D-Bus interface name. Add it # to the class. - attr[attrname] = (dbus_interface_annotations - (alt_interface) - (types.FunctionType - (attribute.func_code, - attribute.func_globals, - attribute.func_name, - attribute.func_defaults, - attribute.func_closure))) + attr[attrname] = ( + dbus_interface_annotations(alt_interface) + (types.FunctionType(attribute.func_code, + attribute.func_globals, + attribute.func_name, + attribute.func_defaults, + attribute.func_closure))) if deprecate: # Deprecate all alternate interfaces iname="_AlternateDBusNames_interface_annotation{}" for interface_name in interface_names: + @dbus_interface_annotations(interface_name) def func(self): return { "org.freedesktop.DBus.Deprecated": - "true" } + "true" } # Find an unused name for aname in (iname.format(i) for i in itertools.count()): @@ -1208,13 +1383,14 @@ # Replace the class with a new subclass of it with # methods, signals, etc. as created above. cls = type(b"{}Alternate".format(cls.__name__), - (cls,), attr) + (cls, ), attr) return cls + return wrapper @alternate_dbus_interfaces({"se.recompile.Mandos": - "se.bsnet.fukt.Mandos"}) + "se.bsnet.fukt.Mandos"}) class ClientDBus(Client, DBusObjectWithProperties): """A Client class using D-Bus @@ -1224,7 +1400,7 @@ """ runtime_expansions = (Client.runtime_expansions - + ("dbus_object_path",)) + + ("dbus_object_path", )) _interface = "se.recompile.Mandos.Client" @@ -1238,14 +1414,15 @@ client_object_name = str(self.name).translate( {ord("."): ord("_"), ord("-"): ord("_")}) - self.dbus_object_path = (dbus.ObjectPath - ("/clients/" + client_object_name)) + self.dbus_object_path = dbus.ObjectPath( + "/clients/" + client_object_name) DBusObjectWithProperties.__init__(self, self.bus, self.dbus_object_path) - def notifychangeproperty(transform_func, - dbus_name, type_func=lambda x: x, - variant_level=1, invalidate_only=False, + def notifychangeproperty(transform_func, dbus_name, + type_func=lambda x: x, + variant_level=1, + invalidate_only=False, _interface=_interface): """ Modify a variable so that it's a property which announces its changes to DBus. @@ -1258,26 +1435,27 @@ variant_level: D-Bus variant level. Default: 1 """ attrname = "_{}".format(dbus_name) + def setter(self, value): if hasattr(self, "dbus_object_path"): if (not hasattr(self, attrname) or type_func(getattr(self, attrname, None)) != type_func(value)): if invalidate_only: - self.PropertiesChanged(_interface, - dbus.Dictionary(), - dbus.Array - ((dbus_name,))) + self.PropertiesChanged( + _interface, dbus.Dictionary(), + dbus.Array((dbus_name, ))) else: - dbus_value = transform_func(type_func(value), - variant_level - =variant_level) + dbus_value = transform_func( + type_func(value), + variant_level = variant_level) self.PropertyChanged(dbus.String(dbus_name), dbus_value) - self.PropertiesChanged(_interface, - dbus.Dictionary({ - dbus.String(dbus_name): - dbus_value }), dbus.Array()) + self.PropertiesChanged( + _interface, + dbus.Dictionary({ dbus.String(dbus_name): + dbus_value }), + dbus.Array()) setattr(self, attrname, value) return property(lambda self: getattr(self, attrname), setter) @@ -1289,9 +1467,9 @@ enabled = notifychangeproperty(dbus.Boolean, "Enabled") last_enabled = notifychangeproperty(datetime_to_dbus, "LastEnabled") - checker = notifychangeproperty(dbus.Boolean, "CheckerRunning", - type_func = lambda checker: - checker is not None) + checker = notifychangeproperty( + dbus.Boolean, "CheckerRunning", + type_func = lambda checker: checker is not None) last_checked_ok = notifychangeproperty(datetime_to_dbus, "LastCheckedOK") last_checker_status = notifychangeproperty(dbus.Int16, @@ -1300,26 +1478,22 @@ datetime_to_dbus, "LastApprovalRequest") approved_by_default = notifychangeproperty(dbus.Boolean, "ApprovedByDefault") - approval_delay = notifychangeproperty(dbus.UInt64, - "ApprovalDelay", - type_func = - lambda td: td.total_seconds() - * 1000) + approval_delay = notifychangeproperty( + dbus.UInt64, "ApprovalDelay", + type_func = lambda td: td.total_seconds() * 1000) approval_duration = notifychangeproperty( dbus.UInt64, "ApprovalDuration", type_func = lambda td: td.total_seconds() * 1000) host = notifychangeproperty(dbus.String, "Host") - timeout = notifychangeproperty(dbus.UInt64, "Timeout", - type_func = lambda td: - td.total_seconds() * 1000) + timeout = notifychangeproperty( + dbus.UInt64, "Timeout", + type_func = lambda td: td.total_seconds() * 1000) extended_timeout = notifychangeproperty( dbus.UInt64, "ExtendedTimeout", type_func = lambda td: td.total_seconds() * 1000) - interval = notifychangeproperty(dbus.UInt64, - "Interval", - type_func = - lambda td: td.total_seconds() - * 1000) + interval = notifychangeproperty( + dbus.UInt64, "Interval", + type_func = lambda td: td.total_seconds() * 1000) checker_command = notifychangeproperty(dbus.String, "Checker") secret = notifychangeproperty(dbus.ByteArray, "Secret", invalidate_only=True) @@ -1335,24 +1509,27 @@ DBusObjectWithProperties.__del__(self, *args, **kwargs) Client.__del__(self, *args, **kwargs) - def checker_callback(self, pid, condition, command, - *args, **kwargs): - self.checker_callback_tag = None - self.checker = None - if os.WIFEXITED(condition): - exitstatus = os.WEXITSTATUS(condition) + def checker_callback(self, source, condition, + connection, command, *args, **kwargs): + ret = Client.checker_callback(self, source, condition, + connection, command, *args, + **kwargs) + exitstatus = self.last_checker_status + if exitstatus >= 0: # Emit D-Bus signal self.CheckerCompleted(dbus.Int16(exitstatus), - dbus.Int64(condition), + # This is specific to GNU libC + dbus.Int64(exitstatus << 8), dbus.String(command)) else: # Emit D-Bus signal self.CheckerCompleted(dbus.Int16(-1), - dbus.Int64(condition), + dbus.Int64( + # This is specific to GNU libC + (exitstatus << 8) + | self.last_checker_signal), dbus.String(command)) - - return Client.checker_callback(self, pid, condition, command, - *args, **kwargs) + return ret def start_checker(self, *args, **kwargs): old_checker_pid = getattr(self.checker, "pid", None) @@ -1433,24 +1610,28 @@ self.checked_ok() # Enable - method + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"}) @dbus.service.method(_interface) def Enable(self): "D-Bus method" self.enable() # StartChecker - method + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"}) @dbus.service.method(_interface) def StartChecker(self): "D-Bus method" self.start_checker() # Disable - method + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"}) @dbus.service.method(_interface) def Disable(self): "D-Bus method" self.disable() # StopChecker - method + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"}) @dbus.service.method(_interface) def StopChecker(self): self.stop_checker() @@ -1463,7 +1644,8 @@ return dbus.Boolean(bool(self.approvals_pending)) # ApprovedByDefault - property - @dbus_service_property(_interface, signature="b", + @dbus_service_property(_interface, + signature="b", access="readwrite") def ApprovedByDefault_dbus_property(self, value=None): if value is None: # get @@ -1471,7 +1653,8 @@ self.approved_by_default = bool(value) # ApprovalDelay - property - @dbus_service_property(_interface, signature="t", + @dbus_service_property(_interface, + signature="t", access="readwrite") def ApprovalDelay_dbus_property(self, value=None): if value is None: # get @@ -1480,7 +1663,8 @@ self.approval_delay = datetime.timedelta(0, 0, 0, value) # ApprovalDuration - property - @dbus_service_property(_interface, signature="t", + @dbus_service_property(_interface, + signature="t", access="readwrite") def ApprovalDuration_dbus_property(self, value=None): if value is None: # get @@ -1489,17 +1673,22 @@ self.approval_duration = datetime.timedelta(0, 0, 0, value) # Name - property + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"}) @dbus_service_property(_interface, signature="s", access="read") def Name_dbus_property(self): return dbus.String(self.name) # Fingerprint - property + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"}) @dbus_service_property(_interface, signature="s", access="read") def Fingerprint_dbus_property(self): return dbus.String(self.fingerprint) # Host - property - @dbus_service_property(_interface, signature="s", + @dbus_service_property(_interface, + signature="s", access="readwrite") def Host_dbus_property(self, value=None): if value is None: # get @@ -1507,6 +1696,8 @@ self.host = str(value) # Created - property + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"}) @dbus_service_property(_interface, signature="s", access="read") def Created_dbus_property(self): return datetime_to_dbus(self.created) @@ -1517,7 +1708,8 @@ return datetime_to_dbus(self.last_enabled) # Enabled - property - @dbus_service_property(_interface, signature="b", + @dbus_service_property(_interface, + signature="b", access="readwrite") def Enabled_dbus_property(self, value=None): if value is None: # get @@ -1528,7 +1720,8 @@ self.disable() # LastCheckedOK - property - @dbus_service_property(_interface, signature="s", + @dbus_service_property(_interface, + signature="s", access="readwrite") def LastCheckedOK_dbus_property(self, value=None): if value is not None: @@ -1537,8 +1730,7 @@ return datetime_to_dbus(self.last_checked_ok) # LastCheckerStatus - property - @dbus_service_property(_interface, signature="n", - access="read") + @dbus_service_property(_interface, signature="n", access="read") def LastCheckerStatus_dbus_property(self): return dbus.Int16(self.last_checker_status) @@ -1553,7 +1745,8 @@ return datetime_to_dbus(self.last_approval_request) # Timeout - property - @dbus_service_property(_interface, signature="t", + @dbus_service_property(_interface, + signature="t", access="readwrite") def Timeout_dbus_property(self, value=None): if value is None: # get @@ -1572,13 +1765,13 @@ is None): return gobject.source_remove(self.disable_initiator_tag) - self.disable_initiator_tag = ( - gobject.timeout_add( - int((self.expires - now).total_seconds() - * 1000), self.disable)) + self.disable_initiator_tag = gobject.timeout_add( + int((self.expires - now).total_seconds() * 1000), + self.disable) # ExtendedTimeout - property - @dbus_service_property(_interface, signature="t", + @dbus_service_property(_interface, + signature="t", access="readwrite") def ExtendedTimeout_dbus_property(self, value=None): if value is None: # get @@ -1587,7 +1780,8 @@ self.extended_timeout = datetime.timedelta(0, 0, 0, value) # Interval - property - @dbus_service_property(_interface, signature="t", + @dbus_service_property(_interface, + signature="t", access="readwrite") def Interval_dbus_property(self, value=None): if value is None: # get @@ -1598,12 +1792,13 @@ if self.enabled: # Reschedule checker run gobject.source_remove(self.checker_initiator_tag) - self.checker_initiator_tag = (gobject.timeout_add - (value, self.start_checker)) - self.start_checker() # Start one now, too + self.checker_initiator_tag = gobject.timeout_add( + value, self.start_checker) + self.start_checker() # Start one now, too # Checker - property - @dbus_service_property(_interface, signature="s", + @dbus_service_property(_interface, + signature="s", access="readwrite") def Checker_dbus_property(self, value=None): if value is None: # get @@ -1611,7 +1806,8 @@ self.checker_command = str(value) # CheckerRunning - property - @dbus_service_property(_interface, signature="b", + @dbus_service_property(_interface, + signature="b", access="readwrite") def CheckerRunning_dbus_property(self, value=None): if value is None: # get @@ -1622,13 +1818,21 @@ self.stop_checker() # ObjectPath - property + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const", + "org.freedesktop.DBus.Deprecated": "true"}) @dbus_service_property(_interface, signature="o", access="read") def ObjectPath_dbus_property(self): return self.dbus_object_path # is already a dbus.ObjectPath # Secret = property - @dbus_service_property(_interface, signature="ay", - access="write", byte_arrays=True) + @dbus_annotations( + {"org.freedesktop.DBus.Property.EmitsChangedSignal": + "invalidates"}) + @dbus_service_property(_interface, + signature="ay", + access="write", + byte_arrays=True) def Secret_dbus_property(self, value): self.secret = bytes(value) @@ -1640,7 +1844,7 @@ self._pipe = child_pipe self._pipe.send(('init', fpr, address)) if not self._pipe.recv(): - raise KeyError() + raise KeyError(fpr) def __getattribute__(self, name): if name == '_pipe': @@ -1650,9 +1854,11 @@ if data[0] == 'data': return data[1] if data[0] == 'function': + def func(*args, **kwargs): self._pipe.send(('funcall', name, args, kwargs)) return self._pipe.recv()[1] + return func def __setattr__(self, name, value): @@ -1674,10 +1880,8 @@ logger.debug("Pipe FD: %d", self.server.child_pipe.fileno()) - session = (gnutls.connection - .ClientSession(self.request, - gnutls.connection - .X509Credentials())) + session = gnutls.connection.ClientSession( + self.request, gnutls.connection .X509Credentials()) # Note: gnutls.connection.X509Credentials is really a # generic GnuTLS certificate credentials object so long as @@ -1692,9 +1896,8 @@ priority = self.server.gnutls_priority if priority is None: priority = "NORMAL" - (gnutls.library.functions - .gnutls_priority_set_direct(session._c_object, - priority, None)) + gnutls.library.functions.gnutls_priority_set_direct( + session._c_object, priority, None) # Start communication using the Mandos protocol # Get protocol number @@ -1720,8 +1923,8 @@ approval_required = False try: try: - fpr = self.fingerprint(self.peer_certificate - (session)) + fpr = self.fingerprint( + self.peer_certificate(session)) except (TypeError, gnutls.errors.GNUTLSError) as error: logger.warning("Bad certificate: %s", error) @@ -1742,7 +1945,7 @@ while True: if not client.enabled: logger.info("Client %s is disabled", - client.name) + client.name) if self.server.use_dbus: # Emit D-Bus signal client.Rejected("Disabled") @@ -1795,9 +1998,9 @@ logger.warning("gnutls send failed", exc_info=error) return - logger.debug("Sent: %d, remaining: %d", - sent, len(client.secret) - - (sent_size + sent)) + logger.debug("Sent: %d, remaining: %d", sent, + len(client.secret) - (sent_size + + sent)) sent_size += sent logger.info("Sending secret to %s", client.name) @@ -1820,8 +2023,8 @@ def peer_certificate(session): "Return the peer's OpenPGP certificate as a bytestring" # If not an OpenPGP certificate... - if (gnutls.library.functions - .gnutls_certificate_type_get(session._c_object) + if (gnutls.library.functions.gnutls_certificate_type_get( + session._c_object) != gnutls.library.constants.GNUTLS_CRT_OPENPGP): # ...do the normal thing return session.peer_certificate @@ -1841,36 +2044,32 @@ def fingerprint(openpgp): "Convert an OpenPGP bytestring to a hexdigit fingerprint" # New GnuTLS "datum" with the OpenPGP public key - datum = (gnutls.library.types - .gnutls_datum_t(ctypes.cast(ctypes.c_char_p(openpgp), - ctypes.POINTER - (ctypes.c_ubyte)), - ctypes.c_uint(len(openpgp)))) + datum = gnutls.library.types.gnutls_datum_t( + ctypes.cast(ctypes.c_char_p(openpgp), + ctypes.POINTER(ctypes.c_ubyte)), + ctypes.c_uint(len(openpgp))) # New empty GnuTLS certificate crt = gnutls.library.types.gnutls_openpgp_crt_t() - (gnutls.library.functions - .gnutls_openpgp_crt_init(ctypes.byref(crt))) + gnutls.library.functions.gnutls_openpgp_crt_init( + ctypes.byref(crt)) # Import the OpenPGP public key into the certificate - (gnutls.library.functions - .gnutls_openpgp_crt_import(crt, ctypes.byref(datum), - gnutls.library.constants - .GNUTLS_OPENPGP_FMT_RAW)) + gnutls.library.functions.gnutls_openpgp_crt_import( + crt, ctypes.byref(datum), + gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW) # Verify the self signature in the key crtverify = ctypes.c_uint() - (gnutls.library.functions - .gnutls_openpgp_crt_verify_self(crt, 0, - ctypes.byref(crtverify))) + gnutls.library.functions.gnutls_openpgp_crt_verify_self( + crt, 0, ctypes.byref(crtverify)) if crtverify.value != 0: gnutls.library.functions.gnutls_openpgp_crt_deinit(crt) - raise (gnutls.errors.CertificateSecurityError - ("Verify failed")) + raise gnutls.errors.CertificateSecurityError( + "Verify failed") # New buffer for the fingerprint buf = ctypes.create_string_buffer(20) buf_len = ctypes.c_size_t() # Get the fingerprint from the certificate into the buffer - (gnutls.library.functions - .gnutls_openpgp_crt_get_fingerprint(crt, ctypes.byref(buf), - ctypes.byref(buf_len))) + gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint( + crt, ctypes.byref(buf), ctypes.byref(buf_len)) # Deinit the certificate gnutls.library.functions.gnutls_openpgp_crt_deinit(crt) # Convert the buffer to a Python bytestring @@ -1882,6 +2081,7 @@ class MultiprocessingMixIn(object): """Like socketserver.ThreadingMixIn, but with multiprocessing""" + def sub_process_main(self, request, address): try: self.finish_request(request, address) @@ -1899,6 +2099,7 @@ class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object): """ adds a pipe to the MixIn """ + def process_request(self, request, client_address): """Overrides and wraps the original process_request(). @@ -1925,8 +2126,11 @@ interface: None or a network interface name (string) use_ipv6: Boolean; to use IPv6 or not """ + def __init__(self, server_address, RequestHandlerClass, - interface=None, use_ipv6=True, socketfd=None): + interface=None, + use_ipv6=True, + socketfd=None): """If socketfd is set, use that file descriptor instead of creating a new one with socket.socket(). """ @@ -1973,10 +2177,9 @@ self.interface) else: try: - self.socket.setsockopt(socket.SOL_SOCKET, - SO_BINDTODEVICE, - (self.interface + "\0") - .encode("utf-8")) + self.socket.setsockopt( + socket.SOL_SOCKET, SO_BINDTODEVICE, + (self.interface + "\0").encode("utf-8")) except socket.error as error: if error.errno == errno.EPERM: logger.error("No permission to bind to" @@ -2000,8 +2203,7 @@ self.server_address = (any_address, self.server_address[1]) elif not self.server_address[1]: - self.server_address = (self.server_address[0], - 0) + self.server_address = (self.server_address[0], 0) # if self.interface: # self.server_address = (self.server_address[0], # 0, # port @@ -2021,9 +2223,14 @@ Assumes a gobject.MainLoop event loop. """ + def __init__(self, server_address, RequestHandlerClass, - interface=None, use_ipv6=True, clients=None, - gnutls_priority=None, use_dbus=True, socketfd=None): + interface=None, + use_ipv6=True, + clients=None, + gnutls_priority=None, + use_dbus=True, + socketfd=None): self.enabled = False self.clients = clients if self.clients is None: @@ -2035,6 +2242,7 @@ interface = interface, use_ipv6 = use_ipv6, socketfd = socketfd) + def server_activate(self): if self.enabled: return socketserver.TCPServer.server_activate(self) @@ -2044,15 +2252,17 @@ def add_pipe(self, parent_pipe, proc): # Call "handle_ipc" for both data and EOF events - gobject.io_add_watch(parent_pipe.fileno(), - gobject.IO_IN | gobject.IO_HUP, - functools.partial(self.handle_ipc, - parent_pipe = - parent_pipe, - proc = proc)) + gobject.io_add_watch( + parent_pipe.fileno(), + gobject.IO_IN | gobject.IO_HUP, + functools.partial(self.handle_ipc, + parent_pipe = parent_pipe, + proc = proc)) - def handle_ipc(self, source, condition, parent_pipe=None, - proc = None, client_object=None): + def handle_ipc(self, source, condition, + parent_pipe=None, + proc = None, + client_object=None): # error, or the other end of multiprocessing.Pipe has closed if condition & (gobject.IO_ERR | gobject.IO_HUP): # Wait for other process to exit @@ -2081,14 +2291,13 @@ parent_pipe.send(False) return False - gobject.io_add_watch(parent_pipe.fileno(), - gobject.IO_IN | gobject.IO_HUP, - functools.partial(self.handle_ipc, - parent_pipe = - parent_pipe, - proc = proc, - client_object = - client)) + gobject.io_add_watch( + parent_pipe.fileno(), + gobject.IO_IN | gobject.IO_HUP, + functools.partial(self.handle_ipc, + parent_pipe = parent_pipe, + proc = proc, + client_object = client)) parent_pipe.send(True) # remove the old hook in favor of the new above hook on # same fileno @@ -2100,15 +2309,16 @@ parent_pipe.send(('data', getattr(client_object, funcname)(*args, - **kwargs))) + **kwargs))) if command == 'getattr': attrname = request[1] - if callable(client_object.__getattribute__(attrname)): - parent_pipe.send(('function',)) + if isinstance(client_object.__getattribute__(attrname), + collections.Callable): + parent_pipe.send(('function', )) else: - parent_pipe.send(('data', client_object - .__getattribute__(attrname))) + parent_pipe.send(( + 'data', client_object.__getattribute__(attrname))) if command == 'setattr': attrname = request[1] @@ -2145,21 +2355,17 @@ # avoid excessive use of external libraries. # New type for defining tokens, syntax, and semantics all-in-one - Token = collections.namedtuple("Token", - ("regexp", # To match token; if - # "value" is not None, - # must have a "group" - # containing digits - "value", # datetime.timedelta or - # None - "followers")) # Tokens valid after - # this token + Token = collections.namedtuple("Token", ( + "regexp", # To match token; if "value" is not None, must have + # a "group" containing digits + "value", # datetime.timedelta or None + "followers")) # Tokens valid after this token # RFC 3339 "duration" tokens, syntax, and semantics; taken from # the "duration" ABNF definition in RFC 3339, Appendix A. token_end = Token(re.compile(r"$"), None, frozenset()) token_second = Token(re.compile(r"(\d+)S"), datetime.timedelta(seconds=1), - frozenset((token_end,))) + frozenset((token_end, ))) token_minute = Token(re.compile(r"(\d+)M"), datetime.timedelta(minutes=1), frozenset((token_second, token_end))) @@ -2181,7 +2387,7 @@ frozenset((token_month, token_end))) token_week = Token(re.compile(r"(\d+)W"), datetime.timedelta(weeks=1), - frozenset((token_end,))) + frozenset((token_end, ))) token_duration = Token(re.compile(r"P"), None, frozenset((token_year, token_month, token_day, token_time, @@ -2189,7 +2395,7 @@ # Define starting values value = datetime.timedelta() # Value so far found_token = None - followers = frozenset((token_duration,)) # Following valid tokens + followers = frozenset((token_duration, )) # Following valid tokens s = duration # String left to parse # Loop until end token is found while found_token is not token_end: @@ -2212,7 +2418,8 @@ break else: # No currently valid tokens were found - raise ValueError("Invalid RFC 3339 duration") + raise ValueError("Invalid RFC 3339 duration: {!r}" + .format(duration)) # End token found return value @@ -2255,8 +2462,7 @@ elif suffix == "w": delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) else: - raise ValueError("Unknown suffix {!r}" - .format(suffix)) + raise ValueError("Unknown suffix {!r}".format(suffix)) except IndexError as e: raise ValueError(*(e.args)) timevalue += delta @@ -2278,7 +2484,8 @@ # Close all standard open file descriptors null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR) if not stat.S_ISCHR(os.fstat(null).st_mode): - raise OSError(errno.ENODEV, "{} not a character device" + raise OSError(errno.ENODEV, + "{} not a character device" .format(os.devnull)) os.dup2(null, sys.stdin.fileno()) os.dup2(null, sys.stdout.fileno()) @@ -2350,7 +2557,8 @@ "port": "", "debug": "False", "priority": - "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160", + "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA" + ":+SIGN-DSA-SHA256", "servicename": "Mandos", "use_dbus": "True", "use_ipv6": "True", @@ -2360,13 +2568,12 @@ "statedir": "/var/lib/mandos", "foreground": "False", "zeroconf": "True", - } + } # Parse config file for server-global settings server_config = configparser.SafeConfigParser(server_defaults) del server_defaults - server_config.read(os.path.join(options.configdir, - "mandos.conf")) + server_config.read(os.path.join(options.configdir, "mandos.conf")) # Convert the SafeConfigParser object to a dict server_settings = server_config.defaults() # Use the appropriate methods on the non-string config options @@ -2390,9 +2597,9 @@ # Override the settings from the config file with command line # options, if set. for option in ("interface", "address", "port", "debug", - "priority", "servicename", "configdir", - "use_dbus", "use_ipv6", "debuglevel", "restore", - "statedir", "socket", "foreground", "zeroconf"): + "priority", "servicename", "configdir", "use_dbus", + "use_ipv6", "debuglevel", "restore", "statedir", + "socket", "foreground", "zeroconf"): value = getattr(options, option) if value is not None: server_settings[option] = value @@ -2413,11 +2620,10 @@ ################################################################## - if (not server_settings["zeroconf"] and - not (server_settings["port"] - or server_settings["socket"] != "")): - parser.error("Needs port or socket to work without" - " Zeroconf") + if (not server_settings["zeroconf"] + and not (server_settings["port"] + or server_settings["socket"] != "")): + parser.error("Needs port or socket to work without Zeroconf") # For convenience debug = server_settings["debug"] @@ -2439,11 +2645,10 @@ initlogger(debug, level) if server_settings["servicename"] != "Mandos": - syslogger.setFormatter(logging.Formatter - ('Mandos ({}) [%(process)d]:' - ' %(levelname)s: %(message)s' - .format(server_settings - ["servicename"]))) + syslogger.setFormatter( + logging.Formatter('Mandos ({}) [%(process)d]:' + ' %(levelname)s: %(message)s'.format( + server_settings["servicename"]))) # Parse config file with clients client_config = configparser.SafeConfigParser(Client @@ -2457,23 +2662,21 @@ socketfd = None if server_settings["socket"] != "": socketfd = server_settings["socket"] - tcp_server = MandosServer((server_settings["address"], - server_settings["port"]), - ClientHandler, - interface=(server_settings["interface"] - or None), - use_ipv6=use_ipv6, - gnutls_priority= - server_settings["priority"], - use_dbus=use_dbus, - socketfd=socketfd) + tcp_server = MandosServer( + (server_settings["address"], server_settings["port"]), + ClientHandler, + interface=(server_settings["interface"] or None), + use_ipv6=use_ipv6, + gnutls_priority=server_settings["priority"], + use_dbus=use_dbus, + socketfd=socketfd) if not foreground: pidfilename = "/run/mandos.pid" if not os.path.isdir("/run/."): pidfilename = "/var/run/mandos.pid" pidfile = None try: - pidfile = open(pidfilename, "w") + pidfile = codecs.open(pidfilename, "w", encoding="utf-8") except IOError as e: logger.error("Could not open file %r", pidfilename, exc_info=e) @@ -2506,8 +2709,8 @@ def debug_gnutls(level, string): logger.debug("GnuTLS: %s", string[:-1]) - (gnutls.library.functions - .gnutls_global_set_log_function(debug_gnutls)) + gnutls.library.functions.gnutls_global_set_log_function( + debug_gnutls) # Redirect stdin so all checkers get /dev/null null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR) @@ -2533,25 +2736,26 @@ if use_dbus: try: bus_name = dbus.service.BusName("se.recompile.Mandos", - bus, do_not_queue=True) - old_bus_name = (dbus.service.BusName - ("se.bsnet.fukt.Mandos", bus, - do_not_queue=True)) - except dbus.exceptions.NameExistsException as e: + bus, + do_not_queue=True) + old_bus_name = dbus.service.BusName( + "se.bsnet.fukt.Mandos", bus, + do_not_queue=True) + except dbus.exceptions.DBusException as e: logger.error("Disabling D-Bus:", exc_info=e) use_dbus = False server_settings["use_dbus"] = False tcp_server.use_dbus = False if zeroconf: protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET - service = AvahiServiceToSyslog(name = - server_settings["servicename"], - servicetype = "_mandos._tcp", - protocol = protocol, bus = bus) + service = AvahiServiceToSyslog( + name = server_settings["servicename"], + servicetype = "_mandos._tcp", + protocol = protocol, + bus = bus) if server_settings["interface"]: - service.interface = (if_nametoindex - (server_settings["interface"] - .encode("utf-8"))) + service.interface = if_nametoindex( + server_settings["interface"].encode("utf-8")) global multiprocessing_manager multiprocessing_manager = multiprocessing.Manager() @@ -2576,20 +2780,21 @@ if server_settings["restore"]: try: with open(stored_state_path, "rb") as stored_state: - clients_data, old_client_settings = (pickle.load - (stored_state)) + clients_data, old_client_settings = pickle.load( + stored_state) os.remove(stored_state_path) except IOError as e: if e.errno == errno.ENOENT: - logger.warning("Could not load persistent state: {}" - .format(os.strerror(e.errno))) + logger.warning("Could not load persistent state:" + " {}".format(os.strerror(e.errno))) else: logger.critical("Could not load persistent state:", exc_info=e) raise except EOFError as e: logger.warning("Could not load persistent state: " - "EOFError:", exc_info=e) + "EOFError:", + exc_info=e) with PGPEngine() as pgp: for client_name, client in clients_data.items(): @@ -2607,15 +2812,15 @@ # For each value in new config, check if it # differs from the old config value (Except for # the "secret" attribute) - if (name != "secret" and - value != old_client_settings[client_name] - [name]): + if (name != "secret" + and (value != + old_client_settings[client_name][name])): client[name] = value except KeyError: pass # Clients who has passed its expire date can still be - # enabled if its last checker was successful. Clients + # enabled if its last checker was successful. A Client # whose checker succeeded before we stored its state is # assumed to have successfully run all checkers during # downtime. @@ -2624,34 +2829,34 @@ if not client["last_checked_ok"]: logger.warning( "disabling client {} - Client never " - "performed a successful checker" - .format(client_name)) + "performed a successful checker".format( + client_name)) client["enabled"] = False elif client["last_checker_status"] != 0: logger.warning( "disabling client {} - Client last" - " checker failed with error code {}" - .format(client_name, - client["last_checker_status"])) + " checker failed with error code" + " {}".format( + client_name, + client["last_checker_status"])) client["enabled"] = False else: - client["expires"] = (datetime.datetime - .utcnow() - + client["timeout"]) + client["expires"] = ( + datetime.datetime.utcnow() + + client["timeout"]) logger.debug("Last checker succeeded," - " keeping {} enabled" - .format(client_name)) + " keeping {} enabled".format( + client_name)) try: - client["secret"] = ( - pgp.decrypt(client["encrypted_secret"], - client_settings[client_name] - ["secret"])) + client["secret"] = pgp.decrypt( + client["encrypted_secret"], + client_settings[client_name]["secret"]) except PGPError: # If decryption fails, we use secret from new settings - logger.debug("Failed to decrypt {} old secret" - .format(client_name)) - client["secret"] = ( - client_settings[client_name]["secret"]) + logger.debug("Failed to decrypt {} old secret".format( + client_name)) + client["secret"] = (client_settings[client_name] + ["secret"]) # Add/remove clients based on new changes made to config for client_name in (set(old_client_settings) @@ -2664,7 +2869,8 @@ # Create all client objects for client_name, client in clients_data.items(): tcp_server.clients[client_name] = client_class( - name = client_name, settings = client, + name = client_name, + settings = client, server_settings = server_settings) if not tcp_server.clients: @@ -2672,10 +2878,10 @@ if not foreground: if pidfile is not None: + pid = os.getpid() try: with pidfile: - pid = os.getpid() - pidfile.write("{}\n".format(pid).encode("utf-8")) + print(pid, file=pidfile) except IOError: logger.error("Could not write to file %r with PID %d", pidfilename, pid) @@ -2686,20 +2892,17 @@ signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit()) if use_dbus: - @alternate_dbus_interfaces({"se.recompile.Mandos": - "se.bsnet.fukt.Mandos"}) - class MandosDBusService(DBusObjectWithProperties): + + @alternate_dbus_interfaces( + { "se.recompile.Mandos": "se.bsnet.fukt.Mandos" }) + class MandosDBusService(DBusObjectWithObjectManager): """A D-Bus proxy object""" + def __init__(self): dbus.service.Object.__init__(self, bus, "/") + _interface = "se.recompile.Mandos" - @dbus_interface_annotations(_interface) - def _foo(self): - return { "org.freedesktop.DBus.Property" - ".EmitsChangedSignal": - "false"} - @dbus.service.signal(_interface, signature="o") def ClientAdded(self, objpath): "D-Bus signal" @@ -2710,24 +2913,30 @@ "D-Bus signal" pass + @dbus_annotations({"org.freedesktop.DBus.Deprecated": + "true"}) @dbus.service.signal(_interface, signature="os") def ClientRemoved(self, objpath, name): "D-Bus signal" pass + @dbus_annotations({"org.freedesktop.DBus.Deprecated": + "true"}) @dbus.service.method(_interface, out_signature="ao") def GetAllClients(self): "D-Bus method" - return dbus.Array(c.dbus_object_path - for c in + return dbus.Array(c.dbus_object_path for c in tcp_server.clients.itervalues()) + @dbus_annotations({"org.freedesktop.DBus.Deprecated": + "true"}) @dbus.service.method(_interface, out_signature="a{oa{sv}}") def GetAllClientsWithProperties(self): "D-Bus method" return dbus.Dictionary( - { c.dbus_object_path: c.GetAll("") + { c.dbus_object_path: c.GetAll( + "se.recompile.Mandos.Client") for c in tcp_server.clients.itervalues() }, signature="oa{sv}") @@ -2738,14 +2947,50 @@ if c.dbus_object_path == object_path: del tcp_server.clients[c.name] c.remove_from_connection() - # Don't signal anything except ClientRemoved + # Don't signal the disabling c.disable(quiet=True) - # Emit D-Bus signal - self.ClientRemoved(object_path, c.name) + # Emit D-Bus signal for removal + self.client_removed_signal(c) return raise KeyError(object_path) del _interface + + @dbus.service.method(dbus.OBJECT_MANAGER_IFACE, + out_signature = "a{oa{sa{sv}}}") + def GetManagedObjects(self): + """D-Bus method""" + return dbus.Dictionary( + { client.dbus_object_path: + dbus.Dictionary( + { interface: client.GetAll(interface) + for interface in + client._get_all_interface_names()}) + for client in tcp_server.clients.values()}) + + def client_added_signal(self, client): + """Send the new standard signal and the old signal""" + if use_dbus: + # New standard signal + self.InterfacesAdded( + client.dbus_object_path, + dbus.Dictionary( + { interface: client.GetAll(interface) + for interface in + client._get_all_interface_names()})) + # Old signal + self.ClientAdded(client.dbus_object_path) + + def client_removed_signal(self, client): + """Send the new standard signal and the old signal""" + if use_dbus: + # New standard signal + self.InterfacesRemoved( + client.dbus_object_path, + client._get_all_interface_names()) + # Old signal + self.ClientRemoved(client.dbus_object_path, + client.name) mandos_dbus_service = MandosDBusService() @@ -2774,8 +3019,8 @@ # + secret. exclude = { "bus", "changedstate", "secret", "checker", "server_settings" } - for name, typ in (inspect.getmembers - (dbus.service.Object)): + for name, typ in inspect.getmembers(dbus.service + .Object): exclude.add(name) client_dict["encrypted_secret"] = (client @@ -2788,12 +3033,14 @@ del client_settings[client.name]["secret"] try: - with (tempfile.NamedTemporaryFile - (mode='wb', suffix=".pickle", prefix='clients-', - dir=os.path.dirname(stored_state_path), - delete=False)) as stored_state: + with tempfile.NamedTemporaryFile( + mode='wb', + suffix=".pickle", + prefix='clients-', + dir=os.path.dirname(stored_state_path), + delete=False) as stored_state: pickle.dump((clients, client_settings), stored_state) - tempname=stored_state.name + tempname = stored_state.name os.rename(tempname, stored_state_path) except (IOError, OSError) as e: if not debug: @@ -2814,21 +3061,18 @@ name, client = tcp_server.clients.popitem() if use_dbus: client.remove_from_connection() - # Don't signal anything except ClientRemoved + # Don't signal the disabling client.disable(quiet=True) - if use_dbus: - # Emit D-Bus signal - mandos_dbus_service.ClientRemoved(client - .dbus_object_path, - client.name) + # Emit D-Bus signal for removal + mandos_dbus_service.client_removed_signal(client) client_settings.clear() atexit.register(cleanup) for client in tcp_server.clients.itervalues(): if use_dbus: - # Emit D-Bus signal - mandos_dbus_service.ClientAdded(client.dbus_object_path) + # Emit D-Bus signal for adding + mandos_dbus_service.client_added_signal(client) # Need to initiate checking of clients if client.enabled: client.init_checker() @@ -2879,5 +3123,6 @@ # Must run before the D-Bus bus name gets deregistered cleanup() + if __name__ == '__main__': main() === modified file 'mandos-clients.conf.xml' --- mandos-clients.conf.xml 2014-06-22 02:19:30 +0000 +++ mandos-clients.conf.xml 2015-07-20 04:03:32 +0000 @@ -3,7 +3,7 @@ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ /etc/mandos/clients.conf"> - + %common; ]> @@ -37,6 +37,9 @@ 2010 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson === modified file 'mandos-ctl' --- mandos-ctl 2014-10-05 20:08:58 +0000 +++ mandos-ctl 2015-08-10 09:00:23 +0000 @@ -3,8 +3,8 @@ # # Mandos Monitor - Control and monitor the Mandos server # -# Copyright © 2008-2014 Teddy Hogeborn -# Copyright © 2008-2014 Björn Påhlsson +# Copyright © 2008-2015 Teddy Hogeborn +# Copyright © 2008-2015 Björn Påhlsson # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -64,8 +64,8 @@ "ApprovalDelay": "Approval Delay", "ApprovalDuration": "Approval Duration", "Checker": "Checker", - "ExtendedTimeout" : "Extended Timeout" - } + "ExtendedTimeout": "Extended Timeout" +} defaultkeywords = ("Name", "Enabled", "Timeout", "LastCheckedOK") domain = "se.recompile" busname = domain + ".Mandos" @@ -74,14 +74,19 @@ client_interface = domain + ".Mandos.Client" version = "1.6.9" + +try: + dbus.OBJECT_MANAGER_IFACE +except AttributeError: + dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager" + def milliseconds_to_string(ms): td = datetime.timedelta(0, 0, 0, ms) - return ("{days}{hours:02}:{minutes:02}:{seconds:02}" - .format(days = "{}T".format(td.days) if td.days else "", - hours = td.seconds // 3600, - minutes = (td.seconds % 3600) // 60, - seconds = td.seconds % 60, - )) + return ("{days}{hours:02}:{minutes:02}:{seconds:02}".format( + days = "{}T".format(td.days) if td.days else "", + hours = td.seconds // 3600, + minutes = (td.seconds % 3600) // 60, + seconds = td.seconds % 60)) def rfc3339_duration_to_delta(duration): @@ -111,21 +116,17 @@ # avoid excessive use of external libraries. # New type for defining tokens, syntax, and semantics all-in-one - Token = collections.namedtuple("Token", - ("regexp", # To match token; if - # "value" is not None, - # must have a "group" - # containing digits - "value", # datetime.timedelta or - # None - "followers")) # Tokens valid after - # this token + Token = collections.namedtuple("Token", ( + "regexp", # To match token; if "value" is not None, must have + # a "group" containing digits + "value", # datetime.timedelta or None + "followers")) # Tokens valid after this token # RFC 3339 "duration" tokens, syntax, and semantics; taken from # the "duration" ABNF definition in RFC 3339, Appendix A. token_end = Token(re.compile(r"$"), None, frozenset()) token_second = Token(re.compile(r"(\d+)S"), datetime.timedelta(seconds=1), - frozenset((token_end,))) + frozenset((token_end, ))) token_minute = Token(re.compile(r"(\d+)M"), datetime.timedelta(minutes=1), frozenset((token_second, token_end))) @@ -147,7 +148,7 @@ frozenset((token_month, token_end))) token_week = Token(re.compile(r"(\d+)W"), datetime.timedelta(weeks=1), - frozenset((token_end,))) + frozenset((token_end, ))) token_duration = Token(re.compile(r"P"), None, frozenset((token_year, token_month, token_day, token_time, @@ -155,7 +156,7 @@ # Define starting values value = datetime.timedelta() # Value so far found_token = None - followers = frozenset((token_duration,)) # Following valid tokens + followers = frozenset((token_duration, )) # Following valid tokens s = duration # String left to parse # Loop until end token is found while found_token is not token_end: @@ -178,7 +179,8 @@ break else: # No currently valid tokens were found - raise ValueError("Invalid RFC 3339 duration") + raise ValueError("Invalid RFC 3339 duration: {!r}" + .format(duration)) # End token found return value @@ -186,17 +188,17 @@ def string_to_delta(interval): """Parse a string and return a datetime.timedelta - >>> string_to_delta("7d") + >>> string_to_delta('7d') datetime.timedelta(7) - >>> string_to_delta("60s") + >>> string_to_delta('60s') datetime.timedelta(0, 60) - >>> string_to_delta("60m") + >>> string_to_delta('60m') datetime.timedelta(0, 3600) - >>> string_to_delta("24h") + >>> string_to_delta('24h') datetime.timedelta(1) - >>> string_to_delta("1w") + >>> string_to_delta('1w') datetime.timedelta(7) - >>> string_to_delta("5m 30s") + >>> string_to_delta('5m 30s') datetime.timedelta(0, 330) """ @@ -223,6 +225,7 @@ value += datetime.timedelta(0, 0, 0, int(num)) return value + def print_clients(clients, keywords): def valuetostring(value, keyword): if type(value) is dbus.Boolean: @@ -234,19 +237,18 @@ # Create format string to print table rows format_string = " ".join("{{{key}:{width}}}".format( - width = max(len(tablewords[key]), - max(len(valuetostring(client[key], - key)) - for client in - clients)), - key = key) for key in keywords) + width = max(len(tablewords[key]), + max(len(valuetostring(client[key], key)) + for client in clients)), + key = key) + for key in keywords) # Print header line print(format_string.format(**tablewords)) for client in clients: - print(format_string.format(**{ key: - valuetostring(client[key], - key) - for key in keywords })) + print(format_string.format(**{ + key: valuetostring(client[key], key) + for key in keywords })) + def has_actions(options): return any((options.enable, @@ -268,6 +270,7 @@ options.approve, options.deny)) + def main(): parser = argparse.ArgumentParser() parser.add_argument("--version", action="version", @@ -338,12 +341,13 @@ bus = dbus.SystemBus() mandos_dbus_objc = bus.get_object(busname, server_path) except dbus.exceptions.DBusException: - print("Could not connect to Mandos server", - file=sys.stderr) + print("Could not connect to Mandos server", file=sys.stderr) sys.exit(1) mandos_serv = dbus.Interface(mandos_dbus_objc, dbus_interface = server_interface) + mandos_serv_object_manager = dbus.Interface( + mandos_dbus_objc, dbus_interface = dbus.OBJECT_MANAGER_IFACE) #block stderr since dbus library prints to stderr null = os.open(os.path.devnull, os.O_RDWR) @@ -352,14 +356,18 @@ os.close(null) try: try: - mandos_clients = mandos_serv.GetAllClientsWithProperties() + mandos_clients = { path: ifs_and_props[client_interface] + for path, ifs_and_props in + mandos_serv_object_manager + .GetManagedObjects().items() + if client_interface in ifs_and_props } finally: #restore stderr os.dup2(stderrcopy, sys.stderr.fileno()) os.close(stderrcopy) - except dbus.exceptions.DBusException: - print("Access denied: Accessing mandos server through dbus.", - file=sys.stderr) + except dbus.exceptions.DBusException as e: + print("Access denied: Accessing mandos server through D-Bus: {}" + .format(e), file=sys.stderr) sys.exit(1) # Compile dict of (clients: properties) to process @@ -382,11 +390,10 @@ if not has_actions(options) and clients: if options.verbose: - keywords = ("Name", "Enabled", "Timeout", - "LastCheckedOK", "Created", "Interval", - "Host", "Fingerprint", "CheckerRunning", - "LastEnabled", "ApprovalPending", - "ApprovedByDefault", + keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK", + "Created", "Interval", "Host", "Fingerprint", + "CheckerRunning", "LastEnabled", + "ApprovalPending", "ApprovedByDefault", "LastApprovalRequest", "ApprovalDelay", "ApprovalDuration", "Checker", "ExtendedTimeout") @@ -397,16 +404,19 @@ else: # Process each client in the list by all selected options for client in clients: + def set_client_prop(prop, value): """Set a Client D-Bus property""" client.Set(client_interface, prop, value, dbus_interface=dbus.PROPERTIES_IFACE) + def set_client_prop_ms(prop, value): """Set a Client D-Bus property, converted from a string to milliseconds.""" set_client_prop(prop, string_to_delta(value).total_seconds() * 1000) + if options.remove: mandos_serv.RemoveClient(client.__dbus_object_path__) if options.enable: @@ -456,5 +466,6 @@ client.Approve(dbus.Boolean(False), dbus_interface=client_interface) + if __name__ == "__main__": main() === modified file 'mandos-ctl.xml' --- mandos-ctl.xml 2012-06-22 23:33:56 +0000 +++ mandos-ctl.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -34,6 +34,9 @@ 2010 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson === modified file 'mandos-keygen' --- mandos-keygen 2014-10-05 20:08:58 +0000 +++ mandos-keygen 2015-05-23 10:41:35 +0000 @@ -2,8 +2,8 @@ # # Mandos key generator - create a new OpenPGP key for a Mandos client # -# Copyright © 2008-2014 Teddy Hogeborn -# Copyright © 2008-2014 Björn Påhlsson +# Copyright © 2008-2015 Teddy Hogeborn +# Copyright © 2008-2015 Björn Påhlsson # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -47,7 +47,7 @@ --name "$0" -- "$@"` help(){ -basename="`basename $0`" +basename="`basename "$0"`" cat <&2 + echo "Unknown arguments: '$*'" >&2 exit 1 fi @@ -285,13 +285,19 @@ esac if [ $SSH -eq 1 ]; then - set +e - ssh_fingerprint="`ssh-keyscan localhost 2>/dev/null`" - if [ $? -ne 0 ]; then - ssh_fingerprint="" - fi - set -e - ssh_fingerprint="${ssh_fingerprint#localhost }" + for ssh_keytype in ed25519 rsa; do + set +e + ssh_fingerprint="`ssh-keyscan -t $ssh_keytype localhost 2>/dev/null`" + set -e + if [ $? -ne 0 ]; then + ssh_fingerprint="" + continue + fi + if [ -n "$ssh_fingerprint" ]; then + ssh_fingerprint="${ssh_fingerprint#localhost }" + break + fi + done fi # Import key into temporary key rings @@ -304,7 +310,7 @@ # Get fingerprint of key FINGERPRINT="`gpg --quiet --batch --no-tty --no-options \ - --enable-dsa2 --homedir \"$RINGDIR\" --trust-model always \ + --enable-dsa2 --homedir "$RINGDIR" --trust-model always \ --fingerprint --with-colons \ | sed --quiet \ --expression='/^fpr:/{s/^fpr:.*:\\([0-9A-Z]*\\):\$/\\1/p;q}'`" @@ -363,7 +369,7 @@ } }' < "$SECFILE" if [ -n "$ssh_fingerprint" ]; then - echo 'checker = ssh-keyscan %%(host)s 2>/dev/null | grep --fixed-strings --line-regexp --quiet --regexp=%%(host)s" %(ssh_fingerprint)s"' + echo 'checker = ssh-keyscan -t '"$ssh_keytype"' %%(host)s 2>/dev/null | grep --fixed-strings --line-regexp --quiet --regexp=%%(host)s" %(ssh_fingerprint)s"' echo "ssh_fingerprint = ${ssh_fingerprint}" fi fi === modified file 'mandos-keygen.xml' --- mandos-keygen.xml 2014-06-22 02:19:30 +0000 +++ mandos-keygen.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,8 +33,12 @@ 2008 2009 + 2010 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson === modified file 'mandos-monitor' --- mandos-monitor 2014-10-05 20:08:58 +0000 +++ mandos-monitor 2015-08-10 09:00:23 +0000 @@ -3,8 +3,8 @@ # # Mandos Monitor - Control and monitor the Mandos server # -# Copyright © 2009-2014 Teddy Hogeborn -# Copyright © 2009-2014 Björn Påhlsson +# Copyright © 2009-2015 Teddy Hogeborn +# Copyright © 2009-2015 Björn Påhlsson # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -62,6 +62,11 @@ client_interface = domain + '.Mandos.Client' version = "1.6.9" +try: + dbus.OBJECT_MANAGER_IFACE +except AttributeError: + dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager" + def isoformat_to_datetime(iso): "Parse an ISO 8601 date string to a datetime.datetime()" if not iso: @@ -105,7 +110,8 @@ It updates the changed properties in the "properties" dict. """ # Update properties dict with new value - self.properties.update(properties) + if interface == client_interface: + self.properties.update(properties) def delete(self): self.property_changed_match.remove() @@ -191,14 +197,6 @@ ' killed by signal {}' .format(self.properties["Name"], command, os.WTERMSIG(condition))) - elif os.WCOREDUMP(condition): - self.logger('Checker for client {} (command "{}") dumped' - ' core'.format(self.properties["Name"], - command)) - else: - self.logger('Checker for client {} completed' - ' mysteriously' - .format(self.properties["Name"])) self.update() def checker_started(self, command): @@ -337,11 +335,13 @@ """Handle keys. This overrides the method from urwid.FlowWidget""" if key == "+": - self.proxy.Enable(dbus_interface = client_interface, - ignore_reply=True) + self.proxy.Set(client_interface, "Enabled", + dbus.Boolean(True), ignore_reply = True, + dbus_interface = dbus.PROPERTIES_IFACE) elif key == "-": - self.proxy.Disable(dbus_interface = client_interface, - ignore_reply=True) + self.proxy.Set(client_interface, "Enabled", False, + ignore_reply = True, + dbus_interface = dbus.PROPERTIES_IFACE) elif key == "a": self.proxy.Approve(dbus.Boolean(True, variant_level=1), dbus_interface = client_interface, @@ -355,11 +355,13 @@ .object_path, ignore_reply=True) elif key == "s": - self.proxy.StartChecker(dbus_interface = client_interface, - ignore_reply=True) + self.proxy.Set(client_interface, "CheckerRunning", + dbus.Boolean(True), ignore_reply = True, + dbus_interface = dbus.PROPERTIES_IFACE) elif key == "S": - self.proxy.StopChecker(dbus_interface = client_interface, - ignore_reply=True) + self.proxy.Set(client_interface, "CheckerRunning", + dbus.Boolean(False), ignore_reply = True, + dbus_interface = dbus.PROPERTIES_IFACE) elif key == "C": self.proxy.CheckedOK(dbus_interface = client_interface, ignore_reply=True) @@ -525,21 +527,32 @@ self.log_message("Wrap mode: {}".format(self.log_wrap), level=0) - def find_and_remove_client(self, path, name): + def find_and_remove_client(self, path, interfaces): """Find a client by its object path and remove it. - This is connected to the ClientRemoved signal from the + This is connected to the InterfacesRemoved signal from the Mandos server object.""" + if client_interface not in interfaces: + # Not a Mandos client object; ignore + return try: client = self.clients_dict[path] except KeyError: # not found? - self.log_message("Unknown client {!r} ({!r}) removed" - .format(name, path)) + self.log_message("Unknown client {!r} removed" + .format(path)) return client.delete() - def add_new_client(self, path): + def add_new_client(self, path, ifs_and_props): + """Find a client by its object path and remove it. + + This is connected to the InterfacesAdded signal from the + Mandos server object. + """ + if client_interface not in ifs_and_props: + # Not a Mandos client object; ignore + return client_proxy_object = self.bus.get_object(self.busname, path) self.add_client(MandosClientWidget(server_proxy_object =self.mandos_serv, @@ -550,7 +563,10 @@ delete_hook =self.remove_client, logger - =self.log_message), + =self.log_message, + properties + = dict(ifs_and_props[ + client_interface])), path=path) def add_client(self, client, path=None): @@ -591,14 +607,16 @@ mandos_clients = dbus.Dictionary() (self.mandos_serv - .connect_to_signal("ClientRemoved", + .connect_to_signal("InterfacesRemoved", self.find_and_remove_client, - dbus_interface=server_interface, + dbus_interface + = dbus.OBJECT_MANAGER_IFACE, byte_arrays=True)) (self.mandos_serv - .connect_to_signal("ClientAdded", + .connect_to_signal("InterfacesAdded", self.add_new_client, - dbus_interface=server_interface, + dbus_interface + = dbus.OBJECT_MANAGER_IFACE, byte_arrays=True)) (self.mandos_serv .connect_to_signal("ClientNotFound", === modified file 'mandos-monitor.xml' --- mandos-monitor.xml 2014-07-14 21:41:08 +0000 +++ mandos-monitor.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -34,6 +34,9 @@ 2010 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson === modified file 'mandos-options.xml' --- mandos-options.xml 2014-06-15 02:48:49 +0000 +++ mandos-options.xml 2015-07-20 03:03:33 +0000 @@ -46,28 +46,18 @@ not run in debug mode. - + GnuTLS priority string for the TLS handshake. The default is SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224: - +SIGN-RSA-RMD160. + >SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA + :+SIGN-DSA-SHA256. See gnutls_priority_init 3 for the syntax. Warning: changing this may make the TLS handshake fail, making server-client - communication impossible. - - - - GnuTLS priority string for the TLS handshake. - The default is SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP. See - gnutls_priority_init - 3 for the syntax. - Warning: changing this may make the - TLS handshake fail, making server-client - communication impossible. + communication impossible. Changing this option may also make the + network traffic decryptable by an attacker. === modified file 'mandos.conf' --- mandos.conf 2014-06-15 02:48:49 +0000 +++ mandos.conf 2015-07-20 03:03:33 +0000 @@ -23,7 +23,7 @@ ;debug = False # GnuTLS priority for the TLS handshake. See gnutls_priority_init(3). -;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160 +;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA:+SIGN-DSA-SHA256 # Zeroconf service name. You need to change this if you for some # reason want to run more than one server on the same *host*. === modified file 'mandos.conf.xml' --- mandos.conf.xml 2013-10-24 20:21:45 +0000 +++ mandos.conf.xml 2015-07-20 04:03:32 +0000 @@ -3,7 +3,7 @@ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ /etc/mandos/mandos.conf"> - + %common; ]> @@ -34,9 +34,12 @@ 2008 2009 + 2010 2011 2012 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson @@ -121,8 +124,7 @@ - + @@ -223,8 +225,8 @@ interface = eth0 address = fe80::aede:48ff:fe71:f6f2 port = 1025 -debug = true -priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP +debug = True +priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA servicename = Daena use_dbus = False use_ipv6 = True === modified file 'mandos.service' --- mandos.service 2014-10-05 19:39:25 +0000 +++ mandos.service 2015-08-10 16:19:28 +0000 @@ -1,20 +1,24 @@ [Unit] Description=Server of encrypted passwords to Mandos clients Documentation=man:intro(8mandos) man:mandos(8) +## If the server is configured to listen to a specific IP or network +## interface, it may be necessary to change "network.target" to +## "network-online.target". +After=network.target +## If the server is configured to not use ZeroConf, these two lines +## become unnecessary and should be removed or commented out. +After=avahi-daemon.service +RequisiteOverridable=avahi-daemon.service [Service] -Type=simple -## Type=dbus is not appropriate, because Mandos also needs to announce -## its ZeroConf service and be reachable on the network. -#Type=dbus -#BusName=se.recompile.Mandos -# If you add --no-dbus, also comment out BusName above, and vice versa +## If the server's D-Bus interface is disabled, the "BusName" setting +## should be removed or commented out. +BusName=se.recompile.Mandos ExecStart=/usr/sbin/mandos --foreground Restart=always -KillMode=process -## Using socket activation won't work either, because systemd always -## does bind() on the socket, and also won't announce the ZeroConf -## service. +KillMode=mixed +## Using socket activation won't work, because systemd always does +## bind() on the socket, and also won't announce the ZeroConf service. #ExecStart=/usr/sbin/mandos --foreground --socket=0 #StandardInput=socket === modified file 'mandos.xml' --- mandos.xml 2014-06-15 02:48:49 +0000 +++ mandos.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -37,6 +37,8 @@ 2011 2012 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson @@ -236,8 +238,7 @@ - + @@ -707,8 +708,7 @@ - GnuTLS + GnuTLS @@ -752,12 +752,12 @@ - RFC 4346: The Transport Layer Security (TLS) - Protocol Version 1.1 + RFC 5246: The Transport Layer Security (TLS) + Protocol Version 1.2 - TLS 1.1 is the protocol implemented by GnuTLS. + TLS 1.2 is the protocol implemented by GnuTLS. @@ -773,8 +773,8 @@ - RFC 5081: Using OpenPGP Keys for Transport Layer - Security + RFC 6091: Using OpenPGP Keys for Transport Layer + Security (TLS) Authentication === added directory 'plugin-helpers' === added file 'plugin-helpers/mandos-client-iprouteadddel.c' --- plugin-helpers/mandos-client-iprouteadddel.c 1970-01-01 00:00:00 +0000 +++ plugin-helpers/mandos-client-iprouteadddel.c 2015-07-05 21:38:01 +0000 @@ -0,0 +1,282 @@ +/* -*- coding: utf-8 -*- */ +/* + * iprouteadddel - Add or delete direct route to a local IP address + * + * Copyright © 2015 Teddy Hogeborn + * Copyright © 2015 Björn Påhlsson + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * . + * + * Contact the authors at . + */ + +#define _GNU_SOURCE /* asprintf(), + program_invocation_short_name */ +#include /* bool, false, true */ +#include /* fprintf(), stderr, FILE, vfprintf */ +#include /* program_invocation_short_name, + errno, perror(), EINVAL, ENOMEM */ +#include /* va_list, va_start */ +#include /* EXIT_SUCCESS */ +#include /* struct argp_option, error_t, struct + argp_state, ARGP_KEY_ARG, + argp_usage(), ARGP_KEY_END, + ARGP_ERR_UNKNOWN, struct argp, + argp_parse() */ +#include /* EX_USAGE, EX_OSERR */ +#include /* sa_family_t, AF_INET6, AF_INET */ +#include /* PRIdMAX, intmax_t */ + +#include /* struct nl_addr, nl_addr_parse(), + nl_geterror(), + nl_addr_get_family(), + nl_addr_put() */ +#include /* struct rtnl_route, + struct rtnl_nexthop, + rtnl_route_alloc(), + rtnl_route_set_family(), + rtnl_route_set_protocol(), + RTPROT_BOOT, + rtnl_route_set_scope(), + RT_SCOPE_LINK, + rtnl_route_set_type(), + RTN_UNICAST, + rtnl_route_set_dst(), + rtnl_route_set_table(), + RT_TABLE_MAIN, + rtnl_route_nh_alloc(), + rtnl_route_nh_set_ifindex(), + rtnl_route_add_nexthop(), + rtnl_route_add(), + rtnl_route_delete(), + rtnl_route_put(), + rtnl_route_nh_free() */ +#include /* struct nl_sock, nl_socket_alloc(), + nl_connect(), nl_socket_free() */ +#include /* rtnl_link_get_kernel(), + rtnl_link_get_ifindex(), + rtnl_link_put() */ + +bool debug = false; +const char *argp_program_version = "mandos-client-iprouteadddel " VERSION; +const char *argp_program_bug_address = ""; + +/* Function to use when printing errors */ +void perror_plus(const char *print_text){ + int e = errno; + fprintf(stderr, "Mandos plugin helper %s: ", + program_invocation_short_name); + errno = e; + perror(print_text); +} + +__attribute__((format (gnu_printf, 2, 3), nonnull)) +int fprintf_plus(FILE *stream, const char *format, ...){ + va_list ap; + va_start (ap, format); + + fprintf(stream, "Mandos plugin helper %s: ", + program_invocation_short_name); + return vfprintf(stream, format, ap); +} + +int main(int argc, char *argv[]){ + int ret; + int exitcode = EXIT_SUCCESS; + struct arguments { + bool add; /* true: add, false: delete */ + char *address; /* IP address as string */ + struct nl_addr *nl_addr; /* Netlink IP address */ + char *interface; /* interface name */ + } arguments = { .add = true, .address = NULL, .interface = NULL }; + struct argp_option options[] = { + { .name = "debug", .key = 128, + .doc = "Debug mode" }, + { .name = NULL } + }; + struct rtnl_route *route = NULL; + struct rtnl_nexthop *nexthop = NULL; + struct nl_sock *sk = NULL; + + error_t parse_opt(int key, char *arg, struct argp_state *state){ + int lret; + errno = 0; + switch(key){ + case 128: /* --debug */ + debug = true; + break; + case ARGP_KEY_ARG: + switch(state->arg_num){ + case 0: + if(strcasecmp(arg, "add") == 0){ + ((struct arguments *)(state->input))->add = true; + } else if(strcasecmp(arg, "delete") == 0){ + ((struct arguments *)(state->input))->add = false; + } else { + fprintf_plus(stderr, "Unrecognized command: %s\n", arg); + argp_usage(state); + } + break; + case 1: + ((struct arguments *)(state->input))->address = arg; + lret = nl_addr_parse(arg, AF_UNSPEC, &(((struct arguments *) + (state->input)) + ->nl_addr)); + if(lret != 0){ + fprintf_plus(stderr, "Failed to parse address %s: %s\n", + arg, nl_geterror(lret)); + argp_usage(state); + } + break; + case 2: + ((struct arguments *)(state->input))->interface = arg; + break; + default: + argp_usage(state); + } + break; + case ARGP_KEY_END: + if(state->arg_num < 3){ + argp_usage(state); + } + break; + default: + return ARGP_ERR_UNKNOWN; + } + return errno; + } + + struct argp argp = { .options = options, .parser = parse_opt, + .args_doc = "[ add | delete ] ADDRESS INTERFACE", + .doc = "Mandos client helper -- Add or delete" + " local route to IP address on interface" }; + + ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, 0, &arguments); + switch(ret){ + case 0: + break; + case EINVAL: + exit(EX_USAGE); + case ENOMEM: + default: + errno = ret; + perror_plus("argp_parse"); + exitcode = EX_OSERR; + goto end; + } + /* Get netlink socket */ + sk = nl_socket_alloc(); + if(sk == NULL){ + fprintf_plus(stderr, "Failed to allocate netlink socket: %s\n", + nl_geterror(ret)); + exitcode = EX_OSERR; + goto end; + } + /* Connect socket to netlink */ + ret = nl_connect(sk, NETLINK_ROUTE); + if(ret < 0){ + fprintf_plus(stderr, "Failed to connect socket to netlink: %s\n", + nl_geterror(ret)); + exitcode = EX_OSERR; + goto end; + } + /* Get link object of specified interface */ + struct rtnl_link *link = NULL; + ret = rtnl_link_get_kernel(sk, 0, arguments.interface, &link); + if(ret < 0){ + fprintf_plus(stderr, "Failed to use interface %s: %s\n", + arguments.interface, nl_geterror(ret)); + exitcode = EX_OSERR; + goto end; + } + /* Get netlink route object */ + route = rtnl_route_alloc(); + if(route == NULL){ + fprintf_plus(stderr, "Failed to get netlink route:\n"); + exitcode = EX_OSERR; + goto end; + } + /* Get address family of specified address */ + sa_family_t af = (sa_family_t)nl_addr_get_family(arguments.nl_addr); + if(debug){ + fprintf_plus(stderr, "Address family of %s is %s (%" PRIdMAX + ")\n", arguments.address, + af == AF_INET6 ? "AF_INET6" : + ( af == AF_INET ? "AF_INET" : "UNKNOWN"), + (intmax_t)af); + } + /* Set route parameters: */ + rtnl_route_set_family(route, (uint8_t)af); /* Address family */ + rtnl_route_set_protocol(route, RTPROT_BOOT); /* protocol - see + ip-route(8) */ + rtnl_route_set_scope(route, RT_SCOPE_LINK); /* link scope */ + rtnl_route_set_type(route, RTN_UNICAST); /* normal unicast + address route */ + rtnl_route_set_dst(route, arguments.nl_addr); /* Destination + address */ + rtnl_route_set_table(route, RT_TABLE_MAIN); /* "main" routing + table */ + /* Create nexthop */ + nexthop = rtnl_route_nh_alloc(); + if(nexthop == NULL){ + fprintf_plus(stderr, "Failed to get netlink route nexthop\n"); + exitcode = EX_OSERR; + goto end; + } + /* Get index number of specified interface */ + int ifindex = rtnl_link_get_ifindex(link); + if(debug){ + fprintf_plus(stderr, "ifindex of %s is %d\n", arguments.interface, + ifindex); + } + /* Set interface index number on nexthop object */ + rtnl_route_nh_set_ifindex(nexthop, ifindex); + /* Set route tu use nexthop object */ + rtnl_route_add_nexthop(route, nexthop); + /* Add or delete route? */ + if(arguments.add){ + ret = rtnl_route_add(sk, route, NLM_F_EXCL); + } else { + ret = rtnl_route_delete(sk, route, 0); + } + if(ret < 0){ + fprintf_plus(stderr, "Failed to %s route: %s\n", + arguments.add ? "add" : "delete", + nl_geterror(ret)); + exitcode = EX_OSERR; + goto end; + } + end: + /* Deallocate route */ + if(route){ + rtnl_route_put(route); + } else if(nexthop) { + /* Deallocate route nexthop */ + rtnl_route_nh_free(nexthop); + } + /* Deallocate parsed address */ + if(arguments.nl_addr){ + nl_addr_put(arguments.nl_addr); + } + /* Deallocate link struct */ + if(link){ + rtnl_link_put(link); + } + /* Deallocate netlink socket struct */ + if(sk){ + nl_socket_free(sk); + } + return exitcode; +} === modified file 'plugin-runner.c' --- plugin-runner.c 2014-07-16 01:41:23 +0000 +++ plugin-runner.c 2015-07-20 04:03:32 +0000 @@ -2,8 +2,8 @@ /* * Mandos plugin runner - Run Mandos plugins * - * Copyright © 2008-2014 Teddy Hogeborn - * Copyright © 2008-2014 Björn Påhlsson + * Copyright © 2008-2015 Teddy Hogeborn + * Copyright © 2008-2015 Björn Påhlsson * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -76,6 +76,7 @@ #define BUFFER_SIZE 256 #define PDIR "/lib/mandos/plugins.d" +#define PHDIR "/lib/mandos/plugin-helpers" #define AFILE "/conf/conf.d/mandos/plugin-runner.conf" const char *argp_program_version = "plugin-runner " VERSION; @@ -347,6 +348,7 @@ int main(int argc, char *argv[]){ char *plugindir = NULL; + char *pluginhelperdir = NULL; char *argfile = NULL; FILE *conffp; struct dirent **direntries = NULL; @@ -414,6 +416,10 @@ .doc = "Group ID the plugins will run as", .group = 3 }, { .name = "debug", .key = 132, .doc = "Debug mode", .group = 4 }, + { .name = "plugin-helper-dir", .key = 133, + .arg = "DIRECTORY", + .doc = "Specify a different plugin helper directory", + .group = 2 }, /* * These reproduce what we would get without ARGP_NO_HELP */ @@ -545,6 +551,13 @@ case 132: /* --debug */ debug = true; break; + case 133: /* --plugin-helper-dir */ + free(pluginhelperdir); + pluginhelperdir = strdup(arg); + if(pluginhelperdir != NULL){ + errno = 0; + } + break; /* * These reproduce what we would get without ARGP_NO_HELP */ @@ -601,6 +614,7 @@ case 130: /* --userid */ case 131: /* --groupid */ case 132: /* --debug */ + case 133: /* --plugin-helper-dir */ case '?': /* --help */ case -3: /* --usage */ case 'V': /* --version */ @@ -761,6 +775,24 @@ goto fallback; } + { + char *pluginhelperenv; + bool bret = true; + ret = asprintf(&pluginhelperenv, "MANDOSPLUGINHELPERDIR=%s", + pluginhelperdir != NULL ? pluginhelperdir : PHDIR); + if(ret != -1){ + bret = add_environment(getplugin(NULL), pluginhelperenv, true); + } + if(ret == -1 or not bret){ + error(0, errno, "Failed to set MANDOSPLUGINHELPERDIR" + " environment variable to \"%s\" for all plugins\n", + pluginhelperdir != NULL ? pluginhelperdir : PHDIR); + } + if(ret != -1){ + free(pluginhelperenv); + } + } + if(debug){ for(plugin *p = plugin_list; p != NULL; p=p->next){ fprintf(stderr, "Plugin: %s has %d arguments\n", @@ -795,7 +827,7 @@ } } } - TEMP_FAILURE_RETRY(close(plugindir_fd)); + close(plugindir_fd); } } @@ -893,7 +925,7 @@ ret = (int)TEMP_FAILURE_RETRY(fstat(plugin_fd, &st)); if(ret == -1){ error(0, errno, "stat"); - TEMP_FAILURE_RETRY(close(plugin_fd)); + close(plugin_fd); free(direntries[i]); continue; } @@ -908,7 +940,7 @@ plugindir != NULL ? plugindir : PDIR, direntries[i]->d_name); } - TEMP_FAILURE_RETRY(close(plugin_fd)); + close(plugin_fd); free(direntries[i]); continue; } @@ -916,7 +948,7 @@ plugin *p = getplugin(direntries[i]->d_name); if(p == NULL){ error(0, errno, "getplugin"); - TEMP_FAILURE_RETRY(close(plugin_fd)); + close(plugin_fd); free(direntries[i]); continue; } @@ -925,7 +957,7 @@ fprintf(stderr, "Ignoring disabled plugin \"%s\"\n", direntries[i]->d_name); } - TEMP_FAILURE_RETRY(close(plugin_fd)); + close(plugin_fd); free(direntries[i]); continue; } @@ -971,8 +1003,8 @@ if(pipefd[0] >= FD_SETSIZE){ fprintf(stderr, "pipe()[0] (%d) >= FD_SETSIZE (%d)", pipefd[0], FD_SETSIZE); - TEMP_FAILURE_RETRY(close(pipefd[0])); - TEMP_FAILURE_RETRY(close(pipefd[1])); + close(pipefd[0]); + close(pipefd[1]); exitstatus = EX_OSERR; free(direntries[i]); goto fallback; @@ -982,8 +1014,8 @@ ret = set_cloexec_flag(pipefd[0]); if(ret < 0){ error(0, errno, "set_cloexec_flag"); - TEMP_FAILURE_RETRY(close(pipefd[0])); - TEMP_FAILURE_RETRY(close(pipefd[1])); + close(pipefd[0]); + close(pipefd[1]); exitstatus = EX_OSERR; free(direntries[i]); goto fallback; @@ -991,8 +1023,8 @@ ret = set_cloexec_flag(pipefd[1]); if(ret < 0){ error(0, errno, "set_cloexec_flag"); - TEMP_FAILURE_RETRY(close(pipefd[0])); - TEMP_FAILURE_RETRY(close(pipefd[1])); + close(pipefd[0]); + close(pipefd[1]); exitstatus = EX_OSERR; free(direntries[i]); goto fallback; @@ -1017,8 +1049,8 @@ error(0, errno, "fork"); TEMP_FAILURE_RETRY(sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL)); - TEMP_FAILURE_RETRY(close(pipefd[0])); - TEMP_FAILURE_RETRY(close(pipefd[1])); + close(pipefd[0]); + close(pipefd[1]); exitstatus = EX_OSERR; free(direntries[i]); goto fallback; @@ -1052,9 +1084,8 @@ /* no return */ } /* Parent process */ - TEMP_FAILURE_RETRY(close(pipefd[1])); /* Close unused write end of - pipe */ - TEMP_FAILURE_RETRY(close(plugin_fd)); + close(pipefd[1]); /* Close unused write end of pipe */ + close(plugin_fd); plugin *new_plugin = getplugin(direntries[i]->d_name); if(new_plugin == NULL){ error(0, errno, "getplugin"); @@ -1106,7 +1137,7 @@ free(direntries); direntries = NULL; - TEMP_FAILURE_RETRY(close(dir_fd)); + close(dir_fd); dir_fd = -1; free_plugin(getplugin(NULL)); @@ -1310,7 +1341,7 @@ free(direntries); if(dir_fd != -1){ - TEMP_FAILURE_RETRY(close(dir_fd)); + close(dir_fd); } /* Kill the processes */ @@ -1336,6 +1367,7 @@ free_plugin_list(); free(plugindir); + free(pluginhelperdir); free(argfile); return exitstatus; === modified file 'plugin-runner.xml' --- plugin-runner.xml 2011-12-31 23:05:34 +0000 +++ plugin-runner.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,7 +33,12 @@ 2008 2009 + 2010 + 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson @@ -114,6 +119,9 @@ + + @@ -320,6 +328,21 @@ + + + + Specify a different plugin helper directory. The default + is /lib/mandos/plugin-helpers, which + will exist in the initial RAM disk + environment. (This will simply be passed to all plugins + via the MANDOSPLUGINHELPERDIR environment + variable. See ) + + + + + @@ -426,7 +449,11 @@ The plugin will run in the initial RAM disk environment, so care must be taken not to depend on any files or running - services not available there. + services not available there. Any helper executables required + by the plugin (which are not in the PATH) can + be placed in the plugin helper directory, the name of which + will be made available to the plugin via the + MANDOSPLUGINHELPERDIR environment variable. The plugin must exit cleanly and free all allocated resources @@ -475,7 +502,9 @@ only passes on its environment to all the plugins. The environment passed to plugins can be modified using the and - options. + options. Also, the option + will affect the environment variable + MANDOSPLUGINHELPERDIR for the plugins. @@ -572,15 +601,16 @@ - Run plugins from a different directory, read a different - configuration file, and add two options to the + Read a different configuration file, run plugins from a + different directory, specify an alternate plugin helper + directory and add two options to the mandos-client 8mandos plugin: -cd /etc/keys/mandos; &COMMANDNAME; --config-file=/etc/mandos/plugin-runner.conf --plugin-dir /usr/lib/mandos/plugins.d --options-for=mandos-client:--pubkey=pubkey.txt,--seckey=seckey.txt +cd /etc/keys/mandos; &COMMANDNAME; --config-file=/etc/mandos/plugin-runner.conf --plugin-dir /usr/lib/x86_64-linux-gnu/mandos/plugins.d --plugin-helper-dir /usr/lib/x86_64-linux-gnu/mandos/plugin-helpers --options-for=mandos-client:--pubkey=pubkey.txt,--seckey=seckey.txt === modified file 'plugins.d/askpass-fifo.c' --- plugins.d/askpass-fifo.c 2014-08-09 23:37:07 +0000 +++ plugins.d/askpass-fifo.c 2015-07-20 04:03:32 +0000 @@ -2,8 +2,8 @@ /* * Askpass-FIFO - Read a password from a FIFO and output it * - * Copyright © 2008-2014 Teddy Hogeborn - * Copyright © 2008-2014 Björn Påhlsson + * Copyright © 2008-2015 Teddy Hogeborn + * Copyright © 2008-2015 Björn Påhlsson * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as === modified file 'plugins.d/askpass-fifo.xml' --- plugins.d/askpass-fifo.xml 2011-12-31 23:05:34 +0000 +++ plugins.d/askpass-fifo.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,8 +33,12 @@ 2008 2009 + 2010 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson === modified file 'plugins.d/mandos-client.c' --- plugins.d/mandos-client.c 2014-08-20 21:46:38 +0000 +++ plugins.d/mandos-client.c 2015-07-20 03:03:33 +0000 @@ -9,8 +9,8 @@ * "browse_callback", and parts of "main". * * Everything else is - * Copyright © 2008-2014 Teddy Hogeborn - * Copyright © 2008-2014 Björn Påhlsson + * Copyright © 2008-2015 Teddy Hogeborn + * Copyright © 2008-2015 Björn Påhlsson * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -46,8 +46,8 @@ #include /* free(), EXIT_SUCCESS, srand(), strtof(), abort() */ #include /* bool, false, true */ -#include /* memset(), strcmp(), strlen(), - strerror(), asprintf(), strcpy() */ +#include /* strcmp(), strlen(), strerror(), + asprintf(), strcpy() */ #include /* ioctl */ #include /* socket(), inet_pton(), sockaddr, sockaddr_in6, PF_INET6, @@ -305,7 +305,7 @@ return false; } - ret = (int)TEMP_FAILURE_RETRY(close(fd)); + ret = close(fd); if(ret == -1){ perror_plus("close"); } @@ -493,12 +493,17 @@ return plaintext_length; } +__attribute__((warn_unused_result, const)) +static const char *safe_string(const char *str){ + if(str == NULL) + return "(unknown)"; + return str; +} + __attribute__((warn_unused_result)) static const char *safer_gnutls_strerror(int value){ const char *ret = gnutls_strerror(value); - if(ret == NULL) - ret = "(unknown)"; - return ret; + return safe_string(ret); } /* GnuTLS log function callback */ @@ -511,8 +516,10 @@ __attribute__((nonnull, warn_unused_result)) static int init_gnutls_global(const char *pubkeyfilename, const char *seckeyfilename, + const char *dhparamsfilename, mandos_context *mc){ int ret; + unsigned int uret; if(debug){ fprintf_plus(stderr, "Initializing GnuTLS\n"); @@ -569,13 +576,178 @@ safer_gnutls_strerror(ret)); goto globalfail; } - ret = gnutls_dh_params_generate2(mc->dh_params, mc->dh_bits); - if(ret != GNUTLS_E_SUCCESS){ - fprintf_plus(stderr, "Error in GnuTLS prime generation: %s\n", - safer_gnutls_strerror(ret)); - goto globalfail; - } - + /* If a Diffie-Hellman parameters file was given, try to use it */ + if(dhparamsfilename != NULL){ + gnutls_datum_t params = { .data = NULL, .size = 0 }; + do { + int dhpfile = open(dhparamsfilename, O_RDONLY); + if(dhpfile == -1){ + perror_plus("open"); + dhparamsfilename = NULL; + break; + } + size_t params_capacity = 0; + while(true){ + params_capacity = incbuffer((char **)¶ms.data, + (size_t)params.size, + (size_t)params_capacity); + if(params_capacity == 0){ + perror_plus("incbuffer"); + free(params.data); + params.data = NULL; + dhparamsfilename = NULL; + break; + } + ssize_t bytes_read = read(dhpfile, + params.data + params.size, + BUFFER_SIZE); + /* EOF */ + if(bytes_read == 0){ + break; + } + /* check bytes_read for failure */ + if(bytes_read < 0){ + perror_plus("read"); + free(params.data); + params.data = NULL; + dhparamsfilename = NULL; + break; + } + params.size += (unsigned int)bytes_read; + } + if(params.data == NULL){ + dhparamsfilename = NULL; + } + if(dhparamsfilename == NULL){ + break; + } + ret = gnutls_dh_params_import_pkcs3(mc->dh_params, ¶ms, + GNUTLS_X509_FMT_PEM); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Failed to parse DH parameters in file" + " \"%s\": %s\n", dhparamsfilename, + safer_gnutls_strerror(ret)); + dhparamsfilename = NULL; + } + } while(false); + } + if(dhparamsfilename == NULL){ + if(mc->dh_bits == 0){ + /* Find out the optimal number of DH bits */ + /* Try to read the private key file */ + gnutls_datum_t buffer = { .data = NULL, .size = 0 }; + do { + int secfile = open(seckeyfilename, O_RDONLY); + if(secfile == -1){ + perror_plus("open"); + break; + } + size_t buffer_capacity = 0; + while(true){ + buffer_capacity = incbuffer((char **)&buffer.data, + (size_t)buffer.size, + (size_t)buffer_capacity); + if(buffer_capacity == 0){ + perror_plus("incbuffer"); + free(buffer.data); + buffer.data = NULL; + break; + } + ssize_t bytes_read = read(secfile, + buffer.data + buffer.size, + BUFFER_SIZE); + /* EOF */ + if(bytes_read == 0){ + break; + } + /* check bytes_read for failure */ + if(bytes_read < 0){ + perror_plus("read"); + free(buffer.data); + buffer.data = NULL; + break; + } + buffer.size += (unsigned int)bytes_read; + } + close(secfile); + } while(false); + /* If successful, use buffer to parse private key */ + gnutls_sec_param_t sec_param = GNUTLS_SEC_PARAM_ULTRA; + if(buffer.data != NULL){ + { + gnutls_openpgp_privkey_t privkey = NULL; + ret = gnutls_openpgp_privkey_init(&privkey); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Error initializing OpenPGP key" + " structure: %s", + safer_gnutls_strerror(ret)); + free(buffer.data); + buffer.data = NULL; + } else { + ret = gnutls_openpgp_privkey_import + (privkey, &buffer, GNUTLS_OPENPGP_FMT_BASE64, "", 0); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Error importing OpenPGP key : %s", + safer_gnutls_strerror(ret)); + privkey = NULL; + } + free(buffer.data); + buffer.data = NULL; + if(privkey != NULL){ + /* Use private key to suggest an appropriate + sec_param */ + sec_param = gnutls_openpgp_privkey_sec_param(privkey); + gnutls_openpgp_privkey_deinit(privkey); + if(debug){ + fprintf_plus(stderr, "This OpenPGP key implies using" + " a GnuTLS security parameter \"%s\".\n", + safe_string(gnutls_sec_param_get_name + (sec_param))); + } + } + } + } + if(sec_param == GNUTLS_SEC_PARAM_UNKNOWN){ + /* Err on the side of caution */ + sec_param = GNUTLS_SEC_PARAM_ULTRA; + if(debug){ + fprintf_plus(stderr, "Falling back to security parameter" + " \"%s\"\n", + safe_string(gnutls_sec_param_get_name + (sec_param))); + } + } + } + uret = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, sec_param); + if(uret != 0){ + mc->dh_bits = uret; + if(debug){ + fprintf_plus(stderr, "A \"%s\" GnuTLS security parameter" + " implies %u DH bits; using that.\n", + safe_string(gnutls_sec_param_get_name + (sec_param)), + mc->dh_bits); + } + } else { + fprintf_plus(stderr, "Failed to get implied number of DH" + " bits for security parameter \"%s\"): %s\n", + safe_string(gnutls_sec_param_get_name + (sec_param)), + safer_gnutls_strerror(ret)); + goto globalfail; + } + } else if(debug){ + fprintf_plus(stderr, "DH bits explicitly set to %u\n", + mc->dh_bits); + } + ret = gnutls_dh_params_generate2(mc->dh_params, mc->dh_bits); + if(ret != GNUTLS_E_SUCCESS){ + fprintf_plus(stderr, "Error in GnuTLS prime generation (%u" + " bits): %s\n", mc->dh_bits, + safer_gnutls_strerror(ret)); + goto globalfail; + } + } gnutls_certificate_set_dh_params(mc->cred, mc->dh_params); return 0; @@ -641,8 +813,6 @@ /* ignore client certificate if any. */ gnutls_certificate_server_set_request(*session, GNUTLS_CERT_IGNORE); - gnutls_dh_set_prime_bits(*session, mc->dh_bits); - return 0; } @@ -650,6 +820,224 @@ static void empty_log(__attribute__((unused)) AvahiLogLevel level, __attribute__((unused)) const char *txt){} +/* Set effective uid to 0, return errno */ +__attribute__((warn_unused_result)) +error_t raise_privileges(void){ + error_t old_errno = errno; + error_t ret_errno = 0; + if(seteuid(0) == -1){ + ret_errno = errno; + } + errno = old_errno; + return ret_errno; +} + +/* Set effective and real user ID to 0. Return errno. */ +__attribute__((warn_unused_result)) +error_t raise_privileges_permanently(void){ + error_t old_errno = errno; + error_t ret_errno = raise_privileges(); + if(ret_errno != 0){ + errno = old_errno; + return ret_errno; + } + if(setuid(0) == -1){ + ret_errno = errno; + } + errno = old_errno; + return ret_errno; +} + +/* Set effective user ID to unprivileged saved user ID */ +__attribute__((warn_unused_result)) +error_t lower_privileges(void){ + error_t old_errno = errno; + error_t ret_errno = 0; + if(seteuid(uid) == -1){ + ret_errno = errno; + } + errno = old_errno; + return ret_errno; +} + +/* Lower privileges permanently */ +__attribute__((warn_unused_result)) +error_t lower_privileges_permanently(void){ + error_t old_errno = errno; + error_t ret_errno = 0; + if(setuid(uid) == -1){ + ret_errno = errno; + } + errno = old_errno; + return ret_errno; +} + +/* Helper function to add_local_route() and delete_local_route() */ +__attribute__((nonnull, warn_unused_result)) +static bool add_delete_local_route(const bool add, + const char *address, + AvahiIfIndex if_index){ + int ret; + char helper[] = "mandos-client-iprouteadddel"; + char add_arg[] = "add"; + char delete_arg[] = "delete"; + char debug_flag[] = "--debug"; + char *pluginhelperdir = getenv("MANDOSPLUGINHELPERDIR"); + if(pluginhelperdir == NULL){ + if(debug){ + fprintf_plus(stderr, "MANDOSPLUGINHELPERDIR environment" + " variable not set; cannot run helper\n"); + } + return false; + } + + char interface[IF_NAMESIZE]; + if(if_indextoname((unsigned int)if_index, interface) == NULL){ + perror_plus("if_indextoname"); + return false; + } + + int devnull = (int)TEMP_FAILURE_RETRY(open("/dev/null", O_RDONLY)); + if(devnull == -1){ + perror_plus("open(\"/dev/null\", O_RDONLY)"); + return false; + } + pid_t pid = fork(); + if(pid == 0){ + /* Child */ + /* Raise privileges */ + errno = raise_privileges_permanently(); + if(errno != 0){ + perror_plus("Failed to raise privileges"); + /* _exit(EX_NOPERM); */ + } else { + /* Set group */ + errno = 0; + ret = setgid(0); + if(ret == -1){ + perror_plus("setgid"); + _exit(EX_NOPERM); + } + /* Reset supplementary groups */ + errno = 0; + ret = setgroups(0, NULL); + if(ret == -1){ + perror_plus("setgroups"); + _exit(EX_NOPERM); + } + } + ret = dup2(devnull, STDIN_FILENO); + if(ret == -1){ + perror_plus("dup2(devnull, STDIN_FILENO)"); + _exit(EX_OSERR); + } + ret = close(devnull); + if(ret == -1){ + perror_plus("close"); + _exit(EX_OSERR); + } + ret = dup2(STDERR_FILENO, STDOUT_FILENO); + if(ret == -1){ + perror_plus("dup2(STDERR_FILENO, STDOUT_FILENO)"); + _exit(EX_OSERR); + } + int helperdir_fd = (int)TEMP_FAILURE_RETRY(open(pluginhelperdir, + O_RDONLY + | O_DIRECTORY + | O_PATH + | O_CLOEXEC)); + if(helperdir_fd == -1){ + perror_plus("open"); + _exit(EX_UNAVAILABLE); + } + int helper_fd = (int)TEMP_FAILURE_RETRY(openat(helperdir_fd, + helper, O_RDONLY)); + if(helper_fd == -1){ + perror_plus("openat"); + close(helperdir_fd); + _exit(EX_UNAVAILABLE); + } + close(helperdir_fd); +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + if(fexecve(helper_fd, (char *const []) + { helper, add ? add_arg : delete_arg, (char *)address, + interface, debug ? debug_flag : NULL, NULL }, + environ) == -1){ +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + perror_plus("fexecve"); + _exit(EXIT_FAILURE); + } + } + if(pid == -1){ + perror_plus("fork"); + return false; + } + int status; + pid_t pret = -1; + errno = 0; + do { + pret = waitpid(pid, &status, 0); + if(pret == -1 and errno == EINTR and quit_now){ + int errno_raising = 0; + if((errno = raise_privileges()) != 0){ + errno_raising = errno; + perror_plus("Failed to raise privileges in order to" + " kill helper program"); + } + if(kill(pid, SIGTERM) == -1){ + perror_plus("kill"); + } + if((errno_raising == 0) and (errno = lower_privileges()) != 0){ + perror_plus("Failed to lower privileges after killing" + " helper program"); + } + return false; + } + } while(pret == -1 and errno == EINTR); + if(pret == -1){ + perror_plus("waitpid"); + return false; + } + if(WIFEXITED(status)){ + if(WEXITSTATUS(status) != 0){ + fprintf_plus(stderr, "Error: iprouteadddel exited" + " with status %d\n", WEXITSTATUS(status)); + return false; + } + return true; + } + if(WIFSIGNALED(status)){ + fprintf_plus(stderr, "Error: iprouteadddel died by" + " signal %d\n", WTERMSIG(status)); + return false; + } + fprintf_plus(stderr, "Error: iprouteadddel crashed\n"); + return false; +} + +__attribute__((nonnull, warn_unused_result)) +static bool add_local_route(const char *address, + AvahiIfIndex if_index){ + if(debug){ + fprintf_plus(stderr, "Adding route to %s\n", address); + } + return add_delete_local_route(true, address, if_index); +} + +__attribute__((nonnull, warn_unused_result)) +static bool delete_local_route(const char *address, + AvahiIfIndex if_index){ + if(debug){ + fprintf_plus(stderr, "Removing route to %s\n", address); + } + return add_delete_local_route(false, address, if_index); +} + /* Called when a Mandos server is found */ __attribute__((nonnull, warn_unused_result)) static int start_mandos_communication(const char *ip, in_port_t port, @@ -666,6 +1054,7 @@ int retval = -1; gnutls_session_t session; int pf; /* Protocol family */ + bool route_added = false; errno = 0; @@ -729,7 +1118,7 @@ PRIuMAX "\n", ip, (uintmax_t)port); } - tcp_sd = socket(pf, SOCK_STREAM, 0); + tcp_sd = socket(pf, SOCK_STREAM | SOCK_CLOEXEC, 0); if(tcp_sd < 0){ int e = errno; perror_plus("socket"); @@ -742,13 +1131,14 @@ goto mandos_end; } - memset(&to, 0, sizeof(to)); if(af == AF_INET6){ - ((struct sockaddr_in6 *)&to)->sin6_family = (sa_family_t)af; - ret = inet_pton(af, ip, &((struct sockaddr_in6 *)&to)->sin6_addr); + struct sockaddr_in6 *to6 = (struct sockaddr_in6 *)&to; + *to6 = (struct sockaddr_in6){ .sin6_family = (sa_family_t)af }; + ret = inet_pton(af, ip, &to6->sin6_addr); } else { /* IPv4 */ - ((struct sockaddr_in *)&to)->sin_family = (sa_family_t)af; - ret = inet_pton(af, ip, &((struct sockaddr_in *)&to)->sin_addr); + struct sockaddr_in *to4 = (struct sockaddr_in *)&to; + *to4 = (struct sockaddr_in){ .sin_family = (sa_family_t)af }; + ret = inet_pton(af, ip, &to4->sin_addr); } if(ret < 0 ){ int e = errno; @@ -824,25 +1214,61 @@ goto mandos_end; } - if(af == AF_INET6){ - ret = connect(tcp_sd, (struct sockaddr *)&to, - sizeof(struct sockaddr_in6)); - } else { - ret = connect(tcp_sd, (struct sockaddr *)&to, /* IPv4 */ - sizeof(struct sockaddr_in)); - } - if(ret < 0){ - if((errno != ECONNREFUSED and errno != ENETUNREACH) or debug){ - int e = errno; - perror_plus("connect"); - errno = e; - } - goto mandos_end; - } - - if(quit_now){ - errno = EINTR; - goto mandos_end; + while(true){ + if(af == AF_INET6){ + ret = connect(tcp_sd, (struct sockaddr *)&to, + sizeof(struct sockaddr_in6)); + } else { + ret = connect(tcp_sd, (struct sockaddr *)&to, /* IPv4 */ + sizeof(struct sockaddr_in)); + } + if(ret < 0){ + if(errno == ENETUNREACH + and if_index != AVAHI_IF_UNSPEC + and connect_to == NULL + and not route_added and + ((af == AF_INET6 and not + IN6_IS_ADDR_LINKLOCAL(&(((struct sockaddr_in6 *) + &to)->sin6_addr))) + or (af == AF_INET and + /* Not a a IPv4LL address */ + (ntohl(((struct sockaddr_in *)&to)->sin_addr.s_addr) + & 0xFFFF0000L) != 0xA9FE0000L))){ + /* Work around Avahi bug - Avahi does not announce link-local + addresses if it has a global address, so local hosts with + *only* a link-local address (e.g. Mandos clients) cannot + connect to a Mandos server announced by Avahi on a server + host with a global address. Work around this by retrying + with an explicit route added with the server's address. + + Avahi bug reference: + http://lists.freedesktop.org/archives/avahi/2010-February/001833.html + https://bugs.debian.org/587961 + */ + if(debug){ + fprintf_plus(stderr, "Mandos server unreachable, trying" + " direct route\n"); + } + int e = errno; + route_added = add_local_route(ip, if_index); + if(route_added){ + continue; + } + errno = e; + } + if(errno != ECONNREFUSED or debug){ + int e = errno; + perror_plus("connect"); + errno = e; + } + goto mandos_end; + } + + if(quit_now){ + errno = EINTR; + goto mandos_end; + } + break; } const char *out = mandos_protocol_version; @@ -1031,11 +1457,17 @@ mandos_end: { + if(route_added){ + if(not delete_local_route(ip, if_index)){ + fprintf_plus(stderr, "Failed to delete local route to %s on" + " interface %d", ip, if_index); + } + } int e = errno; free(decrypted_buffer); free(buffer); if(tcp_sd >= 0){ - ret = (int)TEMP_FAILURE_RETRY(close(tcp_sd)); + ret = close(tcp_sd); } if(ret == -1){ if(e == 0){ @@ -1462,64 +1894,13 @@ } } -/* Set effective uid to 0, return errno */ -__attribute__((warn_unused_result)) -error_t raise_privileges(void){ - error_t old_errno = errno; - error_t ret_errno = 0; - if(seteuid(0) == -1){ - ret_errno = errno; - } - errno = old_errno; - return ret_errno; -} - -/* Set effective and real user ID to 0. Return errno. */ -__attribute__((warn_unused_result)) -error_t raise_privileges_permanently(void){ - error_t old_errno = errno; - error_t ret_errno = raise_privileges(); - if(ret_errno != 0){ - errno = old_errno; - return ret_errno; - } - if(setuid(0) == -1){ - ret_errno = errno; - } - errno = old_errno; - return ret_errno; -} - -/* Set effective user ID to unprivileged saved user ID */ -__attribute__((warn_unused_result)) -error_t lower_privileges(void){ - error_t old_errno = errno; - error_t ret_errno = 0; - if(seteuid(uid) == -1){ - ret_errno = errno; - } - errno = old_errno; - return ret_errno; -} - -/* Lower privileges permanently */ -__attribute__((warn_unused_result)) -error_t lower_privileges_permanently(void){ - error_t old_errno = errno; - error_t ret_errno = 0; - if(setuid(uid) == -1){ - ret_errno = errno; - } - errno = old_errno; - return ret_errno; -} - __attribute__((nonnull)) void run_network_hooks(const char *mode, const char *interface, const float delay){ struct dirent **direntries = NULL; if(hookdir_fd == -1){ - hookdir_fd = open(hookdir, O_RDONLY); + hookdir_fd = open(hookdir, O_RDONLY | O_DIRECTORY | O_PATH + | O_CLOEXEC); if(hookdir_fd == -1){ if(errno == ENOENT){ if(debug){ @@ -1550,7 +1931,11 @@ } struct dirent *direntry; int ret; - int devnull = open("/dev/null", O_RDONLY); + int devnull = (int)TEMP_FAILURE_RETRY(open("/dev/null", O_RDONLY)); + if(devnull == -1){ + perror_plus("open(\"/dev/null\", O_RDONLY)"); + return; + } for(int i = 0; i < numhooks; i++){ direntry = direntries[i]; if(debug){ @@ -1580,21 +1965,6 @@ perror_plus("setgroups"); _exit(EX_NOPERM); } - ret = dup2(devnull, STDIN_FILENO); - if(ret == -1){ - perror_plus("dup2(devnull, STDIN_FILENO)"); - _exit(EX_OSERR); - } - ret = close(devnull); - if(ret == -1){ - perror_plus("close"); - _exit(EX_OSERR); - } - ret = dup2(STDERR_FILENO, STDOUT_FILENO); - if(ret == -1){ - perror_plus("dup2(STDERR_FILENO, STDOUT_FILENO)"); - _exit(EX_OSERR); - } ret = setenv("MANDOSNETHOOKDIR", hookdir, 1); if(ret == -1){ perror_plus("setenv"); @@ -1635,15 +2005,32 @@ _exit(EX_OSERR); } } - int hook_fd = openat(hookdir_fd, direntry->d_name, O_RDONLY); + int hook_fd = (int)TEMP_FAILURE_RETRY(openat(hookdir_fd, + direntry->d_name, + O_RDONLY)); if(hook_fd == -1){ perror_plus("openat"); _exit(EXIT_FAILURE); } - if((int)TEMP_FAILURE_RETRY(close(hookdir_fd)) == -1){ + if(close(hookdir_fd) == -1){ perror_plus("close"); _exit(EXIT_FAILURE); } + ret = dup2(devnull, STDIN_FILENO); + if(ret == -1){ + perror_plus("dup2(devnull, STDIN_FILENO)"); + _exit(EX_OSERR); + } + ret = close(devnull); + if(ret == -1){ + perror_plus("close"); + _exit(EX_OSERR); + } + ret = dup2(STDERR_FILENO, STDOUT_FILENO); + if(ret == -1){ + perror_plus("dup2(STDERR_FILENO, STDOUT_FILENO)"); + _exit(EX_OSERR); + } if(fexecve(hook_fd, (char *const []){ direntry->d_name, NULL }, environ) == -1){ perror_plus("fexecve"); @@ -1689,7 +2076,7 @@ free(direntry); } free(direntries); - if((int)TEMP_FAILURE_RETRY(close(hookdir_fd)) == -1){ + if(close(hookdir_fd) == -1){ perror_plus("close"); } else { hookdir_fd = -1; @@ -1735,7 +2122,7 @@ } if(quit_now){ - ret = (int)TEMP_FAILURE_RETRY(close(sd)); + ret = close(sd); if(ret == -1){ perror_plus("close"); } @@ -1791,7 +2178,7 @@ } /* Close the socket */ - ret = (int)TEMP_FAILURE_RETRY(close(sd)); + ret = close(sd); if(ret == -1){ perror_plus("close"); } @@ -1879,7 +2266,7 @@ } /* Close the socket */ - int ret = (int)TEMP_FAILURE_RETRY(close(sd)); + int ret = close(sd); if(ret == -1){ perror_plus("close"); } @@ -1900,10 +2287,11 @@ } int main(int argc, char *argv[]){ - mandos_context mc = { .server = NULL, .dh_bits = 1024, - .priority = "SECURE256:!CTYPE-X.509:" - "+CTYPE-OPENPGP", .current_server = NULL, - .interfaces = NULL, .interfaces_size = 0 }; + mandos_context mc = { .server = NULL, .dh_bits = 0, + .priority = "SECURE256:!CTYPE-X.509" + ":+CTYPE-OPENPGP:!RSA:+SIGN-DSA-SHA256", + .current_server = NULL, .interfaces = NULL, + .interfaces_size = 0 }; AvahiSServiceBrowser *sb = NULL; error_t ret_errno; int ret; @@ -1918,6 +2306,7 @@ AvahiIfIndex if_index = AVAHI_IF_UNSPEC; const char *seckey = PATHDIR "/" SECKEY; const char *pubkey = PATHDIR "/" PUBKEY; + const char *dh_params_file = NULL; char *interfaces_hooks = NULL; bool gnutls_initialized = false; @@ -1976,6 +2365,11 @@ .doc = "Bit length of the prime number used in the" " Diffie-Hellman key exchange", .group = 2 }, + { .name = "dh-params", .key = 134, + .arg = "FILE", + .doc = "PEM-encoded PKCS#3 file with pre-generated parameters" + " for the Diffie-Hellman key exchange", + .group = 2 }, { .name = "priority", .key = 130, .arg = "STRING", .doc = "GnuTLS priority string for the TLS handshake", @@ -2036,6 +2430,9 @@ } mc.dh_bits = (typeof(mc.dh_bits))tmpmax; break; + case 134: /* --dh-params */ + dh_params_file = arg; + break; case 130: /* --priority */ mc.priority = arg; break; @@ -2097,7 +2494,7 @@ goto end; } } - + { /* Work around Debian bug #633582: */ @@ -2127,10 +2524,10 @@ } } } - TEMP_FAILURE_RETRY(close(seckey_fd)); + close(seckey_fd); } } - + if(strcmp(pubkey, PATHDIR "/" PUBKEY) == 0){ int pubkey_fd = open(pubkey, O_RDONLY); if(pubkey_fd == -1){ @@ -2148,10 +2545,32 @@ } } } - TEMP_FAILURE_RETRY(close(pubkey_fd)); - } - } - + close(pubkey_fd); + } + } + + if(dh_params_file != NULL + and strcmp(dh_params_file, PATHDIR "/dhparams.pem" ) == 0){ + int dhparams_fd = open(dh_params_file, O_RDONLY); + if(dhparams_fd == -1){ + perror_plus("open"); + } else { + ret = (int)TEMP_FAILURE_RETRY(fstat(dhparams_fd, &st)); + if(ret == -1){ + perror_plus("fstat"); + } else { + if(S_ISREG(st.st_mode) + and st.st_uid == 0 and st.st_gid == 0){ + ret = fchown(dhparams_fd, uid, gid); + if(ret == -1){ + perror_plus("fchown"); + } + } + } + close(dhparams_fd); + } + } + /* Lower privileges */ ret_errno = lower_privileges(); if(ret_errno != 0){ @@ -2331,7 +2750,8 @@ errno = bring_up_interface(interface, delay); if(not interface_was_up){ if(errno != 0){ - perror_plus("Failed to bring up interface"); + fprintf_plus(stderr, "Failed to bring up interface \"%s\":" + " %s\n", interface, strerror(errno)); } else { errno = argz_add(&interfaces_to_take_down, &interfaces_to_take_down_size, @@ -2360,7 +2780,7 @@ goto end; } - ret = init_gnutls_global(pubkey, seckey, &mc); + ret = init_gnutls_global(pubkey, seckey, dh_params_file, &mc); if(ret == -1){ fprintf_plus(stderr, "init_gnutls_global failed\n"); exitcode = EX_UNAVAILABLE; @@ -2626,8 +3046,10 @@ /* Removes the GPGME temp directory and all files inside */ if(tempdir != NULL){ struct dirent **direntries = NULL; - int tempdir_fd = (int)TEMP_FAILURE_RETRY(open(tempdir, O_RDONLY | - O_NOFOLLOW)); + int tempdir_fd = (int)TEMP_FAILURE_RETRY(open(tempdir, O_RDONLY + | O_NOFOLLOW + | O_DIRECTORY + | O_PATH)); if(tempdir_fd == -1){ perror_plus("open"); } else { @@ -2664,7 +3086,7 @@ perror_plus("rmdir"); } } - TEMP_FAILURE_RETRY(close(tempdir_fd)); + close(tempdir_fd); } } === modified file 'plugins.d/mandos-client.xml' --- plugins.d/mandos-client.xml 2014-06-22 02:19:30 +0000 +++ plugins.d/mandos-client.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,9 +33,12 @@ 2008 2009 + 2010 + 2011 2012 2013 2014 + 2015 Teddy Hogeborn Björn Påhlsson @@ -98,6 +101,10 @@ + + + + @@ -311,7 +318,27 @@ Sets the number of bits to use for the prime number in the - TLS Diffie-Hellman key exchange. Default is 1024. + TLS Diffie-Hellman key exchange. The default value is + selected automatically based on the OpenPGP key. Note + that if the option is used, + the values from that file will be used instead. + + + + + + + + + Specifies a PEM-encoded PKCS#3 file to read the parameters + needed by the TLS Diffie-Hellman key exchange from. If + this option is not given, or if the file for some reason + could not be used, the parameters will be generated on + startup, which will take some time and processing power. + Those using servers running under time, power or processor + constraints may want to generate such a file in advance + and use this option. @@ -444,9 +471,22 @@ ENVIRONMENT + + + MANDOSPLUGINHELPERDIR + + + This environment variable will be assumed to contain the + directory containing any helper executables. The use and + nature of these helper executables, if any, is + purposefully not documented. + + + + - This program does not use any environment variables, not even - the ones provided by cryptsetup8 . === modified file 'plugins.d/password-prompt.c' --- plugins.d/password-prompt.c 2014-03-29 02:38:15 +0000 +++ plugins.d/password-prompt.c 2015-07-20 04:03:32 +0000 @@ -2,8 +2,8 @@ /* * Password-prompt - Read a password from the terminal and print it * - * Copyright © 2008-2014 Teddy Hogeborn - * Copyright © 2008-2014 Björn Påhlsson + * Copyright © 2008-2015 Teddy Hogeborn + * Copyright © 2008-2015 Björn Påhlsson * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as === modified file 'plugins.d/password-prompt.xml' --- plugins.d/password-prompt.xml 2011-12-31 23:05:34 +0000 +++ plugins.d/password-prompt.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,8 +33,12 @@ 2008 2009 + 2010 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson === modified file 'plugins.d/plymouth.c' --- plugins.d/plymouth.c 2014-03-29 02:38:15 +0000 +++ plugins.d/plymouth.c 2015-07-20 04:03:32 +0000 @@ -2,8 +2,8 @@ /* * Plymouth - Read a password from Plymouth and output it * - * Copyright © 2010-2014 Teddy Hogeborn - * Copyright © 2010-2014 Björn Påhlsson + * Copyright © 2010-2015 Teddy Hogeborn + * Copyright © 2010-2015 Björn Påhlsson * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as === modified file 'plugins.d/plymouth.xml' --- plugins.d/plymouth.xml 2011-12-31 23:05:34 +0000 +++ plugins.d/plymouth.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -34,6 +34,9 @@ 2010 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson === modified file 'plugins.d/splashy.c' --- plugins.d/splashy.c 2014-03-29 02:38:15 +0000 +++ plugins.d/splashy.c 2015-07-20 04:03:32 +0000 @@ -2,8 +2,8 @@ /* * Splashy - Read a password from splashy and output it * - * Copyright © 2008-2014 Teddy Hogeborn - * Copyright © 2008-2014 Björn Påhlsson + * Copyright © 2008-2015 Teddy Hogeborn + * Copyright © 2008-2015 Björn Påhlsson * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as === modified file 'plugins.d/splashy.xml' --- plugins.d/splashy.xml 2011-12-31 23:05:34 +0000 +++ plugins.d/splashy.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,7 +33,12 @@ 2008 2009 + 2010 + 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson === modified file 'plugins.d/usplash.c' --- plugins.d/usplash.c 2014-03-29 02:38:15 +0000 +++ plugins.d/usplash.c 2015-07-20 04:03:32 +0000 @@ -2,8 +2,8 @@ /* * Usplash - Read a password from usplash and output it * - * Copyright © 2008-2014 Teddy Hogeborn - * Copyright © 2008-2014 Björn Påhlsson + * Copyright © 2008-2015 Teddy Hogeborn + * Copyright © 2008-2015 Björn Påhlsson * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as @@ -117,7 +117,7 @@ ret = asprintf(&cmd_line_alloc, "%s %s", cmd, arg); if(ret == -1){ int e = errno; - TEMP_FAILURE_RETRY(close(*fifo_fd_r)); + close(*fifo_fd_r); errno = e; return false; } @@ -133,7 +133,7 @@ cmd_line_len - written); if(sret == -1){ int e = errno; - TEMP_FAILURE_RETRY(close(*fifo_fd_r)); + close(*fifo_fd_r); free(cmd_line_alloc); errno = e; return false; @@ -491,7 +491,7 @@ error_plus(0, errno, "read"); status = EX_OSERR; } - TEMP_FAILURE_RETRY(close(outfifo_fd)); + close(outfifo_fd); goto failure; } if(interrupted_by_signal){ @@ -578,7 +578,7 @@ /* Close FIFO */ if(fifo_fd != -1){ - ret = (int)TEMP_FAILURE_RETRY(close(fifo_fd)); + ret = close(fifo_fd); if(ret == -1 and errno != EINTR){ error_plus(0, errno, "close"); } @@ -587,7 +587,7 @@ /* Close output FIFO */ if(outfifo_fd != -1){ - ret = (int)TEMP_FAILURE_RETRY(close(outfifo_fd)); + ret = close(outfifo_fd); if(ret == -1){ error_plus(0, errno, "close"); } @@ -655,7 +655,7 @@ /* Close FIFO (again) */ if(fifo_fd != -1){ - ret = (int)TEMP_FAILURE_RETRY(close(fifo_fd)); + ret = close(fifo_fd); if(ret == -1 and errno != EINTR){ error_plus(0, errno, "close"); } === modified file 'plugins.d/usplash.xml' --- plugins.d/usplash.xml 2011-12-31 23:05:34 +0000 +++ plugins.d/usplash.xml 2015-07-20 04:03:32 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,8 +33,12 @@ 2008 2009 + 2010 2011 2012 + 2013 + 2014 + 2015 Teddy Hogeborn Björn Påhlsson