=== modified file '.bzrignore' --- .bzrignore 2010-09-30 06:24:20 +0000 +++ .bzrignore 2012-05-17 01:55:58 +0000 @@ -3,6 +3,7 @@ *.8mandos confdir keydir +statedir man plugin-runner plugins.d/askpass-fifo === modified file 'DBUS-API' --- DBUS-API 2011-10-02 19:18:24 +0000 +++ DBUS-API 2014-08-10 14:13:02 +0000 @@ -93,10 +93,11 @@ | Interval (a) | t | Read/Write | interval | | LastApprovalRequest (g) | s | Read | N/A | | LastCheckedOK (h) | s | Read/Write | N/A | - | LastEnabled (i) | s | Read | N/A | + | LastCheckerStatus (i) | n | Read | N/A | + | LastEnabled (j) | s | Read | N/A | | Name | s | Read | (Section name) | | ObjectPath | o | Read | N/A | - | Secret (j) | ay | Write | secret (or secfile) | + | Secret (k) | ay | Write | secret (or secfile) | | Timeout (a) | t | Read/Write | timeout | a) Represented as milliseconds. @@ -112,7 +113,7 @@ Disable(). f) The date and time this client will be disabled, as an RFC 3339 - string, or an empty string if this has not happened. + string, or an empty string if this is not scheduled. g) The date and time of the last approval request, as an RFC 3339 string, or an empty string if this has not happened. @@ -124,10 +125,13 @@ always use an empty string when setting this property, to allow for possible future expansion. - i) The date and time this client was last enabled, as an RFC 3339 + i) The exit status of the last checker, -1 if it did not exit + cleanly, -2 if a checker has not yet returned. + + j) The date and time this client was last enabled, as an RFC 3339 string, or an empty string if this has not happened. - j) A raw byte array, not hexadecimal digits. + k) A raw byte array, not hexadecimal digits. ** Signals *** CheckerCompleted(n: Exitcode, x: Waitstatus, s: Command) @@ -146,16 +150,13 @@ milliseconds, depending on ApprovedByDefault. Approve() can now usefully be called on this client object. -*** PropertyChanged(s: Property, v: Value) - The Property on this client has changed to Value. - *** Rejected(s: Reason) This client was not given its secret for a specified Reason. * Copyright - Copyright © 2010-2011 Teddy Hogeborn - Copyright © 2010-2011 Björn Påhlsson + Copyright © 2010-2012 Teddy Hogeborn + Copyright © 2010-2012 Björn Påhlsson ** License: === modified file 'INSTALL' --- INSTALL 2011-03-08 19:09:03 +0000 +++ INSTALL 2014-07-25 22:44:20 +0000 @@ -4,15 +4,16 @@ ** Operating System - Debian 6.0 "squeeze" or Ubuntu 10.10 "Maverick Meerkat". + Debian 6.0 "squeeze" or Ubuntu 10.10 "Maverick Meerkat" (or later). This is mostly for the support scripts which make sure that the client is installed and started in the initial RAM disk environment - and that the initrd.img file is automatically made unreadable. The - server and client programs themselves *could* be run in other - distributions, but they *are* specific to GNU/Linux systems, and - are not written with portabillity to other Unixes in mind. - + and that the initial RAM file system image file is automatically + made unreadable. The server and client programs themselves *could* + be run in other distributions, but they *are* specific to GNU/Linux + systems, and are not written with portabillity to other Unixes in + mind. + ** Libraries The following libraries and packages are needed. (It is possible @@ -27,7 +28,7 @@ + DocBook 4.5 http://www.docbook.org/ Note: DocBook 5.0 is not compatible. + DocBook XSL stylesheets 1.71.0 - http://wiki.docbook.org/topic/DocBookXslStylesheets + http://wiki.docbook.org/DocBookXslStylesheets Package names: docbook docbook-xsl @@ -35,34 +36,42 @@ To build just the documentation, run the command "make doc". Then the manual page "mandos.8", for example, can be read by running "man -l mandos.8". - + *** Mandos Server - + GnuTLS 2.4 http://www.gnu.org/software/gnutls/ + + GnuTLS 2.4 http://www.gnutls.org/ + Note: GnuTLS 3 will only work with Python-GnuTLS 2 + Avahi 0.6.16 http://www.avahi.org/ - + Python 2.6 http://www.python.org/ - + Python-GnuTLS 1.1.5 http://pypi.python.org/pypi/python-gnutls/ + + Python 2.7 https://www.python.org/ + + Python-GnuTLS 1.1.5 https://pypi.python.org/pypi/python-gnutls/ + dbus-python 0.82.4 http://dbus.freedesktop.org/doc/dbus-python/ - + PyGObject 2.14.2 http://library.gnome.org/devel/pygobject/ - + Urwid 0.9.8.3 http://excess.org/urwid/ + + PyGObject 2.14.2 https://developer.gnome.org/pygobject/ + + pkg-config http://www.freedesktop.org/wiki/Software/pkg-config/ + + Urwid 1.0.1 http://urwid.org/ + (Only needed by the "mandos-monitor" tool.) Strongly recommended: - + fping 2.4b2-to-ipv6 http://www.fping.com/ + + fping 2.4b2-to-ipv6 http://www.fping.org/ + + ssh-keyscan from OpenSSH http://www.openssh.com/ Package names: python-gnutls avahi-daemon python python-avahi python-dbus - python-gobject python-urwid - + python-gobject python-urwid pkg-config fping ssh-client + *** Mandos Client + initramfs-tools 0.85i - http://packages.qa.debian.org/i/initramfs-tools.html - + GnuTLS 2.4 http://www.gnu.org/software/gnutls/ - + Avahi 0.6.16 http://www.avahi.org/ - + GnuPG 1.4.9 http://www.gnupg.org/ - + GPGME 1.1.6 http://www.gnupg.org/related_software/gpgme/ + https://tracker.debian.org/pkg/initramfs-tools + + GnuTLS 2.4 http://www.gnutls.org/ + + Avahi 0.6.16 http://www.avahi.org/ + + GnuPG 1.4.9 https://www.gnupg.org/ + + GPGME 1.1.6 https://www.gnupg.org/related_software/gpgme/ + + pkg-config http://www.freedesktop.org/wiki/Software/pkg-config/ + + Strongly recommended: + + OpenSSH http://www.openssh.com/ Package names: initramfs-tools libgnutls-dev libavahi-core-dev gnupg - libgpgme11-dev + libgpgme11-dev pkg-config ssh * Installing the Mandos server @@ -98,15 +107,11 @@ and append this to the file "/etc/mandos/clients.conf" *on the server computer*. - 4. Configure the client to use the correct network interface. The - interface to use is automatically chosen at boot, and if this - needs to be adjusted, it will be necessary to edit - /etc/initramfs-tools/initramfs.conf to change the DEVICE setting - there. Alternatively, the file /etc/mandos/plugin-runner.conf - can be edited to add a "--device" parameter for the - mandos-client(8) plugin. Please note: If any of those files are - changed, the initrd.img file must be updated, possibly using the - following command: + 4. Configure the client to use any special configuration needed for + your local system. Note: This is not necessary if the server is + present on the same wired local network as the client. If you do + make changes to /etc/mandos/plugin-runner.conf, the initrd.img + file must be updated, possibly using the following command: # update-initramfs -k all -u @@ -126,12 +131,14 @@ After this, the client computer should be able to reboot without needing a password entered on the console, as long as it does not - take more than an hour to reboot. + take more than five minutes to reboot. * Further customizations You may want to tighten or loosen the timeouts in the server configuration files; see mandos.conf(5) and mandos-clients.conf(5). - If IPsec is not used, it is suggested that a more cryptographically - secure checker program is used and configured, since without IPsec - ping packets can be faked. + If IPsec is not used and SSH is not installed, it is suggested that + a more cryptographically secure checker program is used and + configured, since, without IPsec, ping packets can be faked. + +#+STARTUP: showall === modified file 'Makefile' --- Makefile 2011-10-15 16:48:03 +0000 +++ Makefile 2014-10-05 20:08:58 +0000 @@ -1,14 +1,17 @@ -WARN=-O -Wall -Wformat=2 -Winit-self -Wmissing-include-dirs \ - -Wswitch-default -Wswitch-enum -Wunused-parameter \ - -Wstrict-aliasing=1 -Wextra -Wfloat-equal -Wundef -Wshadow \ +WARN=-O -Wall -Wextra -Wdouble-promotion -Wformat=2 -Winit-self \ + -Wmissing-include-dirs -Wswitch-default -Wswitch-enum \ + -Wunused -Wuninitialized -Wstrict-overflow=5 \ + -Wsuggest-attribute=pure -Wsuggest-attribute=const \ + -Wsuggest-attribute=noreturn -Wfloat-equal -Wundef -Wshadow \ -Wunsafe-loop-optimizations -Wpointer-arith \ -Wbad-function-cast -Wcast-qual -Wcast-align -Wwrite-strings \ - -Wconversion -Wstrict-prototypes -Wold-style-definition \ - -Wpacked -Wnested-externs -Winline -Wvolatile-register-var -# -Wunreachable-code + -Wconversion -Wlogical-op -Waggregate-return \ + -Wstrict-prototypes -Wold-style-definition \ + -Wmissing-format-attribute -Wnormalized=nfc -Wpacked \ + -Wredundant-decls -Wnested-externs -Winline -Wvla \ + -Wvolatile-register-var -Woverlength-strings #DEBUG=-ggdb3 -# For info about _FORTIFY_SOURCE, see -# +# For info about _FORTIFY_SOURCE, see feature_test_macros(7) # and . FORTIFY=-D_FORTIFY_SOURCE=2 -fstack-protector-all -fPIC LINK_FORTIFY_LD=-z relro -z now @@ -20,18 +23,23 @@ LINK_FORTIFY += -pie endif #COVERAGE=--coverage -OPTIMIZE=-Os +OPTIMIZE=-Os -fno-strict-aliasing LANGUAGE=-std=gnu99 htmldir=man -version=1.4.1 +version=1.6.9 SED=sed +USER=$(firstword $(subst :, ,$(shell getent passwd _mandos || getent passwd nobody || echo 65534))) +GROUP=$(firstword $(subst :, ,$(shell getent group _mandos || getent group nobody || echo 65534))) + ## Use these settings for a traditional /usr/local install # PREFIX=$(DESTDIR)/usr/local # CONFDIR=$(DESTDIR)/etc/mandos # KEYDIR=$(DESTDIR)/etc/mandos/keys # MANDIR=$(PREFIX)/man # INITRAMFSTOOLS=$(DESTDIR)/etc/initramfs-tools +# STATEDIR=$(DESTDIR)/var/lib/mandos +# LIBDIR=$(PREFIX)/lib ## ## These settings are for a package-type install @@ -40,8 +48,20 @@ KEYDIR=$(DESTDIR)/etc/keys/mandos MANDIR=$(PREFIX)/share/man INITRAMFSTOOLS=$(DESTDIR)/usr/share/initramfs-tools +STATEDIR=$(DESTDIR)/var/lib/mandos +LIBDIR=$(shell \ + for d in \ + "/usr/lib/`dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null`" \ + "`rpm --eval='%{_libdir}' 2>/dev/null`" /usr/lib; do \ + if [ -d "$$d" -a "$$d" = "$${d%/}" ]; then \ + echo "$(DESTDIR)$$d"; \ + break; \ + fi; \ + done) ## +SYSTEMD=$(DESTDIR)$(shell pkg-config systemd --variable=systemdsystemunitdir) + GNUTLS_CFLAGS=$(shell pkg-config --cflags-only-I gnutls) GNUTLS_LIBS=$(shell pkg-config --libs gnutls) AVAHI_CFLAGS=$(shell pkg-config --cflags-only-I avahi-core) @@ -51,10 +71,10 @@ getconf LFS_LDFLAGS) # Do not change these two -CFLAGS=$(WARN) $(DEBUG) $(FORTIFY) $(COVERAGE) $(OPTIMIZE) \ +CFLAGS+=$(WARN) $(DEBUG) $(FORTIFY) $(COVERAGE) $(OPTIMIZE) \ $(LANGUAGE) $(GNUTLS_CFLAGS) $(AVAHI_CFLAGS) $(GPGME_CFLAGS) \ -DVERSION='"$(version)"' -LDFLAGS=-Xlinker --as-needed $(COVERAGE) $(LINK_FORTIFY) $(foreach flag,$(LINK_FORTIFY_LD),-Xlinker $(flag)) +LDFLAGS+=-Xlinker --as-needed $(COVERAGE) $(LINK_FORTIFY) $(foreach flag,$(LINK_FORTIFY_LD),-Xlinker $(flag)) # Commands to format a DocBook document into a manual page DOCBOOKTOMAN=$(strip cd $(dir $<); xsltproc --nonet --xinclude \ @@ -63,13 +83,12 @@ --param make.single.year.ranges 1 \ --param man.output.quietly 1 \ --param man.authors.section.enabled 0 \ - /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl \ + /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl \ $(notdir $<); \ - $(MANPOST) $(notdir $@);\ - LANG=en_US.UTF-8 MANWIDTH=80 man --warnings --encoding=UTF-8 \ - --local-file $(notdir $@) >/dev/null) -# DocBook-to-man post-processing to fix a '\n' escape bug -MANPOST=$(SED) --in-place --expression='s,\\\\en,\\en,g;s,\\n,\\en,g' + if locale --all 2>/dev/null | grep --regexp='^en_US\.utf8$$' \ + && type man 2>/dev/null; then LANG=en_US.UTF-8 MANWIDTH=80 \ + man --warnings --encoding=UTF-8 --local-file $(notdir $@); \ + fi >/dev/null) DOCBOOKTOHTML=$(strip xsltproc --nonet --xinclude \ --param make.year.ranges 1 \ @@ -220,9 +239,11 @@ $(LINK.c) $^ -lrt $(GNUTLS_LIBS) $(AVAHI_LIBS) $(strip\ ) $(GPGME_LIBS) $(LOADLIBES) $(LDLIBS) -o $@ -.PHONY : all doc html clean distclean run-client run-server install \ - install-server install-client uninstall uninstall-server \ - uninstall-client purge purge-server purge-client +.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 \ + uninstall-server uninstall-client purge purge-server \ + purge-client clean: -rm --force $(CPROGS) $(objects) $(htmldocs) $(DOCS) core @@ -230,10 +251,11 @@ distclean: clean mostlyclean: clean maintainer-clean: clean - -rm --force --recursive keydir confdir + -rm --force --recursive keydir confdir statedir check: all ./mandos --check + ./mandos-ctl --check # Run the client with a local config and key run-client: all keydir/seckey.txt keydir/pubkey.txt @@ -242,15 +264,18 @@ @echo "# ignored. The messages are caused by not running as root, but #" @echo "# you should NOT run \"make run-client\" as root unless you also #" @echo "# unpacked and compiled Mandos as root, which is NOT recommended. #" - @echo "# From plugin-runner: setuid: Operation not permitted #" + @echo "# From plugin-runner: setgid: Operation not permitted #" + @echo "# setuid: Operation not permitted #" @echo "# From askpass-fifo: mkfifo: Permission denied #" - @echo "# From mandos-client: setuid: Operation not permitted #" - @echo "# seteuid: Operation not permitted #" - @echo "# klogctl: Operation not permitted #" + @echo "# From mandos-client: #" + @echo "# Failed to raise privileges: Operation not permitted #" + @echo "# Warning: network hook \"*\" exited with status * #" @echo "###################################################################" +# We set GNOME_KEYRING_CONTROL to block pam_gnome_keyring ./plugin-runner --plugin-dir=plugins.d \ --config-file=plugin-runner.conf \ - --options-for=mandos-client:--seckey=keydir/seckey.txt,--pubkey=keydir/pubkey.txt \ + --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= \ $(CLIENTARGS) # Used by run-client @@ -259,14 +284,9 @@ ./mandos-keygen --dir keydir --force # Run the server with a local config -run-server: confdir/mandos.conf confdir/clients.conf - @echo "#################################################################" - @echo "# NOTE: Please IGNORE the error about \"Could not open file #" - @echo "# u'/var/run/mandos.pid'\" - it is harmless and is caused by #" - @echo "# the server not running as root. Do NOT run \"make run-server\" #" - @echo "# server as root if you didn't also unpack and compile it thus. #" - @echo "#################################################################" - ./mandos --debug --no-dbus --configdir=confdir $(SERVERARGS) +run-server: confdir/mandos.conf confdir/clients.conf statedir + ./mandos --debug --no-dbus --configdir=confdir \ + --statedir=statedir $(SERVERARGS) # Used by run-server confdir/mandos.conf: mandos.conf @@ -276,7 +296,9 @@ install --directory confdir install --mode=u=rw $< $@ # Add a client password - ./mandos-keygen --dir keydir --password >> $@ + ./mandos-keygen --dir keydir --password --no-ssh >> $@ +statedir: + install --directory statedir install: install-server install-client-nokey @@ -287,6 +309,12 @@ install-server: doc install --directory $(CONFDIR) + if install --directory --mode=u=rwx --owner=$(USER) \ + --group=$(GROUP) $(STATEDIR); then \ + :; \ + elif install --directory --mode=u=rwx $(STATEDIR); then \ + chown -- $(USER):$(GROUP) $(STATEDIR) || :; \ + fi install --mode=u=rwx,go=rx mandos $(PREFIX)/sbin/mandos install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ mandos-ctl @@ -300,6 +328,9 @@ $(DESTDIR)/etc/dbus-1/system.d/mandos.conf install --mode=u=rwx,go=rx init.d-mandos \ $(DESTDIR)/etc/init.d/mandos + if [ "$(SYSTEMD)" != "$(DESTDIR)" -a -d "$(SYSTEMD)" ]; then \ + install --mode=u=rw,go=r mandos.service $(SYSTEMD); \ + fi install --mode=u=rw,go=r default-mandos \ $(DESTDIR)/etc/default/mandos if [ -z $(DESTDIR) ]; then \ @@ -315,36 +346,40 @@ > $(MANDIR)/man5/mandos.conf.5.gz gzip --best --to-stdout mandos-clients.conf.5 \ > $(MANDIR)/man5/mandos-clients.conf.5.gz + gzip --best --to-stdout intro.8mandos \ + > $(MANDIR)/man8/intro.8mandos.gz install-client-nokey: all doc - install --directory $(PREFIX)/lib/mandos $(CONFDIR) + install --directory $(LIBDIR)/mandos $(CONFDIR) install --directory --mode=u=rwx $(KEYDIR) \ - $(PREFIX)/lib/mandos/plugins.d - if [ "$(CONFDIR)" != "$(PREFIX)/lib/mandos" ]; then \ + $(LIBDIR)/mandos/plugins.d + if [ "$(CONFDIR)" != "$(LIBDIR)/mandos" ]; then \ install --mode=u=rwx \ --directory "$(CONFDIR)/plugins.d"; \ fi + install --mode=u=rwx,go=rx --directory \ + "$(CONFDIR)/network-hooks.d" install --mode=u=rwx,go=rx \ - --target-directory=$(PREFIX)/lib/mandos plugin-runner + --target-directory=$(LIBDIR)/mandos plugin-runner install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ mandos-keygen install --mode=u=rwx,go=rx \ - --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ plugins.d/password-prompt install --mode=u=rwxs,go=rx \ - --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ plugins.d/mandos-client install --mode=u=rwxs,go=rx \ - --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ plugins.d/usplash install --mode=u=rwxs,go=rx \ - --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ plugins.d/splashy install --mode=u=rwxs,go=rx \ - --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ plugins.d/askpass-fifo install --mode=u=rwxs,go=rx \ - --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + --target-directory=$(LIBDIR)/mandos/plugins.d \ plugins.d/plymouth install initramfs-tools-hook \ $(INITRAMFSTOOLS)/hooks/mandos @@ -396,13 +431,13 @@ ! grep --regexp='^ *[^ #].*keyscript=[^,=]*/mandos/' \ $(DESTDIR)/etc/crypttab -rm --force $(PREFIX)/sbin/mandos-keygen \ - $(PREFIX)/lib/mandos/plugin-runner \ - $(PREFIX)/lib/mandos/plugins.d/password-prompt \ - $(PREFIX)/lib/mandos/plugins.d/mandos-client \ - $(PREFIX)/lib/mandos/plugins.d/usplash \ - $(PREFIX)/lib/mandos/plugins.d/splashy \ - $(PREFIX)/lib/mandos/plugins.d/askpass-fifo \ - $(PREFIX)/lib/mandos/plugins.d/plymouth \ + $(LIBDIR)/mandos/plugin-runner \ + $(LIBDIR)/mandos/plugins.d/password-prompt \ + $(LIBDIR)/mandos/plugins.d/mandos-client \ + $(LIBDIR)/mandos/plugins.d/usplash \ + $(LIBDIR)/mandos/plugins.d/splashy \ + $(LIBDIR)/mandos/plugins.d/askpass-fifo \ + $(LIBDIR)/mandos/plugins.d/plymouth \ $(INITRAMFSTOOLS)/hooks/mandos \ $(INITRAMFSTOOLS)/conf-hooks.d/mandos \ $(INITRAMFSTOOLS)/scripts/init-premount/mandos \ @@ -414,8 +449,8 @@ $(MANDIR)/man8/splashy.8mandos.gz \ $(MANDIR)/man8/askpass-fifo.8mandos.gz \ $(MANDIR)/man8/plymouth.8mandos.gz \ - -rmdir $(PREFIX)/lib/mandos/plugins.d $(CONFDIR)/plugins.d \ - $(PREFIX)/lib/mandos $(CONFDIR) $(KEYDIR) + -rmdir $(LIBDIR)/mandos/plugins.d $(CONFDIR)/plugins.d \ + $(LIBDIR)/mandos $(CONFDIR) $(KEYDIR) update-initramfs -k all -u purge: purge-server purge-client @@ -425,6 +460,8 @@ $(DESTDIR)/etc/dbus-1/system.d/mandos.conf $(DESTDIR)/etc/default/mandos \ $(DESTDIR)/etc/init.d/mandos \ + $(SYSTEMD)/mandos.service \ + $(DESTDIR)/run/mandos.pid \ $(DESTDIR)/var/run/mandos.pid -rmdir $(CONFDIR) === modified file 'NEWS' --- NEWS 2011-10-15 16:48:03 +0000 +++ NEWS 2014-10-05 20:08:58 +0000 @@ -1,6 +1,150 @@ This NEWS file records noteworthy changes, very tersely. See the manual for detailed information. +Version 1.6.9 (2014-10-05) +* Server +** Changed to emit standard D-Bus signal when D-Bus properties change. + (The old signal is still emitted too, but marked as deprecated.) + +Version 1.6.8 (2014-08-06) +* Client +** Bug fix: mandos-keygen now generates working SSH checker commands. +* Server +** Bug fix: "mandos-monitor" now really redraws screen on Ctrl-L. +** Now requires Python 2.7. + +Version 1.6.7 (2014-07-17) +* Client +** Bug fix: Now compatible with GPGME 1.5.0. +** Bug fix: Fixed minor memory leaks. +* Server +** "mandos-monitor" now has verbose logging, toggleable with "v". + +Version 1.6.6 (2014-07-13) +* Client +** If client host has an SSH server, "mandos-keygen --password" now + outputs "checker" option which uses "ssh-keyscan"; this is more + secure than the default "fping" checker. +** Bug fix: allow "." in network hook names, to match documentation. +** Better error messages. +* Server +** New --no-zeroconf option. +** Bug fix: Fix --servicename option, broken since 1.6.4. +** Bug fix: Fix --socket option work for --socket=0. + +Version 1.6.5 (2014-05-11) +* Client +** Work around bug in GnuPG +** Give better error messages when run without sufficient privileges +** Only warn if workaround for Debian bug #633582 was necessary and + failed, not if it failed and was unnecessary. + +Version 1.6.4 (2014-02-16) +* Server +** Very minor fix to self-test code. + +Version 1.6.3 (2014-01-21) +* Server +** Add systemd support. +** For PID file, fall back to /var/run if /run does not exist. +* Client +** Moved files from /usr/lib/mandos to whatever the architecture + specifies, like /usr/lib/x86_64-linux-gnu/mandos or + /usr/lib64/mandos. + +Version 1.6.2 (2013-10-24) +* Server +** PID file moved from /var/run to /run. +** Bug fix: Handle long secrets when saving client state. +** Bug fix: Use more magic in the GnuTLS priority string to handle + both old DSA/ELG 2048-bit keys and new RSA/RSA 4096-bit keys. +* Client +** mandos-keygen: Bug fix: now generate RSA keys which GnuTLS can use. + Bug fix: Output passphrase prompts even when + redirecting standard output. + +Version 1.6.1 (2013-10-13) +* Server +** All client options for time intervals now also take an RFC 3339 + duration. The same for all options to mandos-ctl. +** Bug fix: Handle fast checkers (like ":") correctly. +** Bug fix: Don't print output from checkers when running in + foreground. +** Bug fix: Do not fail when client is removed from clients.conf but + saved settings remain. +** Bug fix: mandos-monitor now displays standout (reverse video) again + using new version of Urwid. +** Bug fix: Make boolean options work from the config file again. +** Bug fix: Make --no-ipv6 work again. +** New default priority string to be slightly more compatible with + older versions of GnuTLS. +* Client +** Bug fix: Fix bashism in mandos-keygen. +** Default key and subkey types are now RSA and RSA, respectively. + Also, new default key size is 4096 bits. + +Version 1.6.0 (2012-06-18) +* Server +** Takes new --foreground option +** Init script supports new "status" action. +* Client +** Now uses all interfaces by default; the --interface option can + still be used to restrict it, and the argument to --interface (as + well as the $DEVICE environment variable for the network hooks) is + now a comma-separated list of interfaces to use. + +Version 1.5.5 (2012-06-01) +* Server +** Server takes new --socket option + +Version 1.5.4 (2012-05-20) +* Server +** Bug fix: Regression fix: Make non-zero approval timeout values work. +** Bug fix: Regression fix: Allow changing the Timeout D-Bus property. +** Fall back to not bind to an interface if an invalid interface name + is given. +** Removed support for undocumented feature of using plain "%%s" in + "checker" client option. +** Old D-Bus interface are now marked as deprecated. +** mandos-monitor: Bug fix: show approval timers correctly. +** mandos-ctl: Show "Extended Timeout" correctly, not as milliseconds. + +Version 1.5.3 (2012-01-15) +* Server +** Add D-Bus property se.recompile.Client.LastCheckerStatus and use it + in mandos-monitor. +* Client +** Fix bugs in the example "bridge" network hook. + +Version 1.5.2 (2012-01-08) +* Server +** Removed D-Bus signal se.recompile.Mandos.NewRequest() added in + 1.5.0. It was buggy and was of questionable utility. + +Version 1.5.1 (2012-01-01) +* Server +** Include intro(8mandos) manual page, missing since migration from + README file in version 1.4.0. + +Version 1.5.0 (2012-01-01) +* Client +** Network hooks. The Mandos client can now run custom scripts to take + up a network interface before the client is run. Three example + scripts are provided: "wireless", "openvpn", and "bridge". + To facilitate this, the client now prefers network interfaces which + are up (if any) over all other interfaces. +* Server +** Persistent state. Client state is now saved between server + restarts. +** clients.conf file can now contain "enabled" setting for clients. +** Bug fix: Fix rare crash bug. +** Bug fix: Send corrent D-Bus type in PropertyChanged for + "ApprovalDelay", "ApprovalDuration", "Timeout", and + "ExtendedTimeout". +** mandos-ctl: Bare numbers as arguments are taken to be milliseconds. +** Bug fix: mandos-ctl --secret option now works. +** New D-Bus signal: se.recompile.Mandos.NewRequest(s). + Version 1.4.1 (2011-10-15) * Server ** Make D-Bus properties settable again, and handle checkers === modified file 'README' --- README 2011-10-05 16:00:56 +0000 +++ README 2012-01-01 20:45:53 +0000 @@ -1,7 +1,8 @@ Please see: http://www.recompile.se/mandos/man/intro.8mandos This information previously in this file has been moved to the -intro(8mandos) manual page. Go to the above URL, or run this command: +intro(8mandos) manual page. Go to the above URL, or install the +Mandos server and run this command: man 8mandos intro === modified file 'TODO' --- TODO 2011-11-13 13:55:45 +0000 +++ TODO 2015-05-22 20:23:46 +0000 @@ -1,18 +1,31 @@ -*- org -*- -* Use _attribute_((nonnull)) wherever possible. +* 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]] +** 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 + tailor? + Using bzr-fastimport: [[http://www.fusonic.net/en/blog/2013/03/26/migrating-from-bazaar-to-git/][Migrating from Bazaar to Git]] +** Unresolved: [[http://jameswestby.net/bzr/builddeb/user_manual/split.html][bzr builddeb split mode]] + Maybe: [[http://honk.sigxcpu.org/projects/git-buildpackage/manual-html/gbp.import.html#GBP.IMPORT.UPSTREAM.GIT.NOTARBALL][git-buildpackage - No upstream tarballs]] + [[http://www.python.org/dev/peps/pep-0374/][PEP 374 - Choosing a distributed VCS for the Python project]] + [[http://www.emacswiki.org/emacs/GitForEmacsDevs][Git For Emacs Devs]] * [[http://www.undeadly.org/cgi?action=article&sid=20110530221728][OpenBSD]] +* Testing +** python-nemu + * mandos-applet * mandos-client -** TODO [#A] Document network hook functionality +** TODO [#A] --dh-params=FILE ** TODO [#B] Use capabilities instead of seteuid(). -** TODO [#B] Use struct sockaddr_storage instead of a union + https://forums.grsecurity.net/viewtopic.php?f=7&t=2522 ** TODO [#B] Use getaddrinfo(hints=AI_NUMERICHOST) instead of inet_pton() -** TODO [#B] Use getnameinfo(serv=NULL, NI_NUMERICHOST) instead of inet_ntop() -** TODO [#B] Prefer /run/tmp over /tmp, if it exists +** TODO [#C] Make start_mandos_communication() take "struct server". +** TODO [#C] --interfaces=regex,eth*,noregex (bridge-utils-interfaces(5)) * splashy ** TODO [#B] use scandir(3) instead of readdir(3) @@ -20,10 +33,8 @@ * usplash (Deprecated) ** TODO [#A] Make it work again ** TODO [#B] use scandir(3) instead of readdir(3) -** TODO Use [[info:libc:Argz%20Functions][argz_extract]] * askpass-fifo -** TODO [#B] Drop privileges after opening FIFO. * password-prompt ** TODO [#B] lock stdin (with flock()?) @@ -34,47 +45,54 @@ * plugin-runner ** TODO handle printing for errors for plugins -*** Hook up stderr of plugins, buffer them, and prepend mandos pluig [plugin name] -** TODO [#B] use scandir(3) instead of readdir(3) +*** 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 [#B] Use openat() * 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 Persistent state :BUGS: - /var/lib/mandos/* *** TODO /etc/mandos/clients.d/*.conf Watch this directory and add/remove/update clients? ** TODO [#C] config for TXT record -** TODO Log level option - syslogger.setLevel(logging.WARNING) - + SetLogLevel D-Bus call -** TODO Implement --foreground :BUGS: - [[info:standards:Option%20Table][Table of Long Options]] -** TODO Implement --socket - [[info:standards:Option%20Table][Table of Long Options]] -** TODO Date+time on console log messages :BUGS: - Is this the default? +** TODO Log level dbus option + SetLogLevel D-Bus call ** TODO [#C] DBusServiceObjectUsingSuper ** TODO [#B] Global enable/disable flag -** TODO [#B] By-client countdown on secrets given -** TODO [#B] Support RFC 3339 time duration syntax -** More D-Bus methods -*** NeedsPassword(50) - Timeout, default disapprove +** TODO [#B] By-client countdown on number of secrets given +** D-Bus Client method NeedsPassword(50) - Timeout, default disapprove + SetPass(u"gazonk", True) -> Approval, persistent + Approve(False) -> Close client connection immediately ** TODO [#C] python-parsedatetime -** TODO [#C] systemd/launchd - http://0pointer.de/blog/projects/systemd.html - http://wiki.debian.org/systemd ** TODO Separate logging logic to own object -** TODO make clients to a dict! ** TODO [#A] 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? + http://standards.freedesktop.org/secret-service/ +** TODO Remove D-Bus interfaces with old domain name :2: +** TODO Remove old string_to_delta format :2: +** TODO http://0pointer.de/blog/projects/stateless.html +*** tmpfiles snippet to create /var/lib/mandos with right user+perms +*** File in /usr/lib/sysusers.d to create user+group "_mandos" +** 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 @@ -82,8 +100,7 @@ * mandos-ctl *** Handle "no D-Bus server" and/or "no Mandos server found" better *** [#B] --dump option -** TODO Support RFC 3339 time duration syntax -** TODO Send milliseconds if bare integer is passed as time duration +** TODO Remove old string_to_delta format :2: * TODO mandos-dispatch Listens for specified D-Bus signals and spawns shell commands with @@ -94,9 +111,8 @@ ** Urwid client data displayer Better view of client data in the listing *** Properties popup -** Nicer crashes. Stack traces Messes up shell. -*** Print a nice "We are sorry" message, save stack trace to log. -** Show timeout countdown for approval +** Print a nice "We are sorry" message, save stack trace to log. +** Rename module "gobject" to "GObject". * mandos-keygen ** TODO "--secfile" option @@ -104,10 +120,6 @@ ** TODO [#B] "--test" option For testing decryption before rebooting. -* Makefile -** TODO [#C] Implement DEB_BUILD_OPTIONS - http://www.debian.org/doc/debian-policy/ch-source.html#s-debianrules-options - * Package ** /usr/share/initramfs-tools/hooks/mandos *** TODO [#C] use same file name rules as run-parts(8) === modified file 'clients.conf' --- clients.conf 2011-09-19 09:42:55 +0000 +++ clients.conf 2012-06-23 00:58:49 +0000 @@ -4,19 +4,19 @@ # How long until a client is disabled and not be allowed to get the # data this server holds. -;timeout = 5m +;timeout = PT5M # How often to run the checker to confirm that a client is still up. # Note: a new checker will not be started if an old one is still # running. The server will wait for a checker to complete until the # above "timeout" occurs, at which time the client will be disabled, # and any running checker killed. -;interval = 2m +;interval = PT2M # Extended timeout is an added timeout that is given once after a # password has been sent sucessfully to a client. This allows for # additional delays caused by file system checks and quota checks. -;extended_timeout = 15m +;extended_timeout = PT15M # What command to run as "the checker". ;checker = fping -q -- %%(host)s @@ -25,10 +25,13 @@ ;approved_by_default = True # How long to wait for approval. -;approval_delay = 0s +;approval_delay = PT0S # How long one approval will last. -;approval_duration = 1s +;approval_duration = PT1S + +# Whether this client is enabled by default +;enabled = True ;#### @@ -75,10 +78,10 @@ ;host = 192.0.2.3 ; ;# Parameters from the [DEFAULT] section can be overridden per client. -;interval = 1m +;interval = PT1M ; ;# This client requires manual approval before it receives its secret. ;approved_by_default = False ;# Require approval within 30 seconds. -;approval_delay = 30s +;approval_delay = PT30S ;#### === modified file 'common.ent' --- common.ent 2011-10-15 16:48:03 +0000 +++ common.ent 2014-10-05 20:08:58 +0000 @@ -1,3 +1,3 @@ - + === modified file 'debian/changelog' --- debian/changelog 2011-10-15 16:48:03 +0000 +++ debian/changelog 2014-10-05 20:08:58 +0000 @@ -1,3 +1,212 @@ +mandos (1.6.9-1) unstable; urgency=medium + + * New upstream release. + * debian/control (Build-Depends): Fix "still uses GnutLS 2.x" by + changing from "libgnutls-dev" to "libgnutls28-dev | gnutls-dev" + (Closes: #762349) + + -- Teddy Hogeborn Sun, 05 Oct 2014 22:05:06 +0200 + +mandos (1.6.8-1) unstable; urgency=medium + + * New upstream release. + * debian/control (Source: mandos/Build-Depends-Indep): Since upstream + now requires Python 2.7, depend on exactly the python2.7 package and + all the Python 2.7 versions of the python modules. + (Package: mandos/Depends): - '' - but still depend on python (>=2.7) + and the generic versions of the Python modules; this is for mandos-ctl + and mandos-monitor, both of which are compatible with Python 3, and + use #!/usr/bin/python. + + -- Teddy Hogeborn Wed, 06 Aug 2014 22:55:24 +0200 + +mandos (1.6.7-1) unstable; urgency=medium + + * New upstream release. + + -- Teddy Hogeborn Thu, 17 Jul 2014 05:22:45 +0200 + +mandos (1.6.6-1) unstable; urgency=medium + + * New upstream release. + * debian/mandos.postinst: Fix typo in comment. + * debian/control (mandos/Recommends): Changed to "ssh-client | fping". + (mandos-client/Recommends): New; set to "ssh". + + -- Teddy Hogeborn Sun, 13 Jul 2014 22:49:21 +0200 + +mandos (1.6.5-3) unstable; urgency=medium + + * debian/control (mandos-client/Depends): Add "dpkg-dev (>=1.16.0)"; + initramfs-tools-hook runs "dpkg-architecture -qDEB_HOST_MULTIARCH". + (Closes: #750221) + + -- Teddy Hogeborn Fri, 06 Jun 2014 04:27:15 +0200 + +mandos (1.6.5-2) unstable; urgency=medium + + * debian/rules (override_dh_auto_test-arch): New; does nothing. Fixes + FTBFS for build-indep. + + -- Teddy Hogeborn Tue, 13 May 2014 08:08:31 +0200 + +mandos (1.6.5-1) unstable; urgency=medium + + * New upstream release. + * debian/copyright: Change year to "2014". + * debian/control (Build-Depends, Build-Depends-Indep): Moved build + dependencies of "mandos" package to "Build-Depends-Indep". + * debian/upstream/signing-key.asc: New; upstream source public key. + * debian/control (Standards-Version): Updated to "3.9.5". + * debian/control (mandos/Depends): Remove the dependency on + "avahi-daemon (>= 0.6.31-3) | systemd-sysv". It is unnecessary + since we have a workaround in debian/mandos.postinst anyway. + + -- Teddy Hogeborn Sun, 11 May 2014 22:16:33 +0200 + +mandos (1.6.4-1) unstable; urgency=medium + + * New upstream release. + * debian/control (Build-Depends): Add Python dependencies to + successfully run self-tests. + * debian/copyright: GPLv3 now has its own license file - use it. + * debian/watch: Set PGP signature URL. + + -- Teddy Hogeborn Sun, 16 Feb 2014 14:09:25 +0100 + +mandos (1.6.3-1) unstable; urgency=low + + * New upstream release. + * debian/control (Build-Depends): Added "systemd". + * debian/mandos.dirs (lib/systemd/system): New. + * debian/mandos-client.README.Debian: Refer to architecture libdir. + * debian/control (mandos/Depends): Add "avahi-daemon (>= 0.6.31-3) | + systemd-sysv". + * debian/mandos.postinst: If avahi-daemon is version 0.6.31-2 or older, + edit /etc/init.d script headers Required-Start + and Required-Stop to have "avahi" instead of + "avahi-daemon", before insserv(8) sees it. + * debian/mandos-client.lintian-overrides: Libdir changes. + * debian/rules (override_dh_fixperms): - '' - + + -- Teddy Hogeborn Tue, 21 Jan 2014 22:01:30 +0100 + +mandos (1.6.2-1) unstable; urgency=low + + * New upstream release. + * debian/compat: Changed to "9". + * debian/control (Build-Depends): Changed debhelper version to (>= 9). + (Standards-Version): Updated to "3.9.4". + (DM-Upload-Allowed): Removed. + (mandos/Depends): Add "initscripts (>= 2.88dsf-13.3)" to be able to + use the "/run" directory (for mandos.pid). + * debian/copyright (Copyright): Update year. + * Fix "Mandos/gnutls fails to establish connection, "an algorithm that + is not enabled was negotiated"" fixed by upstream. (Closes: #702120) + + -- Teddy Hogeborn Thu, 24 Oct 2013 22:33:40 +0200 + +mandos (1.6.1-1) unstable; urgency=low + + * New upstream release. + * debian/control (mandos/Depends): No longer depends on + python-gnupginterface, but does + depend on gnupg (<< 2). + (Build-Depends): Depend on debhelper 8.9.7 for using "override-*-arch" + and "override-*-indep" targets in debian/rules. + * debian/mandos-client.README: Update Linux documentation link. + * debian/rules: Completely rewritten to use debhelper v7. + * initramfs-tools-hook: Bug fix: Make sure the right version of GnuPG is + copied into the initramfs image. Always assume that GPGME is used to + avoid searching for it since the path might not be /usr/lib. Thanks + to Félix Sipma for the initial bug report, + and also thanks to Dick Middleton for some more + debugging. (Closes: #721903) + * Fix "bashism in /bin/sh script" fixed by upstream. (Closes: #690639) + + -- Teddy Hogeborn Sun, 13 Oct 2013 19:03:23 +0200 + +mandos (1.6.0-1) unstable; urgency=low + + * New upstream release. + * debian/copyright (Copyright): Join the two lines to a single line. + * debian/mandos-client.README.Debian: Update to refer to the new + location of the example network hooks, and the new feature of using + all network interfaces. + * debian/mandos-client.docs (network-hooks.d): Removed. + * debian/mandos-client.examples (network-hooks.d): New. + * debian/rules (binary-common): Added "dh_installexamples". + (binary-common/dh_fixperms): Exclude new location of + "network-hooks.d". + + -- Teddy Hogeborn Mon, 18 Jun 2012 00:15:23 +0200 + +mandos (1.5.5-1) unstable; urgency=low + + * New upstream release. + * debian/copyright (Format): Updated to + "http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/". + * debian/control (Build-Depends): Removed "man, locales-all". + + -- Teddy Hogeborn Fri, 01 Jun 2012 20:30:41 +0200 + +mandos (1.5.4-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sun, 20 May 2012 15:38:34 +0200 + +mandos (1.5.3-1.2) unstable; urgency=low + + * Non-maintainer upload. + * Set Architecture to linux-any. (Closes: #647670) + + -- Robert Millan Sun, 22 Apr 2012 16:22:01 +0200 + +mandos (1.5.3-1.1) unstable; urgency=low + + * Non-maintainer upload. + * Fix "mandos FTBFS on buildds": add build-dependency on locales-all and + pass LC_ALL to dh_auto_build to make sure we have and use the en_US.UTF-8 + locale for manpage creation. + (Closes: #656178) + + -- gregor herrmann Tue, 31 Jan 2012 17:56:05 +0100 + +mandos (1.5.3-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sun, 15 Jan 2012 22:05:54 +0100 + +mandos (1.5.2-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sun, 08 Jan 2012 11:17:20 +0100 + +mandos (1.5.1-1) unstable; urgency=low + + * New upstream release. + + -- Teddy Hogeborn Sun, 01 Jan 2012 21:53:31 +0100 + +mandos (1.5.0-1) unstable; urgency=low + + * New upstream release. + * debian/control (mandos-client/Depends): Added "initramfs-tools". + * debian/mandos-client.README.Debian: Corrected mail address and adjust + wording. + * debian/rules (binary-common): Exclude new nework-hooks.d directory + from dh_fixperms. + * debian/mandos-client.README.Debian: Document network hook facility. + * debian/mandos-client.docs (network-hooks.d): Added. + * debian/mandos.dirs (var/lib/mandos): Added. + * debian/mandos.postinst: Fix ownership of /var/lib/mandos. + * debian/control (mandos/Depends): Added "python-gnupginterface". + + -- Teddy Hogeborn Sun, 01 Jan 2012 05:58:11 +0100 + mandos (1.4.1-1) unstable; urgency=low * New upstream release. === modified file 'debian/compat' --- debian/compat 2008-09-17 00:34:09 +0000 +++ debian/compat 2013-10-20 15:25:09 +0000 @@ -1,1 +1,1 @@ -7 +9 === modified file 'debian/control' --- debian/control 2011-10-11 19:36:00 +0000 +++ debian/control 2014-10-08 21:03:05 +0000 @@ -4,21 +4,24 @@ Maintainer: Mandos Maintainers Uploaders: Teddy Hogeborn , Björn Påhlsson -Build-Depends: debhelper (>= 7), docbook-xml, docbook-xsl, - libavahi-core-dev, libgpgme11-dev, libgnutls-dev, xsltproc, - pkg-config, man -Standards-Version: 3.9.2 +Build-Depends: debhelper (>= 9), docbook-xml, docbook-xsl, + libavahi-core-dev, libgpgme11-dev, libgnutls28-dev + | gnutls-dev, xsltproc, pkg-config +Build-Depends-Indep: systemd, python2.7, python2.7-gnutls, + python2.7-dbus, python2.7-avahi, python2.7-gobject +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 -DM-Upload-Allowed: yes Package: mandos Architecture: all -Depends: ${misc:Depends}, python (>=2.6), python-gnutls, python-dbus, - python-avahi, python-gobject, avahi-daemon, adduser, - python-urwid, python (>=2.7) | python-argparse -Recommends: fping +Depends: ${misc:Depends}, python (>= 2.7), python2.7, python-gnutls, + python2.7-gnutls, python-dbus, python2.7-dbus, python-avahi, + python2.7-avahi, python-gobject, python2.7-gobject, + avahi-daemon, adduser, python-urwid, python2.7-urwid, + gnupg (<< 2), initscripts (>= 2.88dsf-13.3) +Recommends: ssh-client | fping Description: server giving encrypted passwords to Mandos clients This is the server part of the Mandos system, which allows computers to have encrypted root file systems and at the @@ -35,9 +38,10 @@ whereupon the computers can continue booting normally. Package: mandos-client -Architecture: any +Architecture: linux-any Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, cryptsetup, - gnupg (<< 2) + gnupg (<< 2), initramfs-tools, dpkg-dev (>=1.16.0) +Recommends: ssh Breaks: dropbear (<= 0.53.1-1) Enhances: cryptsetup Description: do unattended reboots with an encrypted root file system === modified file 'debian/copyright' --- debian/copyright 2011-10-10 20:29:58 +0000 +++ debian/copyright 2014-03-01 09:39:25 +0000 @@ -1,11 +1,11 @@ -Format: http://anonscm.debian.org/viewvc/dep/web/deps/dep5.mdwn?revision=202 +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Mandos Upstream-Contact: Mandos Source: Files: * -Copyright: Copyright © 2008-2011 Teddy Hogeborn -Copyright: Copyright © 2008-2011 Björn Påhlsson +Copyright: Copyright © 2008-2014 Teddy Hogeborn + Copyright © 2008-2014 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 @@ -22,4 +22,4 @@ . . On Debian systems, the complete text of the GNU General Public - License can be found in "/usr/share/common-licenses/GPL". + License can be found in "/usr/share/common-licenses/GPL-3". === modified file 'debian/mandos-client.README.Debian' --- debian/mandos-client.README.Debian 2011-10-05 16:00:56 +0000 +++ debian/mandos-client.README.Debian 2013-10-28 10:04:05 +0000 @@ -16,7 +16,8 @@ is possible to verify that the correct password will be received by this client by running the command, on the client: - /usr/lib/mandos/plugins.d/mandos-client \ + /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH \ + )/mandos/plugins.d/mandos-client \ --pubkey=/etc/keys/mandos/pubkey.txt \ --seckey=/etc/keys/mandos/seckey.txt; echo @@ -32,25 +33,31 @@ * Specifying a Client Network Interface - At boot time the network interface to use will by default be - automatically detected. If should result in an incorrect interface, - edit the DEVICE setting in the "/etc/initramfs-tools/initramfs.conf" - file. (The default setting is empty, meaning to autodetect the - interface.) *If* the DEVICE setting is changed, it will be - necessary to update the initrd image by running the command + At boot time the network interfaces to use will by default be + automatically detected. If this should result in incorrect + interfaces, edit the DEVICE setting in the + "/etc/initramfs-tools/initramfs.conf" file. (The default setting is + empty, meaning it will autodetect the interface.) *If* the DEVICE + setting is changed, it will be necessary to update the initrd image + by running the command update-initramfs -k all -u - The device can be overridden at boot time on the Linux kernel + The device can also be overridden at boot time on the Linux kernel command line using the sixth colon-separated field of the "ip=" option; for exact syntax, read the documentation in the file - "/usr/share/doc/linux-doc-*/Documentation/filesystems/nfsroot.txt", + "/usr/share/doc/linux-doc-*/Documentation/filesystems/nfs/nfsroot.txt", available in the "linux-doc-*" package. - Note that since this network interface is used in the initial RAM - disk environment, the network interface *must* exist at that stage. - Thus, the interface can *not* be a pseudo-interface such as "br0" or - "tun0"; instead, only real interface (such as "eth0") can be used. + Note that since the network interfaces are used in the initial RAM + disk environment, the network interfaces *must* exist at that stage. + Thus, an interface can *not* be a pseudo-interface such as "br0" or + "tun0"; instead, only real interfaces (such as "eth0") can be used. + This can be overcome by writing a "network hook" program to create + an interface (see mandos-client(8mandos)) and placing it in + "/etc/mandos/network-hooks.d", from where it will be copied into the + initial RAM disk. Example network hook scripts can be found in + "/usr/share/doc/mandos-client/examples/network-hooks.d". * User-Supplied Plugins @@ -84,4 +91,4 @@ work, "--options-for=mandos-client:--connect=
:" needs to be manually added to the file "/etc/mandos/plugin-runner.conf". - -- Teddy Hogeborn , Wed, 5 Oct 2011 17:50:22 +0200 + -- Teddy Hogeborn , Mon, 28 Oct 2013 11:02:26 +0100 === added file 'debian/mandos-client.examples' --- debian/mandos-client.examples 1970-01-01 00:00:00 +0000 +++ debian/mandos-client.examples 2012-06-01 21:48:12 +0000 @@ -0,0 +1,1 @@ +network-hooks.d === modified file 'debian/mandos-client.lintian-overrides' --- debian/mandos-client.lintian-overrides 2010-10-13 06:12:52 +0000 +++ debian/mandos-client.lintian-overrides 2014-01-20 21:50:11 +0000 @@ -2,12 +2,12 @@ # mandos-client binary: non-standard-dir-perm etc/keys/mandos/ 0700 != 0755 -# The directory /usr/lib/mandos/plugins.d contains setuid binaries -# which are not meant to be run outside an initial RAM disk +# The directory /usr/lib//mandos/plugins.d contains setuid +# binaries which are not meant to be run outside an initial RAM disk # environment (except for test purposes). It would be insecure to # allow anyone to run them. # -mandos-client binary: non-standard-dir-perm usr/lib/mandos/plugins.d/ 0700 != 0755 +mandos-client binary: non-standard-dir-perm usr/lib/*/mandos/plugins.d/ 0700 != 0755 # These binaries must be setuid root, since they need root powers, but # are started by plugin-runner(8mandos), which runs all plugins as @@ -15,14 +15,14 @@ # system, but in an initial RAM disk environment. Here they are # protected from non-root access by the directory permissions, above. # -mandos-client binary: setuid-binary usr/lib/mandos/plugins.d/mandos-client 4755 root/root -mandos-client binary: setuid-binary usr/lib/mandos/plugins.d/askpass-fifo 4755 root/root -mandos-client binary: setuid-binary usr/lib/mandos/plugins.d/splashy 4755 root/root -mandos-client binary: setuid-binary usr/lib/mandos/plugins.d/usplash 4755 root/root -mandos-client binary: setuid-binary usr/lib/mandos/plugins.d/plymouth 4755 root/root +mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/mandos-client 4755 root/root +mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/askpass-fifo 4755 root/root +mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/splashy 4755 root/root +mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/usplash 4755 root/root +mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/plymouth 4755 root/root # The directory /etc/mandos/plugins.d can be used by local system # administrators to place plugins in, overriding and complementing -# /usr/lib/mandos/plugins.d, and must be likewise protected. +# /usr/lib//mandos/plugins.d, and must be likewise protected. # mandos-client binary: non-standard-dir-perm etc/mandos/plugins.d/ 0700 != 0755 === modified file 'debian/mandos.dirs' --- debian/mandos.dirs 2010-09-15 17:33:14 +0000 +++ debian/mandos.dirs 2013-10-27 17:42:23 +0000 @@ -4,3 +4,5 @@ etc/default etc/dbus-1/system.d usr/sbin +var/lib/mandos +lib/systemd/system === modified file 'debian/mandos.postinst' --- debian/mandos.postinst 2011-10-10 20:29:58 +0000 +++ debian/mandos.postinst 2014-06-07 20:29:36 +0000 @@ -35,17 +35,28 @@ --disabled-password --gecos "Mandos password system" \ _mandos fi + chown _mandos:_mandos /var/lib/mandos ;; - + abort-upgrade|abort-deconfigure|abort-remove) ;; - + *) echo "$0 called with unknown argument '$1'" 1>&2 exit 1 ;; esac +# Avahi version 0.6.31-2 and older provides "avahi" (instead of +# "avahi-daemon") in its /etc/init.d script header. To make +# insserv(8) happy, we edit our /etc/init.d script header to contain +# the correct string before the code added by dh_installinit calls +# update.rc-d, which calls insserv. +avahi_version="`dpkg-query --showformat='${Version}' --show avahi-daemon`" +if dpkg --compare-versions "$avahi_version" le 0.6.31-2; then + sed --in-place --expression='/^### BEGIN INIT INFO$/,/^### END INIT INFO$/s/^\(# Required-\(Stop\|Start\):.*avahi\)-daemon\>/\1/g' /etc/init.d/mandos +fi + #DEBHELPER# exit 0 === modified file 'debian/rules' --- debian/rules 2010-09-09 18:16:14 +0000 +++ debian/rules 2014-05-12 21:02:38 +0000 @@ -1,106 +1,31 @@ #!/usr/bin/make -f -# Sample debian/rules that uses debhelper. -# -# This file was originally written by Joey Hess and Craig Small. -# As a special exception, when this file is copied by dh-make into a -# dh-make output file, you may use that output file without restriction. -# This special exception was added by Craig Small in version 0.37 of dh-make. -# -# Modified to make a template file for a multi-binary package with separated -# build-arch and build-indep targets by Bill Allombert 2001 - -# Uncomment this to turn on verbose mode. -#export DH_VERBOSE=1 - -# This has to be exported to make some magic below work. -export DH_OPTIONS - -# -pie was broken briefly on the mips and mipsel architectures, see -# -BINUTILS_V := $(shell dpkg-query --showformat='$${Version}' \ - --show binutils) -ifeq (yes,$(shell dpkg --compare-versions $(BINUTILS_V) lt 2.20-3 \ - && dpkg --compare-versions $(BINUTILS_V) ge 2.19.1-1 \ - && echo yes)) - ifneq (,$(strip $(findstring :$(DEB_HOST_ARCH):,:mips:mipsel:) \ - $(findstring :$(DEB_BUILD_ARCH):,:mips:mipsel:))) - BROKEN_PIE := yes - export BROKEN_PIE - endif -endif - -configure: configure-stamp -configure-stamp: - dh_testdir - touch configure-stamp - -build: build-arch build-indep - -build-arch: build-arch-stamp -build-arch-stamp: configure-stamp - dh_auto_build -- all doc - touch $@ - -build-indep: build-indep-stamp -build-indep-stamp: configure-stamp - dh_auto_build -- doc - touch $@ - -clean: - dh_testdir - dh_testroot - rm -f build-arch-stamp build-indep-stamp configure-stamp - dh_auto_clean - dh_clean - -install: install-indep install-arch -install-indep: - dh_testdir - dh_testroot - dh_prep - dh_installdirs --indep - $(MAKE) DESTDIR=$(CURDIR)/debian/mandos install-server - dh_lintian +%: + dh $@ + +override_dh_auto_build-arch: + LC_ALL=en_US.utf8 dh_auto_build -- all doc + +override_dh_auto_build-indep: + LC_ALL=en_US.utf8 dh_auto_build -- doc + +override_dh_installinit-indep: dh_installinit --onlyscripts \ --update-rcd-params="defaults 25 15" - dh_install --indep - -install-arch: - dh_testdir - dh_testroot - dh_prep - dh_installdirs --same-arch - $(MAKE) DESTDIR=$(CURDIR)/debian/mandos-client install-client-nokey - dh_lintian - dh_install --same-arch - -binary-common: - dh_testdir - dh_testroot - dh_installchangelogs - dh_installdocs - dh_link - dh_strip - dh_compress + +override_dh_auto_install-indep: + $(MAKE) DESTDIR=$(CURDIR)/debian/mandos install-server + +override_dh_auto_install-arch: + $(MAKE) DESTDIR=$(CURDIR)/debian/mandos-client \ + install-client-nokey + +override_dh_fixperms: dh_fixperms --exclude etc/keys/mandos \ --exclude etc/mandos/clients.conf \ --exclude etc/mandos/plugins.d \ - --exclude usr/lib/mandos/plugins.d - dh_installdeb - dh_shlibdeps - dh_gencontrol - dh_md5sums - dh_builddeb - -# Build architecture independant packages using the common target. -binary-indep: build-indep install-indep - $(MAKE) -f debian/rules DH_OPTIONS=--indep binary-common - -# Build architecture dependant packages using the common target. -binary-arch: build-arch install-arch - $(MAKE) -f debian/rules DH_OPTIONS=--same-arch binary-common - -binary: binary-arch binary-indep - -.PHONY: build clean binary-indep binary-arch binary install \ - install-indep install-arch configure + --exclude usr/lib/$(shell dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null)/mandos/plugins.d \ + --exclude usr/share/doc/mandos-client/examples/network-hooks.d + chmod --recursive g-w -- \ + "$(CURDIR)/debian/mandos-client/usr/share/doc/mandos-client/examples/network-hooks.d" + +override_dh_auto_test-arch: ; === added directory 'debian/upstream' === added file 'debian/upstream/signing-key.asc' --- debian/upstream/signing-key.asc 1970-01-01 00:00:00 +0000 +++ debian/upstream/signing-key.asc 2014-03-28 22:32:21 +0000 @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: GnuPG v1.4.12 (GNU/Linux) + +mQINBFJQOFYBEACoWsEGlOxVWFUAxOxdd3GDLaqEKkKihJwLp102Ks7JKMd9friR +7+OZuo3U0gdqLU9q1jPJn36J1QbaUTOvcaKtZp+QpUoYJ2OaGtlOY5ML8LSoC0rZ +MIzGYTtvriwpU/YplLNGPl/90KsB2VqjrY1l1he5M8zziWDlPdJxwg8GFvmPWoif +6oo+1iCswL5IdQ6c5MVO53zYu0cgyUSazLsVD5Xzy59lefgtaDydahJpPycf5aEQ +DAoC9fZt2mgG3FLIUCZdXIhZdOJGCMdjLThBnJXYgGbG4rbGLNlI4W/uA5aqa4ME +WYSAcCyX3ucKY/LkXRtC+z5s05e7tZ3Z+uAJy1eDsbhDXgZERye7a/zPWx1tAlzQ +E80Oltjh1uXWjQORyx99a0jK87zjm49YjhYw1ZN6Z0HfSaws4Yj2QOzp9t4B3l7f +DIUYoWBfHW7mseQeQ+t3TwQU5gjFCNu7oDeATqi5A5MksXN0+BcksterbGRBhEyp +CybIEyrZE033jIs407Ool4Kv10cnjc8oy609BXex/dxwcvVr2vQHle4NPUZd+Xhg +zC+9Z4jFwE0M/EPvtyieA/DWQse+TZ5itDGMYDub/GJfv1U61ANOgPIbTEF7iSa9 +5nWmq7zyUy/txmABka842Kt0Vp6ayoKcF8EIXCaDrVfPnXj+JlKf3c2u6wARAQAB +tCxNYW5kb3MgTWFpbnRhaW5lciBUZWFtIDxtYW5kb3NAcmVjb21waWxlLnNlPokC +PQQTAQgAJwUCUlA4VgIbAwUJCWYBgAULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAK +CRByIylzyjTCxETXEACi56jCV9lJNSBbTp0Iet4X/i7Mx0Z8UkFFa3l7o0i4jFQj +CIBrWECDlcxqZziii2dgh7L0ma93vB3rfjfCWeYLcEQw43MFBsd4dHuobrLXTcqU +7n0Zmc8BsXwk5B25CnEYgvlbWX9BCYtxHGRcZzQrqOFjCMKatq0EIIVuWaz5yuCU +V2rEgnr+veTd/rBOE9ez6Ju6xH11Teob5G7pMM0YPKHtZG/J3rvWPw4BDM6Tc60B +G0sTDZNgkrGWxuB8YaLIwVWzliQK/17Jv/0alajyA3cWLWMkcK9Yhi/einzdMRoD +IjnbtoHcSC9g6i0VGelwnMpHlFTwXriXEBSttULarK3iKE4tOv9nxMAEwicqlw14 +X96PPgz6ogtJG7FiwZfy1CQ81Uby+YuIhHZx3ZEyR1TFq70e98EgCuvjMZWMIhSy +gB5Vssfq7c2lXQjltV3ujhK1PD+7/iHlL7t4QRxPDN8fbMS2VPfAdtnWS1K48d5J +D/jP1LrGWS81HIaX8GFVLVw+jSQEu9cn3TFiZxK/4MMsITmlouJdtZWmQ/otMSMl +wiCCZp3dGpRXMmaqR1N8V0nMKshM8mci7bD92ubd/t6cR/G6l+VIp45WyFONvtde +F3ccfmfrKJuroMDfHxPxMf58EroAuWwzJKCPRH4JmwDvSSQoIIAGNL1lOKp6wrkC +DQRSUDhWARAAzN7pbpAu7XLNPODotV/N+JaCFvNAIqTcr9PrbhxiKFCDs9/IExwP +sGENL9GZd1DfoGEgxQ8j3l8VGw9VSeUoN7uMY2NwwbXTilAFkn/S8xnr2zQDRZ+n +EeFSq9MMFxj4Kt1TqVYDbO8vmFfOT3gRCRHeJ+pn4yJeSPau/ndNrmbQ1/Z6vaUG +yfo931ottx7SXZwkHA6jJVFT9rbHTyx9tzOqMKDJiMrx8qKaHpE9B45oHNR0WJJJ +75zoDVuOZ6wAxXZuqBFu2lKPqDTZeawfzcu5qplrm1RPgSOjz6w1A41HBLqGe+v9 +7Twx5wfNMgnKC0V2wUe0xR6hQHQlyZoCwcGyrasiu+v/joZ1p66SSWKjs+LGjoe4 +Lwh6VjZU2x+irVBjcgIoRWf3k4JAef0nGYsm0cFjAnwXac+/CYxVt+7Y4+HG8wPB +oUNZkW+bvdHQouxEClxnccIEgX/AkriJTYDQ4q3tkl2HVE/51R4pdQVLer2a5ov+ +Jwk44DdqzYstMsvqu+iD48hXzADg6HFvofkpct15h463pMaJf99uVVM9ZDNQ6B34 +l3tPX9ZDkIDl6n9dE2Jkaxx1yNVhPXpeMf4EzL+CEdUErVStB66lUkw+tNkuZyVT +ZTQel9h0196+CNSiqAaL8+ZZdbjKKfzlcB4Qnd897XzMfsFQ7mzJ9QsAEQEAAYkC +JQQYAQgADwUCUlA4VgIbDAUJCWYBgAAKCRByIylzyjTCxFmlEACBTOg5NqX63d8D +mwk4smlFPppQBIduxZaMG9HsLcPi3VKTG9Zg6WI6rEdr/4MnoINsudLsEbrQLgRH +2q1Zs+HqIIP5H2/sYHmswyokYB10zKB6gNUUg/GSlcAcrelsHVKx5B8kccWGT5gk +Wo/X0BGMUTOvQ6lJ6YNo1idcQ2ZjsyfZoz3G8JS7/EXN//jAZf+017yj8WsAS7hw +JRFMy7VET4g00JcBoNOAMP7PkozimZ2OwwsggJSYWkR1RaU2tKR1VmDF8R6UxuEd +BJzwFmz+wNC1Kq+FoSaRNsrKEmzLnfV9unDnF2z7Lc4LqOysXdzOk9zTBPur0gd2 +Lh5H/g5rTAMQBARqXfvIwiTtrBGgil8JW8e4Bc0LQUuHAE7x9gMRil+OtkQrCRk9 +0LWXVS+K0tvvruE4EDtCGiS5046+BEI3aYsp4hNzjHADq0TJeCYjNg9kY0CjxcEq +cfuMoUbQ0MkARGuBbykCdlylfTrkxrj/dPhr49lctY3H+Pj6F4fMDM4TP6UTGA8k +993RRNYhkDWSxIp6G7RJpBZobHN+eHQ3r8A4tWdYb4Fvd2lvwEDjUFT9uD6WAff4 +8A1hM2uSy91UYBOPrIjqYdRFKJc9rThYdXH2T6SiRMYtZMrEKhqPffB/i9mqVBlD +6vKRsaQikZujRdP9Dkf0mLmJ7LANWw== +=9Noe +-----END PGP PUBLIC KEY BLOCK----- === modified file 'debian/watch' --- debian/watch 2011-10-05 16:00:56 +0000 +++ debian/watch 2014-02-16 02:42:42 +0000 @@ -1,2 +1,3 @@ version=3 -ftp://ftp.recompile.se/pub/mandos/mandos[-_]([^\s]+?)(?:\.orig)?\.tar\.(?:gz|bz2|7z|xz) +opts=pgpsigurlmangle=s/$/.asc/ \ + ftp://ftp.recompile.se/pub/mandos/mandos[-_]([^\s]+?)(?:\.orig)?\.tar\.(?:gz|bz2|7z|xz) === modified file 'init.d-mandos' --- init.d-mandos 2011-10-05 16:00:56 +0000 +++ init.d-mandos 2014-01-06 15:56:54 +0000 @@ -1,12 +1,12 @@ #! /bin/sh ### BEGIN INIT INFO # Provides: mandos -# Required-Start: $remote_fs $syslog avahi -# Required-Stop: $remote_fs $syslog avahi +# Required-Start: $remote_fs $syslog avahi-daemon +# Required-Stop: $remote_fs $syslog avahi-daemon # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Mandos server -# Description: Gives encrypted passwords to Mandos clients +# Description: Server of encrypted passwords to Mandos clients ### END INIT INFO # Author: Teddy Hogeborn @@ -23,7 +23,11 @@ NAME=mandos DAEMON=/usr/sbin/$NAME DAEMON_ARGS="" -PIDFILE=/var/run/$NAME.pid +if [ -d /run/. ]; then + PIDFILE=/run/$NAME.pid +else + PIDFILE=/var/run/$NAME.pid +fi SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed @@ -40,7 +44,8 @@ . /lib/init/vars.sh # Define LSB log_* functions. -# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. . /lib/lsb/init-functions # @@ -118,6 +123,9 @@ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; + status) + status_of_proc "$DAEMON" "$NAME" -p "$PIDFILE" && exit 0 || exit $? + ;; #reload|force-reload) # # If do_reload() is not implemented then leave this commented out @@ -144,14 +152,14 @@ esac ;; *) - # Failed to stop + # Failed to stop log_end_msg 1 ;; esac ;; *) #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 - echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac === modified file 'initramfs-tools-hook' --- initramfs-tools-hook 2011-11-13 20:49:21 +0000 +++ initramfs-tools-hook 2014-07-16 23:44:54 +0000 @@ -3,7 +3,7 @@ # This script will be run by 'mkinitramfs' when it creates the image. # Its job is to decide which files to install, then install them into # the staging area, where the initramfs is being created. This -# happens when a new 'linux-image' package is installed, or when the +# happens when a new 'linux-image' package is installed, or when an # administrator runs 'update-initramfs' by hand to update an initramfs # image. @@ -29,13 +29,15 @@ . /usr/share/initramfs-tools/hook-functions -for d in /usr /usr/local; do - if [ -d "$d"/lib/mandos ]; then - prefix="$d" +for d in /usr/lib \ + "/usr/lib/`dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null`" \ + "`rpm --eval='%{_libdir}' 2>/dev/null`" /usr/local/lib; do + if [ -d "$d"/mandos ]; then + libdir="$d" break fi done -if [ -z "$prefix" ]; then +if [ -z "$libdir" ]; then # Mandos not found exit 1 fi @@ -72,17 +74,17 @@ # Make directories install --directory --mode=u=rwx,go=rx "${DESTDIR}${CONFDIR}" \ - "${DESTDIR}${MANDOSDIR}" + "${DESTDIR}${MANDOSDIR}" "${DESTDIR}${HOOKDIR}" install --owner=${mandos_user} --group=${mandos_group} --directory \ --mode=u=rwx "${DESTDIR}${PLUGINDIR}" # Copy the Mandos plugin runner -copy_exec "$prefix"/lib/mandos/plugin-runner "${MANDOSDIR}" +copy_exec "$libdir"/mandos/plugin-runner "${MANDOSDIR}" # Copy the plugins # Copy the packaged plugins -for file in "$prefix"/lib/mandos/plugins.d/*; do +for file in "$libdir"/mandos/plugins.d/*; do base="`basename \"$file\"`" # Is this plugin overridden? if [ -e "/etc/mandos/plugins.d/$base" ]; then @@ -107,30 +109,64 @@ esac done +# Get DEVICE from initramfs.conf and other files +. /etc/initramfs-tools/initramfs.conf +for conf in /etc/initramfs-tools/conf.d/*; do + if [ -n `basename \"$conf\" | grep '^[[:alnum:]][[:alnum:]\._-]*$' \ + | grep -v '\.dpkg-.*$'` ]; then + [ -f ${conf} ] && . ${conf} + fi +done +export DEVICE + # Copy network hooks for hook in /etc/mandos/network-hooks.d/*; do case "`basename \"$hook\"`" in "*") continue ;; - *[!A-Za-z0-9_-]*) continue ;; + *[!A-Za-z0-9_.-]*) continue ;; *) test -d "$hook" || copy_exec "$hook" "${HOOKDIR}" ;; esac - # Copy any files needed by the network hook - MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=files \ - "$hook" files | while read file target; do - if [ -z "${target}" ]; then - copy_exec "$file" - else - copy_exec "$file" "$target" + if [ -x "$hook" ]; then + # Copy any files needed by the network hook + MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=files \ + VERBOSITY=0 "$hook" files | while read file target; do + if [ ! -e "${file}" ]; then + echo "WARNING: file ${file} not found, requested by Mandos network hook '${hook##*/}'" >&2 + fi + if [ -z "${target}" ]; then + copy_exec "$file" + else + copy_exec "$file" "$target" + fi + done + # Copy and load any modules needed by the network hook + MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=modules \ + VERBOSITY=0 "$hook" modules | while read module; do + if [ -z "${target}" ]; then + force_load "$module" + fi + done + fi +done + +# GPGME needs GnuPG +gpg=/usr/bin/gpg +libgpgme11_version="`dpkg-query --showformat='${Version}' --show libgpgme11`" +if dpkg --compare-versions "$libgpgme11_version" ge 1.5.0-0.1; then + if [ -e /usr/bin/gpgconf ]; then + if [ ! -e "${DESTDIR}/usr/bin/gpgconf" ]; then + copy_exec /usr/bin/gpgconf fi - done -done - -# GPGME needs /usr/bin/gpg -if [ ! -e "${DESTDIR}/usr/bin/gpg" \ - -a -n "`ls \"${DESTDIR}\"/usr/lib/libgpgme.so* \ - 2>/dev/null`" ]; then - copy_exec /usr/bin/gpg -fi + gpg="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg:[^:]*://p'`" + fi +elif dpkg --compare-versions "$libgpgme11_version" ge 1.4.1-0.1; then + gpg=/usr/bin/gpg2 +fi +if [ ! -e "${DESTDIR}$gpg" ]; then + copy_exec "$gpg" +fi +unset gpg +unset libgpgme11_version # Config files for file in /etc/mandos/plugin-runner.conf; do === added file 'initramfs-unpack' --- initramfs-unpack 1970-01-01 00:00:00 +0000 +++ initramfs-unpack 2013-10-13 15:43:42 +0000 @@ -0,0 +1,67 @@ +#!/bin/bash +# +# Initramfs unpacker - unpacks initramfs images into /tmp +# +# Copyright © 2013 Teddy Hogeborn +# Copyright © 2013 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 . + +cpio="cpio --extract --make-directories --unconditional --preserve-modification-time" + +if [ -z "$*" ]; then + set -- /boot/initrd.img-* +fi + +for imgfile in "$@"; do + if ! [ -f "$imgfile" ]; then + echo "Error: Not an existing file: $imgfile" >&2 + continue + fi + imgdir="${TMPDIR:-/tmp}/${imgfile##*/}" + if [ -d "$imgdir" ]; then + rm --recursive -- "$imgdir" + fi + mkdir --parents "$imgdir" + # Does this image contain microcode? + if $cpio --quiet --list --file="$imgfile" >/dev/null 2>&1; then + # Number of bytes to skip to get to the compressed archive + skip=$(($(LANG=C $cpio --io-size=1 --list --file="$imgfile" 2>&1 \ + | sed --quiet --expression='s/^\([0-9]\+\) blocks$/\1/p')+8)) + catimg="dd if=$imgfile bs=$skip skip=1 status=noxfer" + else + catimg="cat -- $imgfile" + fi + # Determine the compression method + if { $catimg 2>/dev/null | zcat --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="zcat" + elif { $catimg 2>/dev/null | bzip2 --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="bzip2 --stdout --decompress" + elif { $catimg 2>/dev/null | lzop --test >/dev/null 2>&1; + [ ${PIPESTATUS[-1]} -eq 0 ]; }; then + decomp="lzop --stdout --decompress" + else + echo "Error: Could not determine type of $imgfile" >&2 + continue + fi + $catimg 2>/dev/null | $decomp | ( cd -- "$imgdir" && $cpio --quiet ) + if [ ${PIPESTATUS[-1]} -eq 0 ]; then + echo "$imgfile unpacked into $imgdir" + fi +done === modified file 'intro.xml' --- intro.xml 2011-10-05 16:00:56 +0000 +++ intro.xml 2015-03-10 18:03:38 +0000 @@ -1,7 +1,7 @@ + %common; ]> @@ -31,6 +31,7 @@ 2011 + 2012 Teddy Hogeborn Björn Påhlsson @@ -196,6 +197,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? @@ -214,17 +225,22 @@ - - Faking ping replies? + + Faking checker results? - The default for the server is to use + If the Mandos client does not have an SSH server, the default + is for the Mandos server to use fping, the replies to which could be faked to eliminate the timeout. But this could easily be changed to any shell command, with any security - measures you like. It could, for instance, be changed to an - SSH command with strict keychecking, which could not be faked. - Or IPsec could be used for the ping packets, making them - secure. + measures you like. If the Mandos client + has an SSH server, the default + configuration (as generated by + mandos-keygen with the + option) is for the Mandos server + to use an ssh-keyscan command with strict + keychecking, which can not be faked. Alternatively, IPsec + could be used for the ping packets, making them secure. === modified file 'mandos' --- mandos 2011-10-15 16:48:03 +0000 +++ mandos 2015-05-31 15:56:58 +0000 @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python2.7 # -*- mode: python; coding: utf-8 -*- # # Mandos server - give out binary blobs to connecting clients. @@ -11,8 +11,8 @@ # "AvahiService" class, and some lines in "main". # # Everything else is -# Copyright © 2008-2011 Teddy Hogeborn -# Copyright © 2008-2011 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 @@ -34,7 +34,12 @@ from __future__ import (division, absolute_import, print_function, unicode_literals) -import SocketServer as socketserver +from future_builtins import * + +try: + import SocketServer as socketserver +except ImportError: + import socketserver import socket import argparse import datetime @@ -45,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 @@ -60,13 +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 @@ -82,35 +101,150 @@ except ImportError: SO_BINDTODEVICE = None - -version = "1.4.1" - -#logger = logging.getLogger('mandos') -logger = logging.Logger('mandos') -syslogger = (logging.handlers.SysLogHandler - (facility = logging.handlers.SysLogHandler.LOG_DAEMON, - address = str("/dev/log"))) -syslogger.setFormatter(logging.Formatter - ('Mandos [%(process)d]: %(levelname)s:' - ' %(message)s')) -logger.addHandler(syslogger) - -console = logging.StreamHandler() -console.setFormatter(logging.Formatter('%(name)s [%(process)d]:' - ' %(levelname)s:' - ' %(message)s')) -logger.addHandler(console) +if sys.version_info.major == 2: + str = unicode + +version = "1.6.9" +stored_state_file = "clients.pickle" + +logger = logging.getLogger() +syslogger = None + +try: + 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 + with contextlib.closing(socket.socket()) as s: + ifreq = fcntl.ioctl(s, SIOCGIFINDEX, + struct.pack(b"16s16x", interface)) + interface_index = struct.unpack("I", ifreq[16:20])[0] + return interface_index + + +def initlogger(debug, level=logging.WARNING): + """init logger and add loglevel""" + + global syslogger + syslogger = (logging.handlers.SysLogHandler( + facility = logging.handlers.SysLogHandler.LOG_DAEMON, + address = "/dev/log")) + syslogger.setFormatter(logging.Formatter + ('Mandos [%(process)d]: %(levelname)s:' + ' %(message)s')) + logger.addHandler(syslogger) + + if debug: + console = logging.StreamHandler() + console.setFormatter(logging.Formatter('%(asctime)s %(name)s' + ' [%(process)d]:' + ' %(levelname)s:' + ' %(message)s')) + logger.addHandler(console) + logger.setLevel(level) + + +class PGPError(Exception): + """Exception if encryption/decryption fails""" + pass + + +class PGPEngine(object): + """A simple class for OpenPGP symmetric encryption & decryption""" + + def __init__(self): + self.tempdir = tempfile.mkdtemp(prefix="mandos-") + self.gnupgargs = ['--batch', + '--home', self.tempdir, + '--force-mdc', + '--quiet', + '--no-use-agent'] + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self._cleanup() + return False + + def __del__(self): + self._cleanup() + + def _cleanup(self): + if self.tempdir is not None: + # Delete contents of tempdir + for root, dirs, files in os.walk(self.tempdir, + topdown = False): + for filename in files: + os.remove(os.path.join(root, filename)) + for dirname in dirs: + os.rmdir(os.path.join(root, dirname)) + # Remove tempdir + os.rmdir(self.tempdir) + self.tempdir = None + + def password_encode(self, password): + # Passphrase can not be empty and can not contain newlines or + # NUL bytes. So we prefix it and hex encode it. + encoded = b"mandos" + binascii.hexlify(password) + if len(encoded) > 2048: + # GnuPG can't handle long passwords, so encode differently + encoded = (b"mandos" + password.replace(b"\\", b"\\\\") + .replace(b"\n", b"\\n") + .replace(b"\0", b"\\x00")) + return encoded + + def encrypt(self, data, password): + passphrase = self.password_encode(password) + with tempfile.NamedTemporaryFile( + dir=self.tempdir) as passfile: + passfile.write(passphrase) + passfile.flush() + proc = subprocess.Popen(['gpg', '--symmetric', + '--passphrase-file', + passfile.name] + + self.gnupgargs, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE) + ciphertext, err = proc.communicate(input = data) + if proc.returncode != 0: + raise PGPError(err) + return ciphertext + + def decrypt(self, data, password): + passphrase = self.password_encode(password) + with tempfile.NamedTemporaryFile( + dir = self.tempdir) as passfile: + passfile.write(passphrase) + passfile.flush() + proc = subprocess.Popen(['gpg', '--decrypt', + '--passphrase-file', + passfile.name] + + self.gnupgargs, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.PIPE) + decrypted_plaintext, err = proc.communicate(input = data) + if proc.returncode != 0: + raise PGPError(err) + return decrypted_plaintext + class AvahiError(Exception): def __init__(self, value, *args, **kwargs): self.value = value - super(AvahiError, self).__init__(value, *args, **kwargs) - def __unicode__(self): - return unicode(repr(self.value)) + return super(AvahiError, self).__init__(value, *args, + **kwargs) + class AvahiServiceError(AvahiError): pass + class AvahiGroupError(AvahiError): pass @@ -123,7 +257,7 @@ Used to optionally bind to the specified interface. name: string; Example: 'Mandos' type: string; Example: '_mandos._tcp'. - See + See port: integer; what port to announce TXT: list of strings; TXT record for the service domain: string; Domain to publish on, default to .local if empty. @@ -135,10 +269,18 @@ server: D-Bus Server 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 @@ -153,29 +295,33 @@ self.server = None 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 = unicode(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) - syslogger.setFormatter(logging.Formatter - ('Mandos (%s) [%%(process)d]:' - ' %%(levelname)s: %%(message)s' - % self.name)) - self.remove() + if remove: + self.remove() try: self.add() except dbus.exceptions.DBusException as error: - logger.critical("DBusException: %s", 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""" if self.entry_group_state_changed_match is not None: @@ -183,6 +329,7 @@ self.entry_group_state_changed_match = None if self.group is not None: self.group.Reset() + def add(self): """Derived from the Avahi example code""" self.remove() @@ -193,7 +340,7 @@ avahi.DBUS_INTERFACE_ENTRY_GROUP) self.entry_group_state_changed_match = ( self.group.connect_to_signal( - 'StateChanged', self .entry_group_state_changed)) + 'StateChanged', self.entry_group_state_changed)) logger.debug("Adding Zeroconf service '%s' of type '%s' ...", self.name, self.type) self.group.AddService( @@ -205,6 +352,7 @@ dbus.UInt16(self.port), avahi.string_array_to_txt_array(self.TXT)) self.group.Commit() + def entry_group_state_changed(self, state, error): """Derived from the Avahi example code""" logger.debug("Avahi entry group state change: %i", state) @@ -216,29 +364,29 @@ self.rename() elif state == avahi.ENTRY_GROUP_FAILURE: logger.critical("Avahi: Error in group state changed %s", - unicode(error)) - raise AvahiGroupError("State changed: %s" - % unicode(error)) + str(error)) + raise AvahiGroupError("State changed: {!s}".format(error)) + def cleanup(self): """Derived from the Avahi example code""" if self.group is not None: try: self.group.Free() except (dbus.exceptions.UnknownMethodException, - dbus.exceptions.DBusException) as e: + dbus.exceptions.DBusException): pass self.group = None self.remove() + 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: @@ -253,6 +401,7 @@ logger.debug("Unknown state: %r", state) else: logger.debug("Unknown state: %r: %r", state, error) + def activate(self): """Derived from the Avahi example code""" if self.server is None: @@ -262,21 +411,25 @@ 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()) -def _timedelta_to_milliseconds(td): - "Convert a datetime.timedelta() to milliseconds" - return ((td.days * 24 * 60 * 60 * 1000) - + (td.seconds * 1000) - + (td.microseconds // 1000)) - +class AvahiServiceToSyslog(AvahiService): + def rename(self, *args, **kwargs): + """Add the new name to the syslog messages""" + ret = AvahiService.rename(self, *args, **kwargs) + syslogger.setFormatter(logging.Formatter( + 'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s' + .format(self.name))) + return ret + + class Client(object): """A representation of a client host served by this server. Attributes: - _approved: bool(); 'None' if not yet approved/disapproved + approved: bool(); 'None' if not yet approved/disapproved approval_delay: datetime.timedelta(); Time to wait for approval approval_duration: datetime.timedelta(); Duration of one approval checker: subprocess.Popen(); a running checker process used @@ -289,8 +442,9 @@ instance %(name)s can be used in the command. checker_initiator_tag: a gobject event source tag, or None created: datetime.datetime(); (UTC) object creation + client_structure: Object describing what attributes a client has + and is used for storing the client at exit current_checker_command: string; current running checker_command - disable_hook: If set, called by disable() as disable_hook(self) disable_initiator_tag: a gobject event source tag, or None enabled: bool() fingerprint: string (40 or 32 hexadecimal digits); used to @@ -299,177 +453,221 @@ interval: datetime.timedelta(); How often to start a new checker last_approval_request: datetime.datetime(); (UTC) or None last_checked_ok: datetime.datetime(); (UTC) or None - last_enabled: datetime.datetime(); (UTC) + 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_enabled: datetime.datetime(); (UTC) or None name: string; from the config file, used in log messages and D-Bus identifiers secret: bytestring; sent verbatim (over TLS) to client timeout: datetime.timedelta(); How long from last_checked_ok until this client is disabled - extended_timeout: extra long timeout when password has been sent + extended_timeout: extra long timeout when secret has been sent runtime_expansions: Allowed attributes for runtime expansion. expires: datetime.datetime(); time (UTC) when a client will be disabled, or None + server_settings: The server_settings dict from main() """ runtime_expansions = ("approval_delay", "approval_duration", - "created", "enabled", "fingerprint", - "host", "interval", "last_checked_ok", + "created", "enabled", "expires", + "fingerprint", "host", "interval", + "last_approval_request", "last_checked_ok", "last_enabled", "name", "timeout") - - def timeout_milliseconds(self): - "Return the 'timeout' attribute in milliseconds" - return _timedelta_to_milliseconds(self.timeout) - - def extended_timeout_milliseconds(self): - "Return the 'extended_timeout' attribute in milliseconds" - return _timedelta_to_milliseconds(self.extended_timeout) - - def interval_milliseconds(self): - "Return the 'interval' attribute in milliseconds" - return _timedelta_to_milliseconds(self.interval) - - def approval_delay_milliseconds(self): - return _timedelta_to_milliseconds(self.approval_delay) - - def __init__(self, name = None, disable_hook=None, config=None): - """Note: the 'checker' key in 'config' sets the - 'checker_command' attribute and *not* the 'checker' - attribute.""" + 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): + """Construct a new dict of client settings of this form: + { client_name: {setting_name: value, ...}, ...} + with exceptions for any special settings as defined above. + NOTE: Must be a pure function. Must return the same result + value given the same arguments. + """ + settings = {} + for client_name in config.sections(): + section = dict(config.items(client_name)) + client = settings[client_name] = {} + + client["host"] = section["host"] + # Reformat values from string types to Python types + client["approved_by_default"] = config.getboolean( + client_name, "approved_by_default") + 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: + client["secret"] = section["secret"].decode("base64") + elif "secfile" in section: + with open(os.path.expanduser(os.path.expandvars + (section["secfile"])), + "rb") as secfile: + client["secret"] = secfile.read() + else: + raise TypeError("No secret or secfile for section {}" + .format(section)) + client["timeout"] = string_to_delta(section["timeout"]) + client["extended_timeout"] = string_to_delta( + section["extended_timeout"]) + client["interval"] = string_to_delta(section["interval"]) + client["approval_delay"] = string_to_delta( + section["approval_delay"]) + client["approval_duration"] = string_to_delta( + section["approval_duration"]) + client["checker_command"] = section["checker"] + client["last_approval_request"] = None + client["last_checked_ok"] = None + client["last_checker_status"] = -2 + + return settings + + def __init__(self, settings, name = None, server_settings=None): self.name = name - if config is None: - config = {} + if server_settings is None: + server_settings = {} + self.server_settings = server_settings + # adding all client settings + for setting, value in settings.items(): + setattr(self, setting, value) + + if self.enabled: + if not hasattr(self, "last_enabled"): + self.last_enabled = datetime.datetime.utcnow() + if not hasattr(self, "expires"): + self.expires = (datetime.datetime.utcnow() + + self.timeout) + else: + self.last_enabled = None + 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 - self.fingerprint = (config["fingerprint"].upper() - .replace(" ", "")) logger.debug(" Fingerprint: %s", self.fingerprint) - if "secret" in config: - self.secret = config["secret"].decode("base64") - elif "secfile" in config: - with open(os.path.expanduser(os.path.expandvars - (config["secfile"])), - "rb") as secfile: - self.secret = secfile.read() - else: - raise TypeError("No secret or secfile for client %s" - % self.name) - self.host = config.get("host", "") - self.created = datetime.datetime.utcnow() - self.enabled = False - self.last_approval_request = None - self.last_enabled = None - self.last_checked_ok = None - self.timeout = string_to_delta(config["timeout"]) - self.extended_timeout = string_to_delta(config - ["extended_timeout"]) - self.interval = string_to_delta(config["interval"]) - self.disable_hook = disable_hook + self.created = settings.get("created", + datetime.datetime.utcnow()) + + # attributes specific for this server instance self.checker = None self.checker_initiator_tag = None self.disable_initiator_tag = None - self.expires = None self.checker_callback_tag = None - self.checker_command = config["checker"] self.current_checker_command = None - self.last_connect = None - self._approved = None - self.approved_by_default = config.get("approved_by_default", - True) + self.approved = None self.approvals_pending = 0 - self.approval_delay = string_to_delta( - config["approval_delay"]) - self.approval_duration = string_to_delta( - config["approval_duration"]) - self.changedstate = (multiprocessing_manager - .Condition(multiprocessing_manager - .Lock())) + 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)): + if not name.startswith("_"): + self.client_structure.append(name) + # Send notice to process children that client state has changed def send_changedstate(self): - self.changedstate.acquire() - self.changedstate.notify_all() - self.changedstate.release() + with self.changedstate: + self.changedstate.notify_all() def enable(self): """Start this client's checker and timeout hooks""" if getattr(self, "enabled", False): # Already enabled return - self.send_changedstate() - # Schedule a new checker to be started an 'interval' from now, - # and every interval from then on. - self.checker_initiator_tag = (gobject.timeout_add - (self.interval_milliseconds(), - self.start_checker)) - # Schedule a disable() when 'timeout' has passed self.expires = datetime.datetime.utcnow() + self.timeout - self.disable_initiator_tag = (gobject.timeout_add - (self.timeout_milliseconds(), - self.disable)) self.enabled = True self.last_enabled = datetime.datetime.utcnow() - # Also start a new checker *right now*. - self.start_checker() + self.init_checker() + self.send_changedstate() def disable(self, quiet=True): """Disable this client.""" if not getattr(self, "enabled", False): return False if not quiet: - self.send_changedstate() - if not quiet: logger.info("Disabling client %s", self.name) - if getattr(self, "disable_initiator_tag", False): + if getattr(self, "disable_initiator_tag", None) is not None: gobject.source_remove(self.disable_initiator_tag) self.disable_initiator_tag = None self.expires = None - if getattr(self, "checker_initiator_tag", False): + if getattr(self, "checker_initiator_tag", None) is not None: gobject.source_remove(self.checker_initiator_tag) self.checker_initiator_tag = None self.stop_checker() - if self.disable_hook: - self.disable_hook(self) self.enabled = False + if not quiet: + self.send_changedstate() # Do not run this again if called by a gobject.timeout_add return False def __del__(self): - self.disable_hook = None self.disable() + def init_checker(self): + # Schedule a new checker to be started an 'interval' from now, + # 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) + # 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) + # Also start a new checker *right now*. + self.start_checker() + def checker_callback(self, pid, condition, command): """The checker has completed, so take appropriate actions.""" self.checker_callback_tag = None self.checker = None if os.WIFEXITED(condition): - exitstatus = os.WEXITSTATUS(condition) - if exitstatus == 0: + self.last_checker_status = os.WEXITSTATUS(condition) + 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 logger.warning("Checker for %(name)s crashed?", vars(self)) - def checked_ok(self, timeout=None): - """Bump up the timeout for this client. - - This should only be called when the client has been seen, - alive and well. - """ + 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.bump_timeout() + + def bump_timeout(self, timeout=None): + """Bump up the timeout for this client.""" if timeout is None: timeout = self.timeout - self.last_checked_ok = datetime.datetime.utcnow() if self.disable_initiator_tag is not None: gobject.source_remove(self.disable_initiator_tag) + self.disable_initiator_tag = None if getattr(self, "enabled", False): - self.disable_initiator_tag = (gobject.timeout_add - (_timedelta_to_milliseconds - (timeout), 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): @@ -481,10 +679,10 @@ If a checker already exists, leave it running and do nothing.""" # The reason for not killing a running checker is that if we - # did that, then if a checker (for some reason) started - # running slowly and taking more than 'interval' time, the - # client would inevitably timeout, since no checker would get - # a chance to run to completion. If we instead leave running + # did that, and if a checker (for some reason) started running + # slowly and taking more than 'interval' time, then the client + # would inevitably timeout, since no checker would get a + # chance to run to completion. If we instead leave running # checkers alone, the checker would have to take more time # than 'timeout' for the client to be disabled, which is as it # should be. @@ -492,10 +690,11 @@ # If a checker exists, make sure it is not a zombie try: pid, status = os.waitpid(self.checker.pid, os.WNOHANG) - except (AttributeError, OSError) as error: - if (isinstance(error, OSError) - and error.errno != errno.ECHILD): - raise error + except AttributeError: + pass + except OSError as error: + if error.errno != errno.ECHILD: + raise else: if pid: logger.warning("Checker was a zombie") @@ -504,49 +703,58 @@ self.current_checker_command) # 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 } try: - # In case checker_command has exactly one % operator - command = self.checker_command % self.host - except TypeError: - # Escape attributes for the shell - escaped_attrs = dict( - (attr, - re.escape(unicode(str(getattr(self, attr, "")), - errors= - 'replace'))) - for attr in - self.runtime_expansions) - - try: - command = self.checker_command % escaped_attrs - except TypeError as error: - logger.error('Could not format string "%s":' - ' %s', self.checker_command, error) - return True # Try again later + 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.current_checker_command = command try: - logger.info("Starting checker %r for %s", - command, self.name) + 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="/") - 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. + 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) - if pid: - gobject.source_remove(self.checker_callback_tag) - self.checker_callback(pid, status, command) except OSError as error: - logger.error("Failed to start subprocess: %s", - 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) # Re-run this periodically if run by gobject.timeout_add return True @@ -559,18 +767,20 @@ return logger.debug("Stopping checker for %(name)s", vars(self)) try: - os.kill(self.checker.pid, signal.SIGTERM) + self.checker.terminate() #time.sleep(0.5) #if self.checker.poll() is None: - # os.kill(self.checker.pid, signal.SIGKILL) + # self.checker.kill() except OSError as error: if error.errno != errno.ESRCH: # No such process raise 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. @@ -585,7 +795,8 @@ # "Set" method, so we fail early here: if byte_arrays and signature != "ay": raise ValueError("Byte arrays not supported for non-'ay'" - " signature %r" % signature) + " signature {!r}".format(signature)) + def decorator(func): func._dbus_is_property = True func._dbus_interface = dbus_interface @@ -596,14 +807,55 @@ func._dbus_name = func._dbus_name[:-14] func._dbus_get_args_options = {'byte_arrays': byte_arrays } return func + + return decorator + + +def dbus_interface_annotations(dbus_interface): + """Decorator for marking functions returning interface annotations + + Usage: + + @dbus_interface_annotations("org.example.Interface") + def _foo(self): # Function name does not matter + return {"org.freedesktop.DBus.Deprecated": "true", + "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 + + +def dbus_annotations(annotations): + """Decorator to annotate D-Bus methods, signals or properties + Usage: + + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true", + "org.freedesktop.DBus.Property." + "EmitsChangedSignal": "false"}) + @dbus_service_property("org.example.Interface", signature="b", + access="r") + def Property_dbus_property(self): + return dbus.Boolean(False) + """ + + def decorator(func): + func._dbus_annotations = annotations + return func + return decorator class DBusPropertyException(dbus.exceptions.DBusException): """A base class for D-Bus property-related exceptions """ - def __unicode__(self): - return unicode(str(self)) + pass class DBusPropertyAccessException(DBusPropertyException): @@ -627,34 +879,41 @@ """ @staticmethod - def _is_dbus_property(obj): - return getattr(obj, "_dbus_is_property", False) + def _is_dbus_thing(thing): + """Returns a function testing if an attribute is a D-Bus thing + + If called like _is_dbus_thing("method") it returns a function + suitable for use as predicate to inspect.getmembers(). + """ + return lambda obj: getattr(obj, "_dbus_is_{}".format(thing), + False) - def _get_all_dbus_properties(self): + def _get_all_dbus_things(self, thing): """Returns a generator of (name, attribute) pairs """ - return ((prop.__get__(self)._dbus_name, prop.__get__(self)) + return ((getattr(athing.__get__(self), "_dbus_name", name), + athing.__get__(self)) for cls in self.__class__.__mro__ - for name, prop in - inspect.getmembers(cls, self._is_dbus_property)) + for name, athing in + inspect.getmembers(cls, self._is_dbus_thing(thing))) 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_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) + raise DBusPropertyNotFound("{}:{}.{}".format( + self.dbus_object_path, interface_name, property_name)) - @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss", + @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. @@ -678,12 +937,15 @@ # The byte_arrays option is not supported yet on # signatures other than "ay". if prop._dbus_signature != "ay": - raise ValueError - value = dbus.ByteArray(''.join(unichr(byte) - for byte in value)) + raise ValueError("Byte arrays not supported for non-" + "'ay' signature {!r}" + .format(prop._dbus_signature)) + value = dbus.ByteArray(b''.join(chr(byte) + 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 @@ -691,8 +953,8 @@ Note: Will not include properties with access="write". """ - all = {} - for name, prop in self._get_all_dbus_properties(): + properties = {} + for name, prop in self._get_all_dbus_things("property"): if (interface_name and interface_name != prop._dbus_interface): # Interface non-empty but did not match @@ -702,36 +964,78 @@ continue value = prop() if not hasattr(value, "variant_level"): - all[name] = value + properties[name] = value continue - all[name] = type(value)(value, variant_level= - value.variant_level+1) - return dbus.Dictionary(all, signature="sv") + 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") + def PropertiesChanged(self, interface_name, changed_properties, + invalidated_properties): + """Standard D-Bus PropertiesChanged() signal, see D-Bus + standard. + """ + pass @dbus.service.method(dbus.INTROSPECTABLE_IFACE, out_signature="s", path_keyword='object_path', connection_keyword='connection') def Introspect(self, object_path, connection): - """Standard D-Bus method, overloaded to insert property tags. + """Overloading of standard D-Bus method. + + Inserts property tags and interface annotation tags. """ xmlstring = dbus.service.Object.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) for name, prop - in self._get_all_dbus_properties() + in self._get_all_dbus_things("property") 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 the names to the return values for the # "org.freedesktop.DBus.Properties" methods if (if_tag.getAttribute("name") @@ -752,114 +1056,188 @@ except (AttributeError, xml.dom.DOMException, xml.parsers.expat.ExpatError) as error: logger.error("Failed to override Introspection method", - error) + exc_info=error) return xmlstring -def datetime_to_dbus (dt, variant_level=0): +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) - -class AlternateDBusNamesMetaclass(DBusObjectWithProperties - .__metaclass__): - """Applied to an empty subclass of a D-Bus object, this metaclass - will add additional D-Bus attributes matching a certain pattern. + return dbus.String(dt.isoformat(), variant_level=variant_level) + + +def alternate_dbus_interfaces(alt_interface_names, deprecate=True): + """A class decorator; applied to a subclass of + dbus.service.Object, it will add alternate D-Bus attributes with + interface names according to the "alt_interface_names" mapping. + Usage: + + @alternate_dbus_interfaces({"org.example.Interface": + "net.example.AlternateInterface"}) + class SampleDBusObject(dbus.service.Object): + @dbus.service.method("org.example.Interface") + def SampleDBusMethod(): + pass + + The above "SampleDBusMethod" on "SampleDBusObject" will be + reachable via two interfaces: "org.example.Interface" and + "net.example.AlternateInterface", the latter of which will have + its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to + "true", unless "deprecate" is passed with a False value. + + This works for methods and signals, and also for D-Bus properties + (from DBusObjectWithProperties) and interfaces (from the + dbus_interface_annotations decorator). """ - def __new__(mcs, name, bases, attr): - # Go through all the base classes which could have D-Bus - # methods, signals, or properties in them - for base in (b for b in bases - if issubclass(b, dbus.service.Object)): - # Go though all attributes of the base class - for attrname, attribute in inspect.getmembers(base): + + def wrapper(cls): + for orig_interface_name, alt_interface_name in ( + alt_interface_names.items()): + attr = {} + interface_names = set() + # Go though all attributes of the class + for attrname, attribute in inspect.getmembers(cls): # 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("se.recompile.Mandos")): + 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("se.recompile.Mandos", - "se.bsnet.fukt.Mandos")) + 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 function by - # black magic + # Extract the original non-method undecorated + # function by black magic nonmethod_func = (dict( - zip(attribute.func_code.co_freevars, - attribute.__closure__))["func"] - .cell_contents) + zip(attribute.func_code.co_freevars, + attribute.__closure__)) + ["func"].cell_contents) # 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) + 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))) + nonmethod_func.func_code, + nonmethod_func.func_globals, + nonmethod_func.func_name, + nonmethod_func.func_defaults, + nonmethod_func.func_closure))) + # Copy annotations, if any + try: + new_function._dbus_annotations = dict( + attribute._dbus_annotations) + except AttributeError: + pass # Define a creator of a function to call both the - # old and new functions, so both the old and new - # signals gets sent when the function is called + # original and alternate functions, so both the + # original and alternate signals gets sent when + # the function is called def fixscope(func1, func2): """This function is a scope container to pass func1 and func2 to the "call_both" function outside of its arguments""" + def call_both(*args, **kwargs): """This function will emit two D-Bus signals by calling func1 and func2""" func1(*args, **kwargs) func2(*args, **kwargs) + return call_both # Create the "call_both" function and add it to # the class - attr[attrname] = fixscope(attribute, - new_function) + attr[attrname] = fixscope(attribute, new_function) # Is this a D-Bus method? elif getattr(attribute, "_dbus_is_method", False): # Create a new, but exactly alike, function # 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) + except AttributeError: + pass # Is this a D-Bus property? elif getattr(attribute, "_dbus_is_property", False): # Create a new, but exactly alike, function # 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))) - return type.__new__(mcs, name, bases, attr) - + 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) + except AttributeError: + pass + # Is this a D-Bus interface? + elif getattr(attribute, "_dbus_is_interface", False): + # Create a new, but exactly alike, function + # 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))) + 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" } + # Find an unused name + for aname in (iname.format(i) + for i in itertools.count()): + if aname not in attr: + attr[aname] = func + break + if interface_names: + # 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) + return cls + + return wrapper + + +@alternate_dbus_interfaces({"se.recompile.Mandos": + "se.bsnet.fukt.Mandos"}) class ClientDBus(Client, DBusObjectWithProperties): """A Client class using D-Bus @@ -869,30 +1247,33 @@ """ runtime_expansions = (Client.runtime_expansions - + ("dbus_object_path",)) + + ("dbus_object_path", )) + + _interface = "se.recompile.Mandos.Client" # dbus.service.Object doesn't use super(), so we can't either. def __init__(self, bus = None, *args, **kwargs): - self._approvals_pending = 0 self.bus = bus Client.__init__(self, *args, **kwargs) # Only now, when this client is initialized, can it show up on # the D-Bus - client_object_name = unicode(self.name).translate( + 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): + + 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. - + transform_fun: Function that takes a value and a variant_level and transforms it to a D-Bus type. dbus_name: D-Bus name of the variable @@ -900,22 +1281,32 @@ to the D-Bus. Default: no transform variant_level: D-Bus variant level. Default: 1 """ - attrname = "_{0}".format(dbus_name) + 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)): - dbus_value = transform_func(type_func(value), - variant_level - =variant_level) - self.PropertyChanged(dbus.String(dbus_name), - dbus_value) + if invalidate_only: + self.PropertiesChanged( + _interface, dbus.Dictionary(), + dbus.Array((dbus_name, ))) + else: + 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()) setattr(self, attrname, value) return property(lambda self: getattr(self, attrname), setter) - expires = notifychangeproperty(datetime_to_dbus, "Expires") approvals_pending = notifychangeproperty(dbus.Boolean, "ApprovalPending", @@ -923,34 +1314,36 @@ 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, + "LastCheckerStatus") last_approval_request = notifychangeproperty( datetime_to_dbus, "LastApprovalRequest") approved_by_default = notifychangeproperty(dbus.Boolean, "ApprovedByDefault") - approval_delay = notifychangeproperty(dbus.UInt16, - "ApprovalDelay", - type_func = - _timedelta_to_milliseconds) + approval_delay = notifychangeproperty( + dbus.UInt64, "ApprovalDelay", + type_func = lambda td: td.total_seconds() * 1000) approval_duration = notifychangeproperty( - dbus.UInt16, "ApprovalDuration", - type_func = _timedelta_to_milliseconds) + dbus.UInt64, "ApprovalDuration", + type_func = lambda td: td.total_seconds() * 1000) host = notifychangeproperty(dbus.String, "Host") - timeout = notifychangeproperty(dbus.UInt16, "Timeout", - type_func = - _timedelta_to_milliseconds) + timeout = notifychangeproperty( + dbus.UInt64, "Timeout", + type_func = lambda td: td.total_seconds() * 1000) extended_timeout = notifychangeproperty( - dbus.UInt16, "ExtendedTimeout", - type_func = _timedelta_to_milliseconds) - interval = notifychangeproperty(dbus.UInt16, - "Interval", - type_func = - _timedelta_to_milliseconds) + dbus.UInt64, "ExtendedTimeout", + 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) del notifychangeproperty @@ -983,11 +1376,7 @@ *args, **kwargs) def start_checker(self, *args, **kwargs): - old_checker = self.checker - if self.checker is not None: - old_checker_pid = self.checker.pid - else: - old_checker_pid = None + old_checker_pid = getattr(self.checker, "pid", None) r = Client.start_checker(self, *args, **kwargs) # Only if new checker process was started if (self.checker is not None @@ -997,19 +1386,18 @@ return r def _reset_approved(self): - self._approved = None + self.approved = None return False def approve(self, value=True): + self.approved = value + gobject.timeout_add(int(self.approval_duration.total_seconds() + * 1000), self._reset_approved) self.send_changedstate() - self._approved = value - gobject.timeout_add(_timedelta_to_milliseconds - (self.approval_duration), - self._reset_approved) - ## D-Bus methods, signals & properties - _interface = "se.recompile.Mandos.Client" + + ## Interfaces ## Signals @@ -1026,6 +1414,7 @@ pass # PropertyChanged - signal + @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"}) @dbus.service.signal(_interface, signature="sv") def PropertyChanged(self, property, value): "D-Bus signal" @@ -1095,7 +1484,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 @@ -1103,20 +1493,23 @@ 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 - return dbus.UInt64(self.approval_delay_milliseconds()) + return dbus.UInt64(self.approval_delay.total_seconds() + * 1000) 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 - return dbus.UInt64(_timedelta_to_milliseconds( - self.approval_duration)) + return dbus.UInt64(self.approval_duration.total_seconds() + * 1000) self.approval_duration = datetime.timedelta(0, 0, 0, value) # Name - property @@ -1130,17 +1523,18 @@ 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 return dbus.String(self.host) - self.host = value + self.host = str(value) # Created - property @dbus_service_property(_interface, signature="s", access="read") def Created_dbus_property(self): - return dbus.String(datetime_to_dbus(self.created)) + return datetime_to_dbus(self.created) # LastEnabled - property @dbus_service_property(_interface, signature="s", access="read") @@ -1148,7 +1542,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 @@ -1159,7 +1554,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: @@ -1167,6 +1563,11 @@ return return datetime_to_dbus(self.last_checked_ok) + # LastCheckerStatus - property + @dbus_service_property(_interface, signature="n", access="read") + def LastCheckerStatus_dbus_property(self): + return dbus.Int16(self.last_checker_status) + # Expires - property @dbus_service_property(_interface, signature="s", access="read") def Expires_dbus_property(self): @@ -1178,66 +1579,69 @@ 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 - return dbus.UInt64(self.timeout_milliseconds()) + return dbus.UInt64(self.timeout.total_seconds() * 1000) + old_timeout = self.timeout self.timeout = datetime.timedelta(0, 0, 0, value) - if getattr(self, "disable_initiator_tag", None) is None: - return - # Reschedule timeout - gobject.source_remove(self.disable_initiator_tag) - self.disable_initiator_tag = None - self.expires = None - time_to_die = _timedelta_to_milliseconds((self - .last_checked_ok - + self.timeout) - - datetime.datetime - .utcnow()) - if time_to_die <= 0: - # The timeout has passed - self.disable() - else: - self.expires = (datetime.datetime.utcnow() - + datetime.timedelta(milliseconds = - time_to_die)) - self.disable_initiator_tag = (gobject.timeout_add - (time_to_die, self.disable)) + # Reschedule disabling + if self.enabled: + now = datetime.datetime.utcnow() + self.expires += self.timeout - old_timeout + if self.expires <= now: + # The timeout has passed + self.disable() + else: + if (getattr(self, "disable_initiator_tag", None) + 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) # 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 - return dbus.UInt64(self.extended_timeout_milliseconds()) + return dbus.UInt64(self.extended_timeout.total_seconds() + * 1000) 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 - return dbus.UInt64(self.interval_milliseconds()) + return dbus.UInt64(self.interval.total_seconds() * 1000) self.interval = datetime.timedelta(0, 0, 0, value) if getattr(self, "checker_initiator_tag", None) is None: return - # 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 + 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 # 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 return dbus.String(self.checker_command) - self.checker_command = value + 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 @@ -1253,10 +1657,12 @@ return self.dbus_object_path # is already a dbus.ObjectPath # Secret = property - @dbus_service_property(_interface, signature="ay", - access="write", byte_arrays=True) + @dbus_service_property(_interface, + signature="ay", + access="write", + byte_arrays=True) def Secret_dbus_property(self, value): - self.secret = str(value) + self.secret = bytes(value) del _interface @@ -1266,28 +1672,28 @@ 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'): + if name == '_pipe': return super(ProxyClient, self).__getattribute__(name) self._pipe.send(('getattr', name)) data = self._pipe.recv() 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): - if(name == '_pipe'): + if name == '_pipe': return super(ProxyClient, self).__setattr__(name, value) self._pipe.send(('setattr', name, value)) -class ClientDBusTransitional(ClientDBus): - __metaclass__ = AlternateDBusNamesMetaclass class ClientHandler(socketserver.BaseRequestHandler, object): """A class to handle client connections. @@ -1298,14 +1704,12 @@ def handle(self): with contextlib.closing(self.server.child_pipe) as child_pipe: logger.info("TCP connection from: %s", - unicode(self.client_address)) + str(self.client_address)) 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 @@ -1320,9 +1724,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 @@ -1330,7 +1733,7 @@ logger.debug("Protocol version: %r", line) try: if int(line.strip().split()[0]) > 1: - raise RuntimeError + raise RuntimeError(line) except (ValueError, IndexError, RuntimeError) as error: logger.error("Unknown protocol version: %s", error) return @@ -1348,8 +1751,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) @@ -1370,23 +1773,23 @@ 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") return - if client._approved or not client.approval_delay: + if client.approved or not client.approval_delay: #We are approved or approval is disabled break - elif client._approved is None: + elif client.approved is None: logger.info("Client %s needs approval", client.name) if self.server.use_dbus: # Emit D-Bus signal client.NeedApproval( - client.approval_delay_milliseconds(), - client.approved_by_default) + client.approval_delay.total_seconds() + * 1000, client.approved_by_default) else: logger.warning("Client %s was not approved", client.name) @@ -1398,9 +1801,7 @@ #wait until timeout or approved time = datetime.datetime.now() client.changedstate.acquire() - (client.changedstate.wait - (float(client._timedelta_to_milliseconds(delay) - / 1000))) + client.changedstate.wait(delay.total_seconds()) client.changedstate.release() time2 = datetime.datetime.now() if (time2 - time) >= delay: @@ -1422,16 +1823,17 @@ try: sent = session.send(client.secret[sent_size:]) except gnutls.errors.GNUTLSError as error: - logger.warning("gnutls send failed") + 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) # bump the timeout using extended_timeout - client.checked_ok(client.extended_timeout) + client.bump_timeout(client.extended_timeout) if self.server.use_dbus: # Emit D-Bus signal client.GotSecret() @@ -1442,14 +1844,15 @@ try: session.bye() except gnutls.errors.GNUTLSError as error: - logger.warning("GnuTLS bye failed") + logger.warning("GnuTLS bye failed", + exc_info=error) @staticmethod 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 @@ -1469,65 +1872,62 @@ 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 fpr = ctypes.string_at(buf, buf_len.value) # Convert the bytestring to hexadecimal notation - hex_fpr = ''.join("%02X" % ord(char) for char in fpr) + hex_fpr = binascii.hexlify(fpr).upper() return hex_fpr class MultiprocessingMixIn(object): """Like socketserver.ThreadingMixIn, but with multiprocessing""" + def sub_process_main(self, request, address): try: self.finish_request(request, address) - except: + except Exception: self.handle_error(request, address) self.close_request(request) def process_request(self, request, address): """Start a new process to process the request.""" proc = multiprocessing.Process(target = self.sub_process_main, - args = (request, - address)) + args = (request, address)) proc.start() return proc class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object): """ adds a pipe to the MixIn """ + def process_request(self, request, client_address): """Overrides and wraps the original process_request(). @@ -1542,7 +1942,7 @@ def add_pipe(self, parent_pipe, proc): """Dummy function; override as necessary""" - raise NotImplementedError + raise NotImplementedError() class IPv6_TCPServer(MultiprocessingMixInWithPipe, @@ -1554,13 +1954,46 @@ 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): + interface=None, + use_ipv6=True, + socketfd=None): + """If socketfd is set, use that file descriptor instead of + creating a new one with socket.socket(). + """ self.interface = interface if use_ipv6: self.address_family = socket.AF_INET6 + if socketfd is not None: + # Save the file descriptor + self.socketfd = socketfd + # Save the original socket.socket() function + self.socket_socket = socket.socket + # To implement --socket, we monkey patch socket.socket. + # + # (When socketserver.TCPServer is a new-style class, we + # could make self.socket into a property instead of monkey + # patching socket.socket.) + # + # Create a one-time-only replacement for socket.socket() + @functools.wraps(socket.socket) + def socket_wrapper(*args, **kwargs): + # Restore original function so subsequent calls are + # not affected. + socket.socket = self.socket_socket + del self.socket_socket + # This time only, return a new socket object from the + # saved file descriptor. + return socket.fromfd(self.socketfd, *args, **kwargs) + # Replace socket.socket() function with wrapper + socket.socket = socket_wrapper + # The socketserver.TCPServer.__init__ will call + # socket.socket(), which might be our replacement, + # socket_wrapper(), if socketfd was set. socketserver.TCPServer.__init__(self, server_address, RequestHandlerClass) + def server_bind(self): """This overrides the normal server_bind() function to bind to an interface if one was specified, and also NOT to @@ -1572,19 +2005,20 @@ self.interface) else: try: - self.socket.setsockopt(socket.SOL_SOCKET, - SO_BINDTODEVICE, - str(self.interface - + '\0')) + self.socket.setsockopt( + socket.SOL_SOCKET, SO_BINDTODEVICE, + (self.interface + "\0").encode("utf-8")) except socket.error as error: - if error[0] == errno.EPERM: - logger.error("No permission to" - " bind to interface %s", - self.interface) - elif error[0] == errno.ENOPROTOOPT: + if error.errno == errno.EPERM: + logger.error("No permission to bind to" + " interface %s", self.interface) + elif error.errno == errno.ENOPROTOOPT: logger.error("SO_BINDTODEVICE not available;" " cannot bind to interface %s", self.interface) + elif error.errno == errno.ENODEV: + logger.error("Interface %s does not exist," + " cannot bind", self.interface) else: raise # Only bind(2) the socket if we really need to. @@ -1593,12 +2027,11 @@ if self.address_family == socket.AF_INET6: any_address = "::" # in6addr_any else: - any_address = socket.INADDR_ANY + any_address = "0.0.0.0" # INADDR_ANY 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 @@ -1618,19 +2051,26 @@ 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): + 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: - self.clients = set() + self.clients = {} self.use_dbus = use_dbus self.gnutls_priority = gnutls_priority IPv6_TCPServer.__init__(self, server_address, RequestHandlerClass, interface = interface, - use_ipv6 = use_ipv6) + use_ipv6 = use_ipv6, + socketfd = socketfd) + def server_activate(self): if self.enabled: return socketserver.TCPServer.server_activate(self) @@ -1640,31 +2080,19 @@ 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): - condition_names = { - gobject.IO_IN: "IN", # There is data to read. - gobject.IO_OUT: "OUT", # Data can be written (without - # blocking). - gobject.IO_PRI: "PRI", # There is urgent data to read. - gobject.IO_ERR: "ERR", # Error condition. - gobject.IO_HUP: "HUP" # Hung up (the connection has been - # broken, usually for pipes and - # sockets). - } - conditions_string = ' | '.join(name - for cond, name in - condition_names.iteritems() - if cond & condition) + 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 | condition & gobject.IO_HUP): + if condition & (gobject.IO_ERR | gobject.IO_HUP): # Wait for other process to exit proc.join() return False @@ -1677,7 +2105,7 @@ fpr = request[1] address = request[2] - for c in self.clients: + for c in self.clients.itervalues(): if c.fingerprint == fpr: client = c break @@ -1691,14 +2119,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 @@ -1710,15 +2137,15 @@ 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',)) + 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] @@ -1728,6 +2155,111 @@ return True +def rfc3339_duration_to_delta(duration): + """Parse an RFC 3339 "duration" and return a datetime.timedelta + + >>> rfc3339_duration_to_delta("P7D") + datetime.timedelta(7) + >>> rfc3339_duration_to_delta("PT60S") + datetime.timedelta(0, 60) + >>> rfc3339_duration_to_delta("PT60M") + datetime.timedelta(0, 3600) + >>> rfc3339_duration_to_delta("PT24H") + datetime.timedelta(1) + >>> rfc3339_duration_to_delta("P1W") + datetime.timedelta(7) + >>> rfc3339_duration_to_delta("PT5M30S") + datetime.timedelta(0, 330) + >>> rfc3339_duration_to_delta("P1DT3M20S") + datetime.timedelta(1, 200) + """ + + # Parsing an RFC 3339 duration with regular expressions is not + # possible - there would have to be multiple places for the same + # values, like seconds. The current code, while more esoteric, is + # cleaner without depending on a parsing library. If Python had a + # built-in library for parsing we would use it, but we'd like to + # 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, ))) + token_minute = Token(re.compile(r"(\d+)M"), + datetime.timedelta(minutes=1), + frozenset((token_second, token_end))) + token_hour = Token(re.compile(r"(\d+)H"), + datetime.timedelta(hours=1), + frozenset((token_minute, token_end))) + token_time = Token(re.compile(r"T"), + None, + frozenset((token_hour, token_minute, + token_second))) + token_day = Token(re.compile(r"(\d+)D"), + datetime.timedelta(days=1), + frozenset((token_time, token_end))) + token_month = Token(re.compile(r"(\d+)M"), + datetime.timedelta(weeks=4), + frozenset((token_day, token_end))) + token_year = Token(re.compile(r"(\d+)Y"), + datetime.timedelta(weeks=52), + frozenset((token_month, token_end))) + token_week = Token(re.compile(r"(\d+)W"), + datetime.timedelta(weeks=1), + frozenset((token_end, ))) + token_duration = Token(re.compile(r"P"), None, + frozenset((token_year, token_month, + token_day, token_time, + token_week))) + # Define starting values + value = datetime.timedelta() # Value so far + found_token = None + 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: + # Search for any currently valid tokens + for token in followers: + match = token.regexp.match(s) + if match is not None: + # Token found + if token.value is not None: + # Value found, parse digits + factor = int(match.group(1), 10) + # Add to value so far + value += factor * token.value + # Strip token from string + s = token.regexp.sub("", s, 1) + # Go to found token + found_token = token + # Set valid next tokens + followers = found_token.followers + break + else: + # No currently valid tokens were found + raise ValueError("Invalid RFC 3339 duration: {!r}" + .format(duration)) + # End token found + return value + + def string_to_delta(interval): """Parse a string and return a datetime.timedelta @@ -1744,10 +2276,16 @@ >>> string_to_delta('5m 30s') datetime.timedelta(0, 330) """ + + try: + return rfc3339_duration_to_delta(interval) + except ValueError: + pass + timevalue = datetime.timedelta(0) for s in interval.split(): try: - suffix = unicode(s[-1]) + suffix = s[-1] value = int(s[:-1]) if suffix == "d": delta = datetime.timedelta(value) @@ -1760,37 +2298,13 @@ elif suffix == "w": delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) else: - raise ValueError("Unknown suffix %r" % suffix) - except (ValueError, IndexError) as e: + raise ValueError("Unknown suffix {!r}".format(suffix)) + except IndexError as e: raise ValueError(*(e.args)) timevalue += delta return timevalue -def if_nametoindex(interface): - """Call the C function if_nametoindex(), or equivalent - - Note: This function cannot accept a unicode string.""" - global if_nametoindex - try: - if_nametoindex = (ctypes.cdll.LoadLibrary - (ctypes.util.find_library("c")) - .if_nametoindex) - except (OSError, AttributeError): - logger.warning("Doing if_nametoindex the hard way") - def if_nametoindex(interface): - "Get an interface index the hard way, i.e. using fcntl()" - SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h - with contextlib.closing(socket.socket()) as s: - ifreq = fcntl.ioctl(s, SIOCGIFINDEX, - struct.pack(str("16s16x"), - interface)) - interface_index = struct.unpack(str("I"), - ifreq[16:20])[0] - return interface_index - return if_nametoindex(interface) - - def daemon(nochdir = False, noclose = False): """See daemon(3). Standard BSD Unix function. @@ -1804,11 +2318,11 @@ sys.exit() if not noclose: # Close all standard open file descriptors - null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR) + 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, - "%s not a character device" - % os.path.devnull) + "{} not a character device" + .format(os.devnull)) os.dup2(null, sys.stdin.fileno()) os.dup2(null, sys.stdout.fileno()) os.dup2(null, sys.stderr.fileno()) @@ -1823,7 +2337,7 @@ parser = argparse.ArgumentParser() parser.add_argument("-v", "--version", action="version", - version = "%%(prog)s %s" % version, + version = "%(prog)s {}".format(version), help="show version number and exit") parser.add_argument("-i", "--interface", metavar="IF", help="Bind to interface IF") @@ -1835,7 +2349,7 @@ help="Run self-test") parser.add_argument("--debug", action="store_true", help="Debug mode; run in foreground and log" - " to terminal") + " to terminal", default=None) parser.add_argument("--debuglevel", metavar="LEVEL", help="Debug level for stdout output") parser.add_argument("--priority", help="GnuTLS" @@ -1848,15 +2362,30 @@ " files") parser.add_argument("--no-dbus", action="store_false", dest="use_dbus", help="Do not provide D-Bus" - " system bus interface") + " system bus interface", default=None) parser.add_argument("--no-ipv6", action="store_false", - dest="use_ipv6", help="Do not use IPv6") + dest="use_ipv6", help="Do not use IPv6", + default=None) + parser.add_argument("--no-restore", action="store_false", + dest="restore", help="Do not restore stored" + " state", default=None) + parser.add_argument("--socket", type=int, + help="Specify a file descriptor to a network" + " socket to use instead of creating one") + parser.add_argument("--statedir", metavar="DIR", + help="Directory to save/restore state in") + parser.add_argument("--foreground", action="store_true", + help="Run in foreground", default=None) + parser.add_argument("--no-zeroconf", action="store_false", + dest="zeroconf", help="Do not use Zeroconf", + default=None) + options = parser.parse_args() if options.check: import doctest - doctest.testmod() - sys.exit() + fail_count, test_count = doctest.testmod() + sys.exit(os.EX_OK if fail_count == 0 else 1) # Default values for config file for server-global settings server_defaults = { "interface": "", @@ -1864,118 +2393,146 @@ "port": "", "debug": "False", "priority": - "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP", + "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA" + ":+SIGN-RSA-SHA224:+SIGN-RSA-RMD160", "servicename": "Mandos", "use_dbus": "True", "use_ipv6": "True", "debuglevel": "", - } + "restore": "True", + "socket": "", + "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 - for option in ("debug", "use_dbus", "use_ipv6"): + for option in ("debug", "use_dbus", "use_ipv6", "foreground"): server_settings[option] = server_config.getboolean("DEFAULT", option) if server_settings["port"]: server_settings["port"] = server_config.getint("DEFAULT", "port") + if server_settings["socket"]: + server_settings["socket"] = server_config.getint("DEFAULT", + "socket") + # Later, stdin will, and stdout and stderr might, be dup'ed + # over with an opened os.devnull. But we don't want this to + # happen with a supplied network socket. + if 0 <= server_settings["socket"] <= 2: + server_settings["socket"] = os.dup(server_settings + ["socket"]) del server_config # 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"): + "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 del options # Force all strings to be unicode for option in server_settings.keys(): - if type(server_settings[option]) is str: - server_settings[option] = unicode(server_settings[option]) + if isinstance(server_settings[option], bytes): + server_settings[option] = (server_settings[option] + .decode("utf-8")) + # Force all boolean options to be boolean + for option in ("debug", "use_dbus", "use_ipv6", "restore", + "foreground", "zeroconf"): + server_settings[option] = bool(server_settings[option]) + # Debug implies foreground + if server_settings["debug"]: + server_settings["foreground"] = True # Now we have our good server settings in "server_settings" ################################################################## + 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"] debuglevel = server_settings["debuglevel"] use_dbus = server_settings["use_dbus"] use_ipv6 = server_settings["use_ipv6"] + stored_state_path = os.path.join(server_settings["statedir"], + stored_state_file) + foreground = server_settings["foreground"] + zeroconf = server_settings["zeroconf"] + + if debug: + initlogger(debug, logging.DEBUG) + else: + if not debuglevel: + initlogger(debug) + else: + level = getattr(logging, debuglevel.upper()) + initlogger(debug, level) if server_settings["servicename"] != "Mandos": - syslogger.setFormatter(logging.Formatter - ('Mandos (%s) [%%(process)d]:' - ' %%(levelname)s: %%(message)s' - % server_settings["servicename"])) + syslogger.setFormatter( + logging.Formatter('Mandos ({}) [%(process)d]:' + ' %(levelname)s: %(message)s'.format( + server_settings["servicename"]))) # Parse config file with clients - client_defaults = { "timeout": "5m", - "extended_timeout": "15m", - "interval": "2m", - "checker": "fping -q -- %%(host)s", - "host": "", - "approval_delay": "0s", - "approval_duration": "1s", - } - client_config = configparser.SafeConfigParser(client_defaults) + client_config = configparser.SafeConfigParser(Client + .client_defaults) client_config.read(os.path.join(server_settings["configdir"], "clients.conf")) global mandos_dbus_service mandos_dbus_service = None - 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) - if not debug: - pidfilename = "/var/run/mandos.pid" + 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) + 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") - except IOError: - logger.error("Could not open file %r", pidfilename) + pidfile = codecs.open(pidfilename, "w", encoding="utf-8") + except IOError as e: + logger.error("Could not open file %r", pidfilename, + exc_info=e) - try: - uid = pwd.getpwnam("_mandos").pw_uid - gid = pwd.getpwnam("_mandos").pw_gid - except KeyError: + for name in ("_mandos", "mandos", "nobody"): try: - uid = pwd.getpwnam("mandos").pw_uid - gid = pwd.getpwnam("mandos").pw_gid + uid = pwd.getpwnam(name).pw_uid + gid = pwd.getpwnam(name).pw_gid + break except KeyError: - try: - uid = pwd.getpwnam("nobody").pw_uid - gid = pwd.getpwnam("nobody").pw_gid - except KeyError: - uid = 65534 - gid = 65534 + continue + else: + uid = 65534 + gid = 65534 try: os.setgid(gid) os.setuid(uid) except OSError as error: - if error[0] != errno.EPERM: - raise error - - if not debug and not debuglevel: - syslogger.setLevel(logging.WARNING) - console.setLevel(logging.WARNING) - if debuglevel: - level = getattr(logging, debuglevel.upper()) - syslogger.setLevel(level) - console.setLevel(level) + if error.errno != errno.EPERM: + raise if debug: # Enable all possible GnuTLS debugging @@ -1988,102 +2545,206 @@ 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.path.devnull, os.O_NOCTTY | os.O_RDWR) + null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR) os.dup2(null, sys.stdin.fileno()) if null > 2: os.close(null) - else: - # No console logging - logger.removeHandler(console) # Need to fork before connecting to D-Bus - if not debug: + if not foreground: # Close all input and output, do double fork, etc. daemon() + # multiprocessing will use threads, so before we use gobject we + # need to inform gobject that threads will be used. + gobject.threads_init() + global main_loop # From the Avahi example code - DBusGMainLoop(set_as_default=True ) + DBusGMainLoop(set_as_default=True) main_loop = gobject.MainLoop() bus = dbus.SystemBus() # End of Avahi example code 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: - logger.error(unicode(e) + ", disabling D-Bus") + 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 - protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET - service = AvahiService(name = server_settings["servicename"], - servicetype = "_mandos._tcp", - protocol = protocol, bus = bus) - if server_settings["interface"]: - service.interface = (if_nametoindex - (str(server_settings["interface"]))) + 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) + if server_settings["interface"]: + service.interface = if_nametoindex( + server_settings["interface"].encode("utf-8")) global multiprocessing_manager multiprocessing_manager = multiprocessing.Manager() client_class = Client if use_dbus: - client_class = functools.partial(ClientDBusTransitional, - bus = bus) - def client_config_items(config, section): - special_settings = { - "approved_by_default": - lambda: config.getboolean(section, - "approved_by_default"), - } - for name, value in config.items(section): + client_class = functools.partial(ClientDBus, bus = bus) + + client_settings = Client.config_parser(client_config) + old_client_settings = {} + clients_data = {} + + # This is used to redirect stdout and stderr for checker processes + global wnull + wnull = open(os.devnull, "w") # A writable /dev/null + # Only used if server is running in foreground but not in debug + # mode + if debug or not foreground: + wnull.close() + + # Get client data and settings from last running state. + if server_settings["restore"]: + try: + with open(stored_state_path, "rb") as 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))) + 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) + + with PGPEngine() as pgp: + for client_name, client in clients_data.items(): + # Skip removed clients + if client_name not in client_settings: + continue + + # Decide which value to use after restoring saved state. + # We have three different values: Old config file, + # new config file, and saved state. + # New config value takes precedence if it differs from old + # config value, otherwise use saved state. + for name, value in client_settings[client_name].items(): + try: + # 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])): + client[name] = value + except KeyError: + pass + + # Clients who has passed its expire date can still be + # enabled if its last checker was successful. Clients + # whose checker succeeded before we stored its state is + # assumed to have successfully run all checkers during + # downtime. + if client["enabled"]: + if datetime.datetime.utcnow() >= client["expires"]: + if not client["last_checked_ok"]: + logger.warning( + "disabling client {} - Client never " + "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"])) + client["enabled"] = False + else: + client["expires"] = ( + datetime.datetime.utcnow() + + client["timeout"]) + logger.debug("Last checker succeeded," + " keeping {} enabled".format( + client_name)) try: - yield (name, special_settings[name]()) - except KeyError: - yield (name, value) - - tcp_server.clients.update(set( - client_class(name = section, - config= dict(client_config_items( - client_config, section))) - for section in client_config.sections())) + 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"]) + + # Add/remove clients based on new changes made to config + for client_name in (set(old_client_settings) + - set(client_settings)): + del clients_data[client_name] + for client_name in (set(client_settings) + - set(old_client_settings)): + clients_data[client_name] = client_settings[client_name] + + # Create all client objects + for client_name, client in clients_data.items(): + tcp_server.clients[client_name] = client_class( + name = client_name, + settings = client, + server_settings = server_settings) + if not tcp_server.clients: logger.warning("No clients defined") - - if not debug: - try: - with pidfile: - pid = os.getpid() - pidfile.write(str(pid) + "\n".encode("utf-8")) - del pidfile - except IOError: - logger.error("Could not write to file %r with PID %d", - pidfilename, pid) - except NameError: - # "pidfile" was never created - pass + + if not foreground: + if pidfile is not None: + pid = os.getpid() + try: + with pidfile: + print(pid, file=pidfile) + except IOError: + logger.error("Could not write to file %r with PID %d", + pidfilename, pid) + del pidfile del pidfilename - - signal.signal(signal.SIGINT, signal.SIG_IGN) signal.signal(signal.SIGHUP, lambda signum, frame: sys.exit()) signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit()) if use_dbus: - class MandosDBusService(dbus.service.Object): + + @alternate_dbus_interfaces( + { "se.recompile.Mandos": "se.bsnet.fukt.Mandos" }) + class MandosDBusService(DBusObjectWithProperties): """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" @@ -2102,24 +2763,24 @@ @dbus.service.method(_interface, out_signature="ao") def GetAllClients(self): "D-Bus method" - return dbus.Array(c.dbus_object_path - for c in tcp_server.clients) + return dbus.Array(c.dbus_object_path for c in + tcp_server.clients.itervalues()) @dbus.service.method(_interface, out_signature="a{oa{sv}}") def GetAllClientsWithProperties(self): "D-Bus method" return dbus.Dictionary( - ((c.dbus_object_path, c.GetAll("")) - for c in tcp_server.clients), + { c.dbus_object_path: c.GetAll("") + for c in tcp_server.clients.itervalues() }, signature="oa{sv}") @dbus.service.method(_interface, in_signature="o") def RemoveClient(self, object_path): "D-Bus method" - for c in tcp_server.clients: + for c in tcp_server.clients.itervalues(): if c.dbus_object_path == object_path: - tcp_server.clients.remove(c) + del tcp_server.clients[c.name] c.remove_from_connection() # Don't signal anything except ClientRemoved c.disable(quiet=True) @@ -2130,60 +2791,119 @@ del _interface - class MandosDBusServiceTransitional(MandosDBusService): - __metaclass__ = AlternateDBusNamesMetaclass - mandos_dbus_service = MandosDBusServiceTransitional() + mandos_dbus_service = MandosDBusService() def cleanup(): "Cleanup function; run on exit" - service.cleanup() + if zeroconf: + service.cleanup() multiprocessing.active_children() + wnull.close() + if not (tcp_server.clients or client_settings): + return + + # Store client before exiting. Secrets are encrypted with key + # based on what config file has. If config file is + # removed/edited, old secret will thus be unrecovable. + clients = {} + with PGPEngine() as pgp: + for client in tcp_server.clients.itervalues(): + key = client_settings[client.name]["secret"] + client.encrypted_secret = pgp.encrypt(client.secret, + key) + client_dict = {} + + # A list of attributes that can not be pickled + # + secret. + exclude = { "bus", "changedstate", "secret", + "checker", "server_settings" } + for name, typ in inspect.getmembers(dbus.service + .Object): + exclude.add(name) + + client_dict["encrypted_secret"] = (client + .encrypted_secret) + for attr in client.client_structure: + if attr not in exclude: + client_dict[attr] = getattr(client, attr) + + clients[client.name] = client_dict + 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: + pickle.dump((clients, client_settings), stored_state) + tempname = stored_state.name + os.rename(tempname, stored_state_path) + except (IOError, OSError) as e: + if not debug: + try: + os.remove(tempname) + except NameError: + pass + if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST): + logger.warning("Could not save persistent state: {}" + .format(os.strerror(e.errno))) + else: + logger.warning("Could not save persistent state:", + exc_info=e) + raise + + # Delete all clients, and settings from config while tcp_server.clients: - client = tcp_server.clients.pop() + name, client = tcp_server.clients.popitem() if use_dbus: client.remove_from_connection() - client.disable_hook = None # Don't signal anything except ClientRemoved client.disable(quiet=True) if use_dbus: # Emit D-Bus signal - mandos_dbus_service.ClientRemoved(client - .dbus_object_path, - client.name) + mandos_dbus_service.ClientRemoved( + client.dbus_object_path, client.name) + client_settings.clear() atexit.register(cleanup) - for client in tcp_server.clients: + for client in tcp_server.clients.itervalues(): if use_dbus: # Emit D-Bus signal mandos_dbus_service.ClientAdded(client.dbus_object_path) - client.enable() + # Need to initiate checking of clients + if client.enabled: + client.init_checker() tcp_server.enable() tcp_server.server_activate() # Find out what port we got - service.port = tcp_server.socket.getsockname()[1] + if zeroconf: + service.port = tcp_server.socket.getsockname()[1] if use_ipv6: logger.info("Now listening on address %r, port %d," - " flowinfo %d, scope_id %d" - % tcp_server.socket.getsockname()) + " flowinfo %d, scope_id %d", + *tcp_server.socket.getsockname()) else: # IPv4 - logger.info("Now listening on address %r, port %d" - % tcp_server.socket.getsockname()) + logger.info("Now listening on address %r, port %d", + *tcp_server.socket.getsockname()) #service.interface = tcp_server.socket.getsockname()[3] try: - # From the Avahi example code - try: - service.activate() - except dbus.exceptions.DBusException as error: - logger.critical("DBusException: %s", error) - cleanup() - sys.exit(1) - # End of Avahi example code + if zeroconf: + # From the Avahi example code + try: + service.activate() + except dbus.exceptions.DBusException as error: + logger.critical("D-Bus Exception", exc_info=error) + cleanup() + sys.exit(1) + # End of Avahi example code gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN, lambda *args, **kwargs: @@ -2193,7 +2913,7 @@ logger.debug("Starting main loop") main_loop.run() except AvahiError as error: - logger.critical("AvahiError: %s", error) + logger.critical("Avahi Error", exc_info=error) cleanup() sys.exit(1) except KeyboardInterrupt: === modified file 'mandos-clients.conf.xml' --- mandos-clients.conf.xml 2011-10-10 20:29:58 +0000 +++ mandos-clients.conf.xml 2014-06-22 02:19:30 +0000 @@ -3,7 +3,7 @@ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ /etc/mandos/clients.conf"> - + %common; ]> @@ -36,6 +36,7 @@ 2009 2010 2011 + 2012 Teddy Hogeborn Björn Påhlsson @@ -65,8 +66,13 @@ >mandos 8, read by it at startup. The file needs to list all clients that should be able to use - the service. All clients listed will be regarded as enabled, - even if a client was disabled in a previous run of the server. + the service. The settings in this file can be overridden by + runtime changes to the server, which it saves across restarts. + (See the section called PERSISTENT STATE in + mandos8.) However, any changes to this file (including adding and removing + clients) will, at startup, override changes done during runtime. The format starts with a [section @@ -111,7 +117,7 @@ How long to wait for external approval before resorting to use the value. The - default is 0s, i.e. not to wait. + default is PT0S, i.e. not to wait. The format of TIME is the same @@ -161,17 +167,23 @@ This option is optional. - This option allows you to override the default shell - command that the server will use to check if the client is - still up. Any output of the command will be ignored, only - the exit code is checked: If the exit code of the command - is zero, the client is considered up. The command will be - run using /bin/sh + This option overrides the default shell command that the + server will use to check if the client is still up. Any + output of the command will be ignored, only the exit code + is checked: If the exit code of the command is zero, the + client is considered up. The command will be run using + /bin/sh , so PATH will be searched. The default value for the checker command is fping %%(host)s. + >-- %%(host)s. Note that + mandos-keygen, when generating output + to be inserted into this file, normally looks for an SSH + server on the Mandos client, and, if it find one, outputs + a option to check for the + client’s key fingerprint – this is more secure against + spoofing. In addition to normal start time expansion, this option @@ -329,17 +341,26 @@ option. - The TIME is specified as a - space-separated number of values, each of which is a - number and a one-character suffix. The suffix must be one - of d, s, m, - h, and w for days, seconds, - minutes, hours, and weeks, respectively. The values are - added together to give the total time value, so all of - 330s, - 110s 110s 110s, and - 5m 30s will give a value - of five minutes and thirty seconds. + The TIME is specified as an RFC + 3339 duration; for example + P1Y2M3DT4H5M6S meaning + one year, two months, three days, four hours, five + minutes, and six seconds. Some values can be omitted, see + RFC 3339 Appendix A for details. + + + + + + + + + Whether this client should be enabled by default. The + default is true. @@ -389,6 +410,7 @@ approval_duration, created, enabled, + expires, fingerprint, host, interval, @@ -444,8 +466,8 @@ [DEFAULT] -timeout = 5m -interval = 2m +timeout = PT5M +interval = PT2M checker = fping -q -- %%(host)s # Client "foo" @@ -468,15 +490,15 @@ 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O QlnHIvPzEArRQLo= host = foo.example.org -interval = 1m +interval = PT1M # Client "bar" [bar] fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 secfile = /etc/mandos/bar-secret -timeout = 15m +timeout = PT15M approved_by_default = False -approval_delay = 30s +approval_delay = PT30S @@ -491,8 +513,24 @@ mandos.conf 5, mandos + 8, + fping 8 + + + + RFC 3339: Date and Time on the Internet: + Timestamps + + + + The time intervals are in the "duration" format, as + specified in ABNF in Appendix A of RFC 3339. + + + + === modified file 'mandos-ctl' --- mandos-ctl 2011-10-15 16:48:03 +0000 +++ mandos-ctl 2015-04-02 21:56:15 +0000 @@ -3,8 +3,8 @@ # # Mandos Monitor - Control and monitor the Mandos server # -# Copyright © 2008-2011 Teddy Hogeborn -# Copyright © 2008-2011 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 @@ -17,7 +17,8 @@ # 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 . +# along with this program. If not, see +# . # # Contact the authors at . # @@ -25,13 +26,24 @@ from __future__ import (division, absolute_import, print_function, unicode_literals) +try: + from future_builtins import * +except ImportError: + pass + import sys -import dbus import argparse import locale import datetime import re import os +import collections +import doctest + +import dbus + +if sys.version_info.major == 2: + str = unicode locale.setlocale(locale.LC_ALL, "") @@ -52,30 +64,124 @@ "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" server_path = "/" server_interface = domain + ".Mandos" client_interface = domain + ".Mandos.Client" -version = "1.4.1" +version = "1.6.9" -def timedelta_to_milliseconds(td): - """Convert a datetime.timedelta object to milliseconds""" - return ((td.days * 24 * 60 * 60 * 1000) - + (td.seconds * 1000) - + (td.microseconds // 1000)) def milliseconds_to_string(ms): td = datetime.timedelta(0, 0, 0, ms) - return ("%(days)s%(hours)02d:%(minutes)02d:%(seconds)02d" - % { "days": "%dT" % 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): + """Parse an RFC 3339 "duration" and return a datetime.timedelta + + >>> rfc3339_duration_to_delta("P7D") + datetime.timedelta(7) + >>> rfc3339_duration_to_delta("PT60S") + datetime.timedelta(0, 60) + >>> rfc3339_duration_to_delta("PT60M") + datetime.timedelta(0, 3600) + >>> rfc3339_duration_to_delta("PT24H") + datetime.timedelta(1) + >>> rfc3339_duration_to_delta("P1W") + datetime.timedelta(7) + >>> rfc3339_duration_to_delta("PT5M30S") + datetime.timedelta(0, 330) + >>> rfc3339_duration_to_delta("P1DT3M20S") + datetime.timedelta(1, 200) + """ + + # Parsing an RFC 3339 duration with regular expressions is not + # possible - there would have to be multiple places for the same + # values, like seconds. The current code, while more esoteric, is + # cleaner without depending on a parsing library. If Python had a + # built-in library for parsing we would use it, but we'd like to + # 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 + # 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, ))) + token_minute = Token(re.compile(r"(\d+)M"), + datetime.timedelta(minutes=1), + frozenset((token_second, token_end))) + token_hour = Token(re.compile(r"(\d+)H"), + datetime.timedelta(hours=1), + frozenset((token_minute, token_end))) + token_time = Token(re.compile(r"T"), + None, + frozenset((token_hour, token_minute, + token_second))) + token_day = Token(re.compile(r"(\d+)D"), + datetime.timedelta(days=1), + frozenset((token_time, token_end))) + token_month = Token(re.compile(r"(\d+)M"), + datetime.timedelta(weeks=4), + frozenset((token_day, token_end))) + token_year = Token(re.compile(r"(\d+)Y"), + datetime.timedelta(weeks=52), + frozenset((token_month, token_end))) + token_week = Token(re.compile(r"(\d+)W"), + datetime.timedelta(weeks=1), + frozenset((token_end, ))) + token_duration = Token(re.compile(r"P"), None, + frozenset((token_year, token_month, + token_day, token_time, + token_week))) + # Define starting values + value = datetime.timedelta() # Value so far + found_token = None + 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: + # Search for any currently valid tokens + for token in followers: + match = token.regexp.match(s) + if match is not None: + # Token found + if token.value is not None: + # Value found, parse digits + factor = int(match.group(1), 10) + # Add to value so far + value += factor * token.value + # Strip token from string + s = token.regexp.sub("", s, 1) + # Go to found token + found_token = token + # Set valid next tokens + followers = found_token.followers + break + else: + # No currently valid tokens were found + raise ValueError("Invalid RFC 3339 duration") + # End token found + return value + def string_to_delta(interval): """Parse a string and return a datetime.timedelta @@ -93,52 +199,54 @@ >>> string_to_delta("5m 30s") datetime.timedelta(0, 330) """ - timevalue = datetime.timedelta(0) - regexp = re.compile("\d+[dsmhw]") - - for s in regexp.findall(interval): - try: - suffix = unicode(s[-1]) - value = int(s[:-1]) - if suffix == "d": - delta = datetime.timedelta(value) - elif suffix == "s": - delta = datetime.timedelta(0, value) - elif suffix == "m": - delta = datetime.timedelta(0, 0, 0, 0, value) - elif suffix == "h": - delta = datetime.timedelta(0, 0, 0, 0, 0, value) - elif suffix == "w": - delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) - else: - raise ValueError - except (ValueError, IndexError): - raise ValueError - timevalue += delta - return timevalue + + try: + return rfc3339_duration_to_delta(interval) + except ValueError: + pass + + value = datetime.timedelta(0) + regexp = re.compile(r"(\d+)([dsmhw]?)") + + for num, suffix in regexp.findall(interval): + if suffix == "d": + value += datetime.timedelta(int(num)) + elif suffix == "s": + value += datetime.timedelta(0, int(num)) + elif suffix == "m": + value += datetime.timedelta(0, 0, 0, 0, int(num)) + elif suffix == "h": + value += datetime.timedelta(0, 0, 0, 0, 0, int(num)) + elif suffix == "w": + value += datetime.timedelta(0, 0, 0, 0, 0, 0, int(num)) + elif suffix == "": + 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: return "Yes" if value else "No" if keyword in ("Timeout", "Interval", "ApprovalDelay", - "ApprovalDuration"): + "ApprovalDuration", "ExtendedTimeout"): return milliseconds_to_string(value) - return unicode(value) + return str(value) # Create format string to print table rows - format_string = " ".join("%%-%ds" % - max(len(tablewords[key]), - max(len(valuetostring(client[key], - key)) - for client in - clients)) + 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) # Print header line - print(format_string % tuple(tablewords[key] for key in keywords)) + print(format_string.format(**tablewords)) for client in clients: - print(format_string % tuple(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, @@ -160,10 +268,11 @@ options.approve, options.deny)) + def main(): parser = argparse.ArgumentParser() parser.add_argument("--version", action="version", - version = "%%prog %s" % version, + version = "%(prog)s {}".format(version), help="show version number and exit") parser.add_argument("-a", "--all", action="store_true", help="Select all clients") @@ -202,29 +311,35 @@ parser.add_argument("--approval-duration", help="Set duration of one client approval") parser.add_argument("-H", "--host", help="Set host for client") - parser.add_argument("-s", "--secret", type=file, + parser.add_argument("-s", "--secret", + type=argparse.FileType(mode="rb"), help="Set password blob (file) for client") parser.add_argument("-A", "--approve", action="store_true", help="Approve any current client request") parser.add_argument("-D", "--deny", action="store_true", help="Deny any current client request") + parser.add_argument("--check", action="store_true", + help="Run self-test") parser.add_argument("client", nargs="*", help="Client name") options = parser.parse_args() - if has_actions(options) and not options.client and not options.all: + if has_actions(options) and not (options.client or options.all): parser.error("Options require clients names or --all.") if options.verbose and has_actions(options): parser.error("--verbose can only be used alone or with" " --all.") if options.all and not has_actions(options): parser.error("--all requires an action.") + + if options.check: + fail_count, test_count = doctest.testmod() + sys.exit(os.EX_OK if fail_count == 0 else 1) try: 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, @@ -242,7 +357,7 @@ #restore stderr os.dup2(stderrcopy, sys.stderr.fileno()) os.close(stderrcopy) - except dbus.exceptions.DBusException, e: + except dbus.exceptions.DBusException: print("Access denied: Accessing mandos server through dbus.", file=sys.stderr) sys.exit(1) @@ -251,28 +366,26 @@ clients={} if options.all or not options.client: - clients = dict((bus.get_object(busname, path), properties) - for path, properties in - mandos_clients.iteritems()) + clients = { bus.get_object(busname, path): properties + for path, properties in mandos_clients.items() } else: for name in options.client: - for path, client in mandos_clients.iteritems(): + for path, client in mandos_clients.items(): if client["Name"] == name: client_objc = bus.get_object(busname, path) clients[client_objc] = client break else: - print("Client not found on server: %r" % name, - file=sys.stderr) + print("Client not found on server: {!r}" + .format(name), file=sys.stderr) sys.exit(1) 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") @@ -283,18 +396,31 @@ 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: - client.Enable(dbus_interface=client_interface) + set_client_prop("Enabled", dbus.Boolean(True)) if options.disable: - client.Disable(dbus_interface=client_interface) + set_client_prop("Enabled", dbus.Boolean(False)) if options.bump_timeout: - client.CheckedOK(dbus_interface=client_interface) + set_client_prop("LastCheckedOK", "") if options.start_checker: - client.StartChecker(dbus_interface=client_interface) + set_client_prop("CheckerRunning", dbus.Boolean(True)) if options.stop_checker: - client.StopChecker(dbus_interface=client_interface) + set_client_prop("CheckerRunning", dbus.Boolean(False)) if options.is_enabled: sys.exit(0 if client.Get(client_interface, "Enabled", @@ -302,49 +428,29 @@ dbus.PROPERTIES_IFACE) else 1) if options.checker is not None: - client.Set(client_interface, "Checker", - options.checker, - dbus_interface=dbus.PROPERTIES_IFACE) + set_client_prop("Checker", options.checker) if options.host is not None: - client.Set(client_interface, "Host", options.host, - dbus_interface=dbus.PROPERTIES_IFACE) + set_client_prop("Host", options.host) if options.interval is not None: - client.Set(client_interface, "Interval", - timedelta_to_milliseconds - (string_to_delta(options.interval)), - dbus_interface=dbus.PROPERTIES_IFACE) + set_client_prop_ms("Interval", options.interval) if options.approval_delay is not None: - client.Set(client_interface, "ApprovalDelay", - timedelta_to_milliseconds - (string_to_delta(options. - approval_delay)), - dbus_interface=dbus.PROPERTIES_IFACE) + set_client_prop_ms("ApprovalDelay", + options.approval_delay) if options.approval_duration is not None: - client.Set(client_interface, "ApprovalDuration", - timedelta_to_milliseconds - (string_to_delta(options. - approval_duration)), - dbus_interface=dbus.PROPERTIES_IFACE) + set_client_prop_ms("ApprovalDuration", + options.approval_duration) if options.timeout is not None: - client.Set(client_interface, "Timeout", - timedelta_to_milliseconds - (string_to_delta(options.timeout)), - dbus_interface=dbus.PROPERTIES_IFACE) + set_client_prop_ms("Timeout", options.timeout) if options.extended_timeout is not None: - client.Set(client_interface, "ExtendedTimeout", - timedelta_to_milliseconds - (string_to_delta(options.extended_timeout)), - dbus_interface=dbus.PROPERTIES_IFACE) + set_client_prop_ms("ExtendedTimeout", + options.extended_timeout) if options.secret is not None: - client.Set(client_interface, "Secret", - dbus.ByteArray(open(options.secret, - "rb").read()), - dbus_interface=dbus.PROPERTIES_IFACE) + set_client_prop("Secret", + dbus.ByteArray(options.secret.read())) if options.approved_by_default is not None: - client.Set(client_interface, "ApprovedByDefault", - dbus.Boolean(options - .approved_by_default), - dbus_interface=dbus.PROPERTIES_IFACE) + set_client_prop("ApprovedByDefault", + dbus.Boolean(options + .approved_by_default)) if options.approve: client.Approve(dbus.Boolean(True), dbus_interface=client_interface) @@ -352,5 +458,6 @@ client.Approve(dbus.Boolean(False), dbus_interface=client_interface) + if __name__ == "__main__": main() === modified file 'mandos-ctl.xml' --- mandos-ctl.xml 2011-10-05 16:00:56 +0000 +++ mandos-ctl.xml 2012-06-22 23:33:56 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,6 +33,7 @@ 2010 2011 + 2012 Teddy Hogeborn Björn Påhlsson @@ -194,6 +195,10 @@ + + &COMMANDNAME; + + @@ -475,6 +480,15 @@ + + + + + Run self-tests. This includes any unit tests, etc. + + + + === modified file 'mandos-keygen' --- mandos-keygen 2011-10-15 16:48:03 +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-2011 Teddy Hogeborn -# Copyright © 2008-2011 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 @@ -21,18 +21,19 @@ # Contact the authors at . # -VERSION="1.4.1" +VERSION="1.6.9" KEYDIR="/etc/keys/mandos" -KEYTYPE=DSA -KEYLENGTH=2048 -SUBKEYTYPE=ELG-E -SUBKEYLENGTH=2048 +KEYTYPE=RSA +KEYLENGTH=4096 +SUBKEYTYPE=RSA +SUBKEYLENGTH=4096 KEYNAME="`hostname --fqdn 2>/dev/null || hostname`" KEYEMAIL="" -KEYCOMMENT="Mandos client key" +KEYCOMMENT="" KEYEXPIRE=0 FORCE=no +SSH=yes KEYCOMMENT_ORIG="$KEYCOMMENT" mode=keygen @@ -41,12 +42,12 @@ fi # Parse options -TEMP=`getopt --options vhpF:d:t:l:s:L:n:e:c:x:f \ - --longoptions version,help,password,passfile:,dir:,type:,length:,subtype:,sublength:,name:,email:,comment:,expire:,force \ +TEMP=`getopt --options vhpF:d:t:l:s:L:n:e:c:x:fS \ + --longoptions version,help,password,passfile:,dir:,type:,length:,subtype:,sublength:,name:,email:,comment:,expire:,force,no-ssh \ --name "$0" -- "$@"` help(){ -basename="`basename $0`" +basename="`basename "$0"`" cat <&2 + echo "Unknown arguments: '$*'" >&2 exit 1 fi @@ -189,7 +191,7 @@ trap " set +e; \ test -n \"$SECFILE\" && shred --remove \"$SECFILE\"; \ -shred --remove \"$RINGDIR\"/sec*; +shred --remove \"$RINGDIR\"/sec* 2>/dev/null; test -n \"$BATCHFILE\" && rm --force \"$BATCHFILE\"; \ rm --recursive --force \"$RINGDIR\"; tty --quiet && stty echo; \ @@ -204,10 +206,10 @@ cat >"$BATCHFILE" <<-EOF Key-Type: $KEYTYPE Key-Length: $KEYLENGTH - #Key-Usage: encrypt,sign,auth + Key-Usage: sign,auth Subkey-Type: $SUBKEYTYPE Subkey-Length: $SUBKEYLENGTH - #Subkey-Usage: encrypt,sign,auth + Subkey-Usage: encrypt Name-Real: $KEYNAME $KEYCOMMENTLINE $KEYEMAILLINE @@ -229,6 +231,11 @@ date fi + # Make sure trustdb.gpg exists; + # this is a workaround for Debian bug #737128 + gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ + --homedir "$RINGDIR" \ + --import-ownertrust < /dev/null # Generate a new key in the key rings gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ --homedir "$RINGDIR" --trust-model always \ @@ -270,6 +277,29 @@ fi if [ "$mode" = password ]; then + + # Make SSH be 0 or 1 + case "$SSH" in + [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]) SSH=1;; + [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) SSH=0;; + esac + + if [ $SSH -eq 1 ]; then + 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 gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ --homedir "$RINGDIR" --trust-model always --armor \ @@ -280,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}'`" @@ -294,9 +324,11 @@ cat "$PASSFILE" else tty --quiet && stty -echo - read -p "Enter passphrase: " first + echo -n "Enter passphrase: " >&2 + read first tty --quiet && echo >&2 - read -p "Repeat passphrase: " second + echo -n "Repeat passphrase: " >&2 + read second if tty --quiet; then echo >&2 stty echo @@ -336,6 +368,10 @@ /^[^-]/s/^/ /p } }' < "$SECFILE" + if [ -n "$ssh_fingerprint" ]; then + 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 trap - EXIT @@ -346,5 +382,5 @@ shred --remove "$SECFILE" fi # Remove the key rings -shred --remove "$RINGDIR"/sec* +shred --remove "$RINGDIR"/sec* 2>/dev/null rm --recursive --force "$RINGDIR" === modified file 'mandos-keygen.xml' --- mandos-keygen.xml 2011-10-05 16:00:56 +0000 +++ mandos-keygen.xml 2014-06-22 02:19:30 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -34,6 +34,7 @@ 2008 2009 2011 + 2012 Teddy Hogeborn Björn Påhlsson @@ -118,7 +119,10 @@ TIME - + + + + &COMMANDNAME; @@ -144,6 +148,10 @@ + + + + &COMMANDNAME; @@ -214,7 +222,7 @@ Target directory for key files. Default is - /etc/mandos. + /etc/mandos. @@ -226,7 +234,7 @@ TYPE - Key type. Default is DSA. + Key type. Default is RSA. @@ -238,7 +246,7 @@ BITS - Key length in bits. Default is 2048. + Key length in bits. Default is 4096. @@ -250,7 +258,7 @@ KEYTYPE - Subkey type. Default is ELG-E (Elgamal + Subkey type. Default is RSA (Elgamal encryption-only). @@ -263,7 +271,7 @@ BITS - Subkey length in bits. Default is 2048. + Subkey length in bits. Default is 4096. @@ -287,8 +295,7 @@ TEXT - Comment field for key. The default value is - Mandos client key. + Comment field for key. Default is empty. @@ -346,6 +353,22 @@ + + + + + + When or + is given, this option will + prevent &COMMANDNAME; from calling + ssh-keyscan to get an SSH fingerprint + for this host and, if successful, output suitable config + options to use this fingerprint as a + option in the output. This is + otherwise the default behavior. + + + @@ -411,7 +434,7 @@ - /tmp + /tmp Temporary files will be written here if @@ -452,9 +475,9 @@ - Prompt for a password, encrypt it with the key in - /etc/mandos and output a section suitable - for clients.conf. + Prompt for a password, encrypt it with the key in /etc/mandos and output a section + suitable for clients.conf. &COMMANDNAME; --password @@ -502,7 +525,9 @@ mandos 8, mandos-client - 8mandos + 8mandos, + ssh-keyscan + 1 === modified file 'mandos-monitor' --- mandos-monitor 2011-10-15 16:48:03 +0000 +++ mandos-monitor 2014-10-05 20:08:58 +0000 @@ -3,8 +3,8 @@ # # Mandos Monitor - Control and monitor the Mandos server # -# Copyright © 2009-2011 Teddy Hogeborn -# Copyright © 2009-2011 Björn Påhlsson +# Copyright © 2009-2014 Teddy Hogeborn +# Copyright © 2009-2014 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 @@ -17,17 +17,21 @@ # 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 . +# along with this program. If not, see +# . # # Contact the authors at . # from __future__ import (division, absolute_import, print_function, unicode_literals) +try: + from future_builtins import * +except ImportError: + pass import sys import os -import signal import datetime @@ -35,14 +39,18 @@ import urwid from dbus.mainloop.glib import DBusGMainLoop -import gobject +try: + import gobject +except ImportError: + from gi.repository import GObject as gobject import dbus -import UserList - import locale +if sys.version_info.major == 2: + str = unicode + locale.setlocale(locale.LC_ALL, '') import logging @@ -52,15 +60,7 @@ domain = 'se.recompile' server_interface = domain + '.Mandos' client_interface = domain + '.Mandos.Client' -version = "1.4.1" - -# Always run in monochrome mode -urwid.curses_display.curses.has_colors = lambda : False - -# Urwid doesn't support blinking, but we want it. Since we have no -# use for underline on its own, we make underline also always blink. -urwid.curses_display.curses.A_UNDERLINE |= ( - urwid.curses_display.curses.A_BLINK) +version = "1.6.9" def isoformat_to_datetime(iso): "Parse an ISO 8601 date string to a datetime.datetime()" @@ -83,35 +83,32 @@ properties and calls a hook function when any of them are changed. """ - def __init__(self, proxy_object=None, *args, **kwargs): + def __init__(self, proxy_object=None, properties=None, **kwargs): self.proxy = proxy_object # Mandos Client proxy object - - self.properties = dict() + self.properties = dict() if properties is None else properties self.property_changed_match = ( - self.proxy.connect_to_signal("PropertyChanged", - self.property_changed, - client_interface, + self.proxy.connect_to_signal("PropertiesChanged", + self.properties_changed, + dbus.PROPERTIES_IFACE, byte_arrays=True)) - self.properties.update( - self.proxy.GetAll(client_interface, - dbus_interface = dbus.PROPERTIES_IFACE)) - - #XXX This breaks good super behaviour -# super(MandosClientPropertyCache, self).__init__( -# *args, **kwargs) + if properties is None: + self.properties.update( + self.proxy.GetAll(client_interface, + dbus_interface + = dbus.PROPERTIES_IFACE)) + + super(MandosClientPropertyCache, self).__init__(**kwargs) - def property_changed(self, property=None, value=None): - """This is called whenever we get a PropertyChanged signal - It updates the changed property in the "properties" dict. + def properties_changed(self, interface, properties, invalidated): + """This is called whenever we get a PropertiesChanged signal + It updates the changed properties in the "properties" dict. """ # Update properties dict with new value - self.properties[property] = value + self.properties.update(properties) - def delete(self, *args, **kwargs): + def delete(self): self.property_changed_match.remove() - super(MandosClientPropertyCache, self).__init__( - *args, **kwargs) class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache): @@ -119,7 +116,7 @@ """ def __init__(self, server_proxy_object=None, update_hook=None, - delete_hook=None, logger=None, *args, **kwargs): + delete_hook=None, logger=None, **kwargs): # Called on update self.update_hook = update_hook # Called on delete @@ -130,37 +127,15 @@ self.logger = logger self._update_timer_callback_tag = None - self._update_timer_callback_lock = 0 - self.last_checker_failed = False # The widget shown normally self._text_widget = urwid.Text("") # The widget shown when we have focus self._focus_text_widget = urwid.Text("") - super(MandosClientWidget, self).__init__( - update_hook=update_hook, delete_hook=delete_hook, - *args, **kwargs) + super(MandosClientWidget, self).__init__(**kwargs) self.update() self.opened = False - last_checked_ok = isoformat_to_datetime(self.properties - ["LastCheckedOK"]) - if last_checked_ok is None: - self.last_checker_failed = True - else: - self.last_checker_failed = ((datetime.datetime.utcnow() - - last_checked_ok) - > datetime.timedelta - (milliseconds= - self.properties - ["Interval"])) - - if self.last_checker_failed: - self.using_timer(True) - - if self.need_approval: - self.using_timer(True) - self.match_objects = ( self.proxy.connect_to_signal("CheckerCompleted", self.checker_completed, @@ -182,87 +157,71 @@ self.rejected, client_interface, byte_arrays=True)) - #self.logger('Created client %s' % (self.properties["Name"])) + self.logger('Created client {}' + .format(self.properties["Name"]), level=0) - def property_changed(self, property=None, value=None): - super(self, MandosClientWidget).property_changed(property, - value) - if property == "ApprovalPending": - using_timer(bool(value)) - def using_timer(self, flag): """Call this method with True or False when timer should be activated or deactivated. """ - old = self._update_timer_callback_lock - if flag: - self._update_timer_callback_lock += 1 - else: - self._update_timer_callback_lock -= 1 - if old == 0 and self._update_timer_callback_lock: + if flag and self._update_timer_callback_tag is None: # Will update the shown timer value every second self._update_timer_callback_tag = (gobject.timeout_add (1000, self.update_timer)) - elif old and self._update_timer_callback_lock == 0: + elif not (flag or self._update_timer_callback_tag is None): gobject.source_remove(self._update_timer_callback_tag) self._update_timer_callback_tag = None def checker_completed(self, exitstatus, condition, command): if exitstatus == 0: - if self.last_checker_failed: - self.last_checker_failed = False - self.using_timer(False) - #self.logger('Checker for client %s (command "%s")' - # ' was successful' - # % (self.properties["Name"], command)) + self.logger('Checker for client {} (command "{}")' + ' succeeded'.format(self.properties["Name"], + command), level=0) self.update() return # Checker failed - if not self.last_checker_failed: - self.last_checker_failed = True - self.using_timer(True) if os.WIFEXITED(condition): - self.logger('Checker for client %s (command "%s")' - ' failed with exit code %s' - % (self.properties["Name"], command, - os.WEXITSTATUS(condition))) + self.logger('Checker for client {} (command "{}") failed' + ' with exit code {}' + .format(self.properties["Name"], command, + os.WEXITSTATUS(condition))) elif os.WIFSIGNALED(condition): - self.logger('Checker for client %s (command "%s")' - ' was killed by signal %s' - % (self.properties["Name"], command, - os.WTERMSIG(condition))) + self.logger('Checker for client {} (command "{}") was' + ' killed by signal {}' + .format(self.properties["Name"], command, + os.WTERMSIG(condition))) elif os.WCOREDUMP(condition): - self.logger('Checker for client %s (command "%s")' - ' dumped core' - % (self.properties["Name"], command)) + self.logger('Checker for client {} (command "{}") dumped' + ' core'.format(self.properties["Name"], + command)) else: - self.logger('Checker for client %s completed' - ' mysteriously') + self.logger('Checker for client {} completed' + ' mysteriously' + .format(self.properties["Name"])) self.update() def checker_started(self, command): - #self.logger('Client %s started checker "%s"' - # % (self.properties["Name"], unicode(command))) - pass + """Server signals that a checker started.""" + self.logger('Client {} started checker "{}"' + .format(self.properties["Name"], + command), level=0) def got_secret(self): - self.last_checker_failed = False - self.logger('Client %s received its secret' - % self.properties["Name"]) + self.logger('Client {} received its secret' + .format(self.properties["Name"])) def need_approval(self, timeout, default): if not default: - message = 'Client %s needs approval within %s seconds' + message = 'Client {} needs approval within {} seconds' else: - message = 'Client %s will get its secret in %s seconds' - self.logger(message - % (self.properties["Name"], timeout/1000)) - self.using_timer(True) + message = 'Client {} will get its secret in {} seconds' + self.logger(message.format(self.properties["Name"], + timeout/1000)) def rejected(self, reason): - self.logger('Client %s was rejected; reason: %s' - % (self.properties["Name"], reason)) + self.logger('Client {} was rejected; reason: {}' + .format(self.properties["Name"], reason)) def selectable(self): """Make this a "selectable" widget. @@ -290,14 +249,14 @@ "bold-underline-blink": "bold-underline-blink-standout", } - + # Rebuild focus and non-focus widgets using current properties - + # Base part of a client. Name! - base = ('%(name)s: ' - % {"name": self.properties["Name"]}) + base = '{name}: '.format(name=self.properties["Name"]) if not self.properties["Enabled"]: message = "DISABLED" + self.using_timer(False) elif self.properties["ApprovalPending"]: timeout = datetime.timedelta(milliseconds = self.properties @@ -305,31 +264,36 @@ last_approval_request = isoformat_to_datetime( self.properties["LastApprovalRequest"]) if last_approval_request is not None: - timer = timeout - (datetime.datetime.utcnow() - - last_approval_request) + timer = max(timeout - (datetime.datetime.utcnow() + - last_approval_request), + datetime.timedelta()) else: timer = datetime.timedelta() if self.properties["ApprovedByDefault"]: - message = "Approval in %s. (d)eny?" + message = "Approval in {}. (d)eny?" else: - message = "Denial in %s. (a)pprove?" - message = message % unicode(timer).rsplit(".", 1)[0] - elif self.last_checker_failed: - # When checker has failed, print a timer until client expires + message = "Denial in {}. (a)pprove?" + message = message.format(str(timer).rsplit(".", 1)[0]) + self.using_timer(True) + elif self.properties["LastCheckerStatus"] != 0: + # When checker has failed, show timer until client expires expires = self.properties["Expires"] if expires == "": timer = datetime.timedelta(0) else: - expires = datetime.datetime.strptime(expires, - '%Y-%m-%dT%H:%M:%S.%f') - timer = expires - datetime.datetime.utcnow() + expires = (datetime.datetime.strptime + (expires, '%Y-%m-%dT%H:%M:%S.%f')) + timer = max(expires - datetime.datetime.utcnow(), + datetime.timedelta()) message = ('A checker has failed! Time until client' - ' gets disabled: %s' - % unicode(timer).rsplit(".", 1)[0]) + ' gets disabled: {}' + .format(str(timer).rsplit(".", 1)[0])) + self.using_timer(True) else: message = "enabled" - self._text = "%s%s" % (base, message) - + self.using_timer(False) + self._text = "{}{}".format(base, message) + if not urwid.supports_unicode(): self._text = self._text.encode("ascii", "replace") textlist = [("normal", self._text)] @@ -352,7 +316,7 @@ self.update() return True # Keep calling this - def delete(self, *args, **kwargs): + def delete(self, **kwargs): if self._update_timer_callback_tag is not None: gobject.source_remove(self._update_timer_callback_tag) self._update_timer_callback_tag = None @@ -361,7 +325,7 @@ self.match_objects = () if self.delete_hook is not None: self.delete_hook(self) - return super(MandosClientWidget, self).delete(*args, **kwargs) + return super(MandosClientWidget, self).delete(**kwargs) def render(self, maxcolrow, focus=False): """Render differently if we have focus. @@ -409,15 +373,15 @@ else: return key - def property_changed(self, property=None, value=None, - *args, **kwargs): - """Call self.update() if old value is not new value. + def properties_changed(self, interface, properties, invalidated): + """Call self.update() if any properties changed. This overrides the method from MandosClientPropertyCache""" - property_name = unicode(property) - old_value = self.properties.get(property_name) - super(MandosClientWidget, self).property_changed( - property=property, value=value, *args, **kwargs) - if self.properties.get(property_name) != old_value: + old_values = { key: self.properties.get(key) + for key in properties.keys() } + super(MandosClientWidget, self).properties_changed( + interface, properties, invalidated) + if any(old_values[key] != self.properties.get(key) + for key in old_values): self.update() @@ -426,8 +390,8 @@ "down" key presses, thus not allowing any containing widgets to use them as an excuse to shift focus away from this widget. """ - def keypress(self, maxcolrow, key): - ret = super(ConstrainedListBox, self).keypress(maxcolrow, key) + def keypress(self, *args, **kwargs): + ret = super(ConstrainedListBox, self).keypress(*args, **kwargs) if ret in ("up", "down"): return return ret @@ -437,7 +401,7 @@ """This is the entire user interface - the whole screen with boxes, lists of client widgets, etc. """ - def __init__(self, max_log_length=1000): + def __init__(self, max_log_length=1000, log_level=1): DBusGMainLoop(set_as_default=True) self.screen = urwid.curses_display.Screen() @@ -446,20 +410,21 @@ ("normal", "default", "default", None), ("bold", - "default", "default", "bold"), + "bold", "default", "bold"), ("underline-blink", - "default", "default", "underline"), + "underline,blink", "default", "underline,blink"), ("standout", - "default", "default", "standout"), + "standout", "default", "standout"), ("bold-underline-blink", - "default", "default", ("bold", "underline")), + "bold,underline,blink", "default", "bold,underline,blink"), ("bold-standout", - "default", "default", ("bold", "standout")), + "bold,standout", "default", "bold,standout"), ("underline-blink-standout", - "default", "default", ("underline", "standout")), + "underline,blink,standout", "default", + "underline,blink,standout"), ("bold-underline-blink-standout", - "default", "default", ("bold", "underline", - "standout")), + "bold,underline,blink,standout", "default", + "bold,underline,blink,standout"), )) if urwid.supports_unicode(): @@ -480,6 +445,8 @@ self.log = [] self.max_log_length = max_log_length + self.log_level = log_level + # We keep a reference to the log widget so we can remove it # from the ListWalker without it getting destroyed self.logbox = ConstrainedListBox(self.log) @@ -497,53 +464,11 @@ self.busname = domain + '.Mandos' self.main_loop = gobject.MainLoop() - self.bus = dbus.SystemBus() - mandos_dbus_objc = self.bus.get_object( - self.busname, "/", follow_name_owner_changes=True) - self.mandos_serv = dbus.Interface(mandos_dbus_objc, - dbus_interface - = server_interface) - try: - mandos_clients = (self.mandos_serv - .GetAllClientsWithProperties()) - except dbus.exceptions.DBusException: - mandos_clients = dbus.Dictionary() - - (self.mandos_serv - .connect_to_signal("ClientRemoved", - self.find_and_remove_client, - dbus_interface=server_interface, - byte_arrays=True)) - (self.mandos_serv - .connect_to_signal("ClientAdded", - self.add_new_client, - dbus_interface=server_interface, - byte_arrays=True)) - (self.mandos_serv - .connect_to_signal("ClientNotFound", - self.client_not_found, - dbus_interface=server_interface, - byte_arrays=True)) - for path, client in mandos_clients.iteritems(): - client_proxy_object = self.bus.get_object(self.busname, - path) - self.add_client(MandosClientWidget(server_proxy_object - =self.mandos_serv, - proxy_object - =client_proxy_object, - properties=client, - update_hook - =self.refresh, - delete_hook - =self.remove_client, - logger - =self.log_message), - path=path) def client_not_found(self, fingerprint, address): - self.log_message(("Client with address %s and fingerprint %s" - " could not be found" % (address, - fingerprint))) + self.log_message("Client with address {} and fingerprint {}" + " could not be found" + .format(address, fingerprint)) def rebuild(self): """This rebuilds the User Interface. @@ -559,15 +484,20 @@ self.divider))) if self.log_visible: self.uilist.append(self.logbox) - pass self.topwidget = urwid.Pile(self.uilist) - def log_message(self, message): + def log_message(self, message, level=1): + """Log message formatted with timestamp""" + if level < self.log_level: + return timestamp = datetime.datetime.now().isoformat() - self.log_message_raw(timestamp + ": " + message) + self.log_message_raw("{}: {}".format(timestamp, message), + level=level) - def log_message_raw(self, markup): + def log_message_raw(self, markup, level=1): """Add a log message to the log buffer.""" + if level < self.log_level: + return self.log.append(urwid.Text(markup, wrap=self.log_wrap)) if (self.max_log_length and len(self.log) > self.max_log_length): @@ -580,8 +510,8 @@ """Toggle visibility of the log buffer.""" self.log_visible = not self.log_visible self.rebuild() - #self.log_message("Log visibility changed to: " - # + unicode(self.log_visible)) + self.log_message("Log visibility changed to: {}" + .format(self.log_visible), level=0) def change_log_display(self): """Change type of log display. @@ -592,7 +522,8 @@ self.log_wrap = "clip" for textwidget in self.log: textwidget.set_wrap_mode(self.log_wrap) - #self.log_message("Wrap mode: " + self.log_wrap) + self.log_message("Wrap mode: {}".format(self.log_wrap), + level=0) def find_and_remove_client(self, path, name): """Find a client by its object path and remove it. @@ -603,8 +534,8 @@ client = self.clients_dict[path] except KeyError: # not found? - self.log_message("Unknown client %r (%r) removed", name, - path) + self.log_message("Unknown client {!r} ({!r}) removed" + .format(name, path)) return client.delete() @@ -627,7 +558,7 @@ if path is None: path = client.proxy.object_path self.clients_dict[path] = client - self.clients.sort(None, lambda c: c.properties["Name"]) + self.clients.sort(key=lambda c: c.properties["Name"]) self.refresh() def remove_client(self, client, path=None): @@ -635,11 +566,6 @@ if path is None: path = client.proxy.object_path del self.clients_dict[path] - if not self.clients_dict: - # Work around bug in Urwid 0.9.8.3 - if a SimpleListWalker - # is completely emptied, we need to recreate it. - self.clients = urwid.SimpleListWalker([]) - self.rebuild() self.refresh() def refresh(self): @@ -649,6 +575,52 @@ def run(self): """Start the main loop and exit when it's done.""" + self.bus = dbus.SystemBus() + mandos_dbus_objc = self.bus.get_object( + self.busname, "/", follow_name_owner_changes=True) + self.mandos_serv = dbus.Interface(mandos_dbus_objc, + dbus_interface + = server_interface) + try: + mandos_clients = (self.mandos_serv + .GetAllClientsWithProperties()) + if not mandos_clients: + self.log_message_raw(("bold", "Note: Server has no clients.")) + except dbus.exceptions.DBusException: + self.log_message_raw(("bold", "Note: No Mandos server running.")) + mandos_clients = dbus.Dictionary() + + (self.mandos_serv + .connect_to_signal("ClientRemoved", + self.find_and_remove_client, + dbus_interface=server_interface, + byte_arrays=True)) + (self.mandos_serv + .connect_to_signal("ClientAdded", + self.add_new_client, + dbus_interface=server_interface, + byte_arrays=True)) + (self.mandos_serv + .connect_to_signal("ClientNotFound", + self.client_not_found, + dbus_interface=server_interface, + byte_arrays=True)) + for path, client in mandos_clients.items(): + client_proxy_object = self.bus.get_object(self.busname, + path) + self.add_client(MandosClientWidget(server_proxy_object + =self.mandos_serv, + proxy_object + =client_proxy_object, + properties=client, + update_hook + =self.refresh, + delete_hook + =self.remove_client, + logger + =self.log_message), + path=path) + self.refresh() self._input_callback_tag = (gobject.io_add_watch (sys.stdin.fileno(), @@ -686,7 +658,8 @@ elif key == "window resize": self.size = self.screen.get_cols_rows() self.refresh() - elif key == "\f": # Ctrl-L + elif key == "ctrl l": + self.screen.clear() self.refresh() elif key == "l" or key == "D": self.toggle_log_display() @@ -704,7 +677,9 @@ "?: Help", "l: Log window toggle", "TAB: Switch window", - "w: Wrap (log)")))) + "w: Wrap (log lines)", + "v: Toggle verbose log", + )))) self.log_message_raw(("bold", " " .join(("Clients:", @@ -723,6 +698,13 @@ else: self.topwidget.set_focus(self.logbox) self.refresh() + elif key == "v": + if self.log_level == 0: + self.log_level = 1 + self.log_message("Verbose mode: Off") + else: + self.log_level = 0 + self.log_message("Verbose mode: On") #elif (key == "end" or key == "meta >" or key == "G" # or key == ">"): # pass # xxx end-of-buffer @@ -751,7 +733,7 @@ ui.run() except KeyboardInterrupt: ui.screen.stop() -except Exception, e: - ui.log_message(unicode(e)) +except Exception as e: + ui.log_message(str(e)) ui.screen.stop() raise === modified file 'mandos-monitor.xml' --- mandos-monitor.xml 2011-10-05 16:00:56 +0000 +++ mandos-monitor.xml 2014-07-14 21:41:08 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,6 +33,7 @@ 2010 2011 + 2012 Teddy Hogeborn Björn Påhlsson @@ -128,6 +129,10 @@ Toggle log window line wrap + v + Toggle verbose logging + + Up, Ctrl-P, k Move up a line === modified file 'mandos-options.xml' --- mandos-options.xml 2009-02-13 05:38:21 +0000 +++ mandos-options.xml 2015-03-10 18:03:38 +0000 @@ -46,15 +46,31 @@ not run in debug mode. + + GnuTLS priority string for the TLS handshake. + The default is SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA + :+SIGN-RSA-SHA224:+SIGN-RSA-RMD160. + See gnutls_priority_init + 3 for the syntax. + Warning: changing this may make the + TLS handshake fail, making server-client + communication impossible. Changing this option may also make the + network traffic decryptable by an attacker. + + GnuTLS priority string for the TLS handshake. The default is SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP. See - gnutls_priority_init + >SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA. + 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. @@ -86,4 +102,35 @@ advanced users should consider changing this option. + + This option controls whether the server will restore its state + from the last time it ran. Default is to restore last state. + + + + Directory to save (and restore) state in. Default is + /var/lib/mandos. + + + + If this option is used, the server will not create a new network + socket, but will instead use the supplied file descriptor. By + default, the server will create a new network socket. + + + + This option will make the server run in the foreground and not + write a PID file. The default is to not run + in the foreground, except in mode, which + implies this option. + + + + This option controls whether the server will announce its + existence using Zeroconf. Default is to use Zeroconf. If + Zeroconf is not used, a number or a + is required. + + === modified file 'mandos.conf' --- mandos.conf 2009-02-13 05:38:21 +0000 +++ mandos.conf 2015-04-02 18:59:29 +0000 @@ -4,32 +4,26 @@ # These are the default values for the server, uncomment and change # them if needed. - # If "interface" is set, the server will only listen to a specific # network interface. ;interface = - # If "address" is set, the server will only listen to a specific # address. This must currently be an IPv6 address; an IPv4 address # can be specified using the "::FFFF:192.0.2.3" syntax. Also, if this # is a link-local address, an interface should be set above. ;address = - # If "port" is set, the server to bind to that port. By default, the # server will listen to an arbitrary port. ;port = - # If "debug" is true, the server will run in the foreground and print # a lot of debugging information. ;debug = False - # GnuTLS priority for the TLS handshake. See gnutls_priority_init(3). -;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP - +;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA:+SIGN-RSA-SHA224:+SIGN-RSA-RMD160 # Zeroconf service name. You need to change this if you for some # reason want to run more than one server on the same *host*. @@ -42,3 +36,18 @@ # Whether to use IPv6. (Changing this is NOT recommended.) ;use_ipv6 = True + +# Whether to restore saved state on startup +;restore = True + +# The directory where state is saved +;statedir = /var/lib/mandos + +# Whether to run in the foreground +;foreground = False + +# File descriptor number to use for network socket +;socket = + +# Whether to use ZeroConf; if false, requires port or socket +;zeroconf = True === modified file 'mandos.conf.xml' --- mandos.conf.xml 2011-10-05 16:00:56 +0000 +++ mandos.conf.xml 2015-04-02 18:59:29 +0000 @@ -3,7 +3,7 @@ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ /etc/mandos/mandos.conf"> - + %common; ]> @@ -35,6 +35,8 @@ 2008 2009 2011 + 2012 + 2013 Teddy Hogeborn Björn Påhlsson @@ -119,7 +121,8 @@ - + @@ -154,6 +157,33 @@ + + + + + + + + + + + + + + + + + + + + + @@ -193,11 +223,13 @@ 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 +restore = True +statedir = /var/lib/mandos === modified file 'mandos.lsm' --- mandos.lsm 2011-10-15 16:48:03 +0000 +++ mandos.lsm 2014-10-05 20:08:58 +0000 @@ -1,22 +1,23 @@ Begin4 Title: Mandos -Version: 1.4.1 -Entered-date: 2011-10-15 +Version: 1.6.9 +Entered-date: 2014-10-05 Description: The Mandos system allows computers to have encrypted -root file systems and at the same time be capable of remote and/or -unattended reboots. + root file systems and at the same time be capable of + remote and/or unattended reboots. Keywords: boot, encryption, luks, cryptsetup, network, openpgp, -tls, dm-crypt + tls, dm-crypt Author: teddy@recompile.se (Teddy Hogeborn), belorn@recompile.se (Björn Påhlsson) Maintained-by: teddy@recompile.se (Teddy Hogeborn), belorn@recompile.se (Björn Påhlsson) -Primary-site: http://www.recompile.se/mandos - 138K mandos_1.4.1.orig.tar.gz -Alternate-site: ftp://ftp.recompile.se/pub/mandos - 138K mandos_1.4.1.orig.tar.gz +Primary-site: http://www.recompile.se/mandos + 161K mandos_1.6.9.orig.tar.gz +Alternate-site: ftp://ftp.recompile.se/pub/mandos + 161K mandos_1.6.9.orig.tar.gz Platforms: Requires GCC, GNU libC, Avahi, GnuPG, Python 2.6, and -various other libraries. While made for Debian GNU/Linux, it is -probably portable to other distributions, but not other Unixes. -Copying-policy: GNU General Public License version 3.0 or later + various other libraries. While made for Debian + GNU/Linux, it is probably portable to other + distributions, but not other Unixes. +Copying-policy: GNU General Public License version 3.0 or later End === added file 'mandos.service' --- mandos.service 1970-01-01 00:00:00 +0000 +++ mandos.service 2014-10-05 19:39:25 +0000 @@ -0,0 +1,22 @@ +[Unit] +Description=Server of encrypted passwords to Mandos clients +Documentation=man:intro(8mandos) man:mandos(8) + +[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 +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. +#ExecStart=/usr/sbin/mandos --foreground --socket=0 +#StandardInput=socket + +[Install] +WantedBy=multi-user.target === modified file 'mandos.xml' --- mandos.xml 2011-10-05 16:00:56 +0000 +++ mandos.xml 2015-01-25 00:02:51 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -35,6 +35,8 @@ 2009 2010 2011 + 2012 + 2013 Teddy Hogeborn Björn Påhlsson @@ -94,6 +96,18 @@ + + + + + + + + + + &COMMANDNAME; @@ -222,7 +236,8 @@ - + @@ -275,6 +290,48 @@ + + + + + + + See also . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -357,9 +414,7 @@ extended timeout, checker program, and interval between checks can be configured both globally and per client; see mandos-clients.conf - 5. A client successfully - receiving its password will also be treated as a successful - checker run. + 5. @@ -387,12 +442,24 @@ LOGGING The server will send log message with various severity levels to - /dev/log. With the + /dev/log. With the option, it will log even more messages, and also show them on the console. + + PERSISTENT STATE + + Client settings, initially read from + clients.conf, are persistent across + restarts, and run-time changes will override settings in + clients.conf. However, if a setting is + changed (or a client added, or removed) in + clients.conf, this will take precedence. + + + D-BUS INTERFACE @@ -460,11 +527,29 @@ - /var/run/mandos.pid + /run/mandos.pid The file containing the process id of the &COMMANDNAME; process started last. + Note: If the /run directory does not + exist, /var/run/mandos.pid will be + used instead. + + + + + /dev/log + + + /var/lib/mandos + + + Directory where persistent state will be saved. Change + this with the option. See + also the option. @@ -498,20 +583,9 @@ backtrace. This could be considered a feature. - Currently, if a client is disabled due to having timed out, the - server does not record this fact onto permanent storage. This - has some security implications, see . - - There is no fine-grained control over logging and debug output. - Debug mode is conflated with running in the foreground. - - - The console log messages do not show a time stamp. - - This server does not check the expire time of clients’ OpenPGP keys. @@ -530,9 +604,9 @@ Run the server in debug mode, read configuration files from - the ~/mandos directory, and use the - Zeroconf service name Test to not collide with - any other official Mandos server on this host: + the ~/mandos directory, + and use the Zeroconf service name Test to not + collide with any other official Mandos server on this host: @@ -587,21 +661,6 @@ compromised if they are gone for too long. - If a client is compromised, its downtime should be duly noted - by the server which would therefore disable the client. But - if the server was ever restarted, it would re-read its client - list from its configuration file and again regard all clients - therein as enabled, and hence eligible to receive their - passwords. Therefore, be careful when restarting servers if - it is suspected that a client has, in fact, been compromised - by parties who may now be running a fake Mandos client with - the keys from the non-encrypted initial RAM - image of the client host. What should be done in that case - (if restarting the server program really is necessary) is to - stop the server program, edit the configuration file to omit - any suspect clients, and restart the server program. - - For more details on client-side security, see mandos-client 8mandos. @@ -648,8 +707,7 @@ - GnuTLS + GnuTLS @@ -693,12 +751,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. @@ -714,8 +772,8 @@ - RFC 5081: Using OpenPGP Keys for Transport Layer - Security + RFC 6091: Using OpenPGP Keys for Transport Layer + Security (TLS) Authentication === modified file 'network-hooks.d/bridge' --- network-hooks.d/bridge 2011-11-24 20:15:24 +0000 +++ network-hooks.d/bridge 2012-06-13 22:06:57 +0000 @@ -6,6 +6,9 @@ # configuration file(s) should be copied into the # /etc/mandos/network-hooks.d directory. # +# Copyright © 2012 Teddy Hogeborn +# Copyright © 2012 Björn Påhlsson +# # Copying and distribution of this file, with or without modification, # are permitted in any medium without royalty provided the copyright # notice and this notice are preserved. This file is offered as-is, @@ -15,47 +18,76 @@ CONFIG="$MANDOSNETHOOKDIR/bridge.conf" -# Read config file, which must set "BRIDGE", "PORTS", and optionally -# "IPADDRS" and "ROUTES". +addrtoif(){ + grep -liFe "$1" /sys/class/net/*/address \ + | sed -e 's,.*/\([^/]*\)/[^/]*,\1,' -e "/^${BRIDGE}\$/d" +} + +# Read config file, which must set "BRIDGE", "PORT_ADDRESSES", and +# optionally "IPADDRS" and "ROUTES". if [ -e "$CONFIG" ]; then . "$CONFIG" fi -if [ -z "$BRIDGE" -o -z "$PORTS" ]; then - exit -fi - -if [ -n "$DEVICE" -a "$DEVICE" != "$BRIDGE" ]; then - exit -fi - -case "$1" in - start) - brctl addbr "$BRIDGE" - for port in $PORTS; do - brctl addif "$BRIDGE" "$port" - done - ip link set up "$BRIDGE" - if [ -n "$IPADDRS" ]; then - for ipaddr in $IPADDRS; do - ip addr add "$ipaddr" dev "$BRIDGE" - done - fi - if [ -n "$ROUTES" ]; then - for route in $ROUTES; do - ip route add "$route" dev "$BRIDGE" - done - fi - ;; - stop) - ip link set down "$BRIDGE" - for port in $PORTS; do - brctl delif "$BRIDGE" "$port" - done - brctl delbr "$BRIDGE" +if [ -z "$BRIDGE" -o -z "$PORT_ADDRESSES" ]; then + exit +fi + +if [ -n "$DEVICE" ]; then + case "$DEVICE" in + *,"$BRIDGE"|*,"$BRIDGE",*|"$BRIDGE",*|"$BRIDGE") :;; + *) exit;; + esac +fi + +brctl="/sbin/brctl" +for b in "$brctl" /usr/sbin/brctl; do + if [ -e "$b" ]; then + brctl="$b" + break + fi +done + +do_start(){ + "$brctl" addbr "$BRIDGE" + for address in $PORT_ADDRESSES; do + interface=`addrtoif "$address"` + "$brctl" addif "$BRIDGE" "$interface" + ip link set dev "$interface" up + done + ip link set dev "$BRIDGE" up + sleep "${DELAY%%.*}" + if [ -n "$IPADDRS" ]; then + for ipaddr in $IPADDRS; do + ip addr add "$ipaddr" dev "$BRIDGE" + done + fi + if [ -n "$ROUTES" ]; then + for route in $ROUTES; do + ip route add "$route" dev "$BRIDGE" + done + fi +} + +do_stop(){ + ip link set dev "$BRIDGE" down + for address in $PORT_ADDRESSES; do + interface=`addrtoif "$address"` + ip link set dev "$interface" down + "$brctl" delif "$BRIDGE" "$interface" + done + "$brctl" delbr "$BRIDGE" +} + +case "${MODE:-$1}" in + start|stop) + do_"${MODE:-$1}" ;; files) echo /bin/ip - echo /usr/bin/brctl + echo "$brctl" + ;; + modules) + echo bridge ;; esac === modified file 'network-hooks.d/bridge.conf' --- network-hooks.d/bridge.conf 2011-11-24 20:15:24 +0000 +++ network-hooks.d/bridge.conf 2011-12-31 13:25:58 +0000 @@ -2,7 +2,7 @@ #BRIDGE=br0 -#PORTS="eth0 eth1" +#PORT_ADDRESSES="00:11:22:33:44:55 11:22:33:44:55:66" ## Optional === added file 'network-hooks.d/openvpn' --- network-hooks.d/openvpn 1970-01-01 00:00:00 +0000 +++ network-hooks.d/openvpn 2012-06-13 22:06:57 +0000 @@ -0,0 +1,66 @@ +#!/bin/sh +# +# This is an example of a Mandos client network hook. This hook +# brings up an OpenVPN interface as specified in a separate +# configuration file. To be used, this file and any needed +# configuration file(s) should be copied into the +# /etc/mandos/network-hooks.d directory. +# +# Copyright © 2012 Teddy Hogeborn +# Copyright © 2012 Björn Påhlsson +# +# Copying and distribution of this file, with or without modification, +# are permitted in any medium without royalty provided the copyright +# notice and this notice are preserved. This file is offered as-is, +# without any warranty. + +set -e + +CONFIG="openvpn.conf" + +# Extract the "dev" setting from the config file +VPNDEVICE=`sed -n -e 's/[[:space:]]#.*//' \ + -e 's/^[[:space:]]*dev[[:space:]]\+//p' \ + "$MANDOSNETHOOKDIR/$CONFIG"` + +PIDFILE=/run/openvpn-mandos.pid + +# Exit if no device set in config +if [ -z "$VPNDEVICE" ]; then + exit +fi + +# Exit if DEVICE is set and it doesn't match the VPN interface +if [ -n "$DEVICE" ]; then + case "$DEVICE" in + *,"$VPNDEVICE"*|"$VPNDEVICE"*) :;; + *) exit;; + esac +fi + +openvpn=/usr/sbin/openvpn + +do_start(){ + "$openvpn" --cd "$MANDOSNETHOOKDIR" --daemon 'openvpn(Mandos)' \ + --writepid "$PIDFILE" --config "$CONFIG" + sleep "$DELAY" +} + +do_stop(){ + PID="`cat \"$PIDFILE\"`" + if [ "$PID" -gt 0 ]; then + kill "$PID" + fi +} + +case "${MODE:-$1}" in + start|stop) + do_"${MODE:-$1}" + ;; + files) + echo "$openvpn" + ;; + modules) + echo tun + ;; +esac === added file 'network-hooks.d/openvpn.conf' --- network-hooks.d/openvpn.conf 1970-01-01 00:00:00 +0000 +++ network-hooks.d/openvpn.conf 2011-12-02 16:52:50 +0000 @@ -0,0 +1,19 @@ +# Sample OpenVPN configuration file +# Uncomment and change - see openvpn(8) + +# Network device. +#dev tun + +# Our remote peer +#remote 192.0.2.3 +#float 192.0.2.3 +#port 1194 + +# VPN endpoints +#ifconfig 10.1.0.1 10.1.0.2 + +# A pre-shared static key +#secret openvpn.key + +# Cipher +#cipher AES-128-CBC === added file 'network-hooks.d/wireless' --- network-hooks.d/wireless 1970-01-01 00:00:00 +0000 +++ network-hooks.d/wireless 2012-06-13 22:06:57 +0000 @@ -0,0 +1,165 @@ +#!/bin/sh +# +# This is an example of a Mandos client network hook. This hook +# brings up a wireless interface as specified in a separate +# configuration file. To be used, this file and any needed +# configuration file(s) should be copied into the +# /etc/mandos/network-hooks.d directory. +# +# Copyright © 2012 Teddy Hogeborn +# Copyright © 2012 Björn Påhlsson +# +# Copying and distribution of this file, with or without modification, +# are permitted in any medium without royalty provided the copyright +# notice and this notice are preserved. This file is offered as-is, +# without any warranty. + +set -e + +RUNDIR="/run" +CTRL="$RUNDIR/wpa_supplicant-global" +CTRLDIR="$RUNDIR/wpa_supplicant" +PIDFILE="$RUNDIR/wpa_supplicant-mandos.pid" + +CONFIG="$MANDOSNETHOOKDIR/wireless.conf" + +addrtoif(){ + grep -liFe "$1" /sys/class/net/*/address \ + | sed -e 's,.*/\([^/]*\)/[^/]*,\1,' +} + +# Read config file +if [ -e "$CONFIG" ]; then + . "$CONFIG" +else + exit +fi + +ifkeys=`sed -n -e 's/^ADDRESS_\([^=]*\)=.*/\1/p' "$CONFIG" | sort -u` + +# Exit if DEVICE is set and is not any of the wireless interfaces +if [ -n "$DEVICE" ]; then + while :; do + for KEY in $ifkeys; do + ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"` + INTERFACE=`addrtoif "$ADDRESS"` + + case "$DEVICE" in + *,"$INTERFACE"|*,"$INTERFACE",*|"$INTERFACE",*|"$INTERFACE") + break 2;; + esac + done + exit + done +fi + +wpa_supplicant=/sbin/wpa_supplicant +wpa_cli=/sbin/wpa_cli +ip=/bin/ip + +# Used by the wpa_interface_* functions in the wireless.conf file +wpa_cli_set(){ + case "$1" in + ssid|psk) arg="\"$2\"" ;; + *) arg="$2" ;; + esac + "$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" set_network "$NETWORK" \ + "$1" "$arg" 2>&1 | sed -e '/^OK$/d' +} + +if [ $VERBOSITY -gt 0 ]; then + WPAS_OPTIONS="-d $WPAS_OPTIONS" +fi +if [ -n "$PIDFILE" ]; then + WPAS_OPTIONS="-P$PIDFILE $WPAS_OPTIONS" +fi + +do_start(){ + mkdir -m u=rwx,go= -p "$CTRLDIR" + "$wpa_supplicant" -B -g "$CTRL" -p "$CTRLDIR" $WPAS_OPTIONS + for KEY in $ifkeys; do + ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"` + INTERFACE=`addrtoif "$ADDRESS"` + DRIVER=`eval 'echo "$WPA_DRIVER_'"$KEY"\"` + IFDELAY=`eval 'echo "$DELAY_'"$KEY"\"` + "$wpa_cli" -g "$CTRL" interface_add "$INTERFACE" "" \ + "${DRIVER:-wext}" "$CTRLDIR" > /dev/null \ + | sed -e '/^OK$/d' + NETWORK=`"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" add_network` + eval wpa_interface_"$KEY" + "$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" enable_network \ + "$NETWORK" | sed -e '/^OK$/d' + sleep "${IFDELAY:-$DELAY}" & + sleep=$! + while :; do + kill -0 $sleep 2>/dev/null || break + STATE=`"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" status \ + | sed -n -e 's/^wpa_state=//p'` + if [ "$STATE" = COMPLETED ]; then + while :; do + kill -0 $sleep 2>/dev/null || break 2 + UP=`cat /sys/class/net/"$INTERFACE"/operstate` + if [ "$UP" = up ]; then + kill $sleep 2>/dev/null + break 2 + fi + sleep 1 + done + fi + sleep 1 + done & + wait $sleep || : + IPADDRS=`eval 'echo "$IPADDRS_'"$KEY"\"` + if [ -n "$IPADDRS" ]; then + if [ "$IPADDRS" = dhcp ]; then + ipconfig -c dhcp -d "$INTERFACE" || : + #dhclient "$INTERFACE" + else + for ipaddr in $IPADDRS; do + "$ip" addr add "$ipaddr" dev "$INTERFACE" + done + fi + fi + ROUTES=`eval 'echo "$ROUTES_'"$KEY"\"` + if [ -n "$ROUTES" ]; then + for route in $ROUTES; do + "$ip" route add "$route" dev "$INTERFACE" + done + fi + done +} + +do_stop(){ + "$wpa_cli" -g "$CTRL" terminate 2>&1 | sed -e '/^OK$/d' + for KEY in $ifkeys; do + ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"` + INTERFACE=`addrtoif "$ADDRESS"` + "$ip" addr show scope global permanent dev "$INTERFACE" \ + | while read type addr rest; do + case "$type" in + inet|inet6) + "$ip" addr del "$addr" dev "$INTERFACE" + ;; + esac + done + "$ip" link set dev "$INTERFACE" down + done +} + +case "${MODE:-$1}" in + start|stop) + do_"${MODE:-$1}" + ;; + files) + echo "$wpa_supplicant" + echo "$wpa_cli" + echo "$ip" + ;; + modules) + if [ "$IPADDRS" = dhcp ]; then + echo af_packet + fi + sed -n -e 's/#.*$//' -e 's/[ ]*$//' \ + -e 's/^MODULE_[^=]\+=//p' "$CONFIG" + ;; +esac === added file 'network-hooks.d/wireless.conf' --- network-hooks.d/wireless.conf 1970-01-01 00:00:00 +0000 +++ network-hooks.d/wireless.conf 2011-12-31 13:25:58 +0000 @@ -0,0 +1,23 @@ +# Extra options for wpa_supplicant, if any +#WPAS_OPTIONS="" + +# wlan0 +ADDRESS_0=00:11:22:33:44:55 +MODULE_0=ath9k +#WPA_DRIVER_0=wext +wpa_interface_0(){ + # Use this format to set simple things: + wpa_cli_set ssid home + wpa_cli_set psk "secret passphrase" + # Use this format to do more complex things with wpa_cli: + #"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" bssid "$NETWORK" 00:11:22:33:44:55 + #"$wpa_cli" -g "$CTRL" ping +} +#DELAY_0=10 +IPADDRS_0=dhcp +#IPADDRS_0="192.0.2.3/24 2001:DB8::aede:48ff:fe71:f6f2/32" +#ROUTES_0="192.0.2.0/24 2001:DB8::/32" + +#ADDRESS_1=11:22:33:44:55:66 +#MODULE_1=... +#... === modified file 'plugin-runner.c' --- plugin-runner.c 2011-10-09 12:32:13 +0000 +++ plugin-runner.c 2014-07-16 01:41:23 +0000 @@ -2,8 +2,8 @@ /* * Mandos plugin runner - Run Mandos plugins * - * Copyright © 2008-2011 Teddy Hogeborn - * Copyright © 2008-2011 Björn Påhlsson + * Copyright © 2008-2014 Teddy Hogeborn + * Copyright © 2008-2014 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 @@ -23,38 +23,37 @@ */ #define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), getline(), - asprintf(), O_CLOEXEC */ + O_CLOEXEC, pipe2() */ #include /* size_t, NULL */ #include /* malloc(), exit(), EXIT_SUCCESS, realloc() */ #include /* bool, true, false */ #include /* fileno(), fprintf(), - stderr, STDOUT_FILENO */ -#include /* DIR, fdopendir(), stat(), struct - stat, waitpid(), WIFEXITED(), - WEXITSTATUS(), wait(), pid_t, - uid_t, gid_t, getuid(), getgid(), - dirfd() */ + stderr, STDOUT_FILENO, fclose() */ +#include /* fstat(), struct stat, waitpid(), + WIFEXITED(), WEXITSTATUS(), wait(), + pid_t, uid_t, gid_t, getuid(), + getgid() */ #include /* fd_set, select(), FD_ZERO(), FD_SET(), FD_ISSET(), FD_CLR */ #include /* wait(), waitpid(), WIFEXITED(), WEXITSTATUS(), WTERMSIG(), WCOREDUMP() */ -#include /* struct stat, stat(), S_ISREG() */ +#include /* struct stat, fstat(), S_ISREG() */ #include /* and, or, not */ -#include /* DIR, struct dirent, fdopendir(), - readdir(), closedir(), dirfd() */ -#include /* struct stat, stat(), S_ISREG(), - fcntl(), setuid(), setgid(), - F_GETFD, F_SETFD, FD_CLOEXEC, - access(), pipe(), fork(), close() - dup2(), STDOUT_FILENO, _exit(), - execv(), write(), read(), - close() */ +#include /* struct dirent, scandirat() */ +#include /* fcntl(), F_GETFD, F_SETFD, + FD_CLOEXEC, write(), STDOUT_FILENO, + struct stat, fstat(), close(), + setgid(), setuid(), S_ISREG(), + faccessat() pipe2(), fork(), + _exit(), dup2(), fexecve(), read() + */ #include /* fcntl(), F_GETFD, F_SETFD, - FD_CLOEXEC */ -#include /* strsep, strlen(), asprintf(), - strsignal(), strcmp(), strncmp() */ + FD_CLOEXEC, openat(), scandirat(), + pipe2() */ +#include /* strsep, strlen(), strsignal(), + strcmp(), strncmp() */ #include /* errno */ #include /* struct argp_option, struct argp_state, struct argp, @@ -72,6 +71,7 @@ EX_CONFIG, EX_UNAVAILABLE, EX_OK */ #include /* errno */ #include /* error() */ +#include /* fnmatch() */ #define BUFFER_SIZE 256 @@ -105,6 +105,7 @@ /* Gets an existing plugin based on name, or if none is found, creates a new one */ +__attribute__((warn_unused_result)) static plugin *getplugin(char *name){ /* Check for existing plugin with that name */ for(plugin *p = plugin_list; p != NULL; p = p->next){ @@ -171,17 +172,20 @@ } /* Helper function for add_argument and add_environment */ +__attribute__((nonnull, warn_unused_result)) static bool add_to_char_array(const char *new, char ***array, int *len){ /* Resize the pointed-to array to hold one more pointer */ + char **new_array = NULL; do { - *array = realloc(*array, sizeof(char *) - * (size_t) ((*len) + 2)); - } while(*array == NULL and errno == EINTR); + new_array = realloc(*array, sizeof(char *) + * (size_t) ((*len) + 2)); + } while(new_array == NULL and errno == EINTR); /* Malloc check */ - if(*array == NULL){ + if(new_array == NULL){ return false; } + *array = new_array; /* Make a copy of the new string */ char *copy; do { @@ -199,6 +203,7 @@ } /* Add to a plugin's argument vector */ +__attribute__((nonnull(2), warn_unused_result)) static bool add_argument(plugin *p, const char *arg){ if(p == NULL){ return false; @@ -207,6 +212,7 @@ } /* Add to a plugin's environment */ +__attribute__((nonnull(2), warn_unused_result)) static bool add_environment(plugin *p, const char *def, bool replace){ if(p == NULL){ return false; @@ -214,19 +220,19 @@ /* namelen = length of name of environment variable */ size_t namelen = (size_t)(strchrnul(def, '=') - def); /* Search for this environment variable */ - for(char **e = p->environ; *e != NULL; e++){ - if(strncmp(*e, def, namelen + 1) == 0){ + for(char **envdef = p->environ; *envdef != NULL; envdef++){ + if(strncmp(*envdef, def, namelen + 1) == 0){ /* It already exists */ if(replace){ - char *new; + char *new_envdef; do { - new = realloc(*e, strlen(def) + 1); - } while(new == NULL and errno == EINTR); - if(new == NULL){ + new_envdef = realloc(*envdef, strlen(def) + 1); + } while(new_envdef == NULL and errno == EINTR); + if(new_envdef == NULL){ return false; } - *e = new; - strcpy(*e, def); + *envdef = new_envdef; + strcpy(*envdef, def); } return true; } @@ -234,11 +240,13 @@ return add_to_char_array(def, &(p->environ), &(p->envc)); } +#ifndef O_CLOEXEC /* * Based on the example in the GNU LibC manual chapter 13.13 "File * Descriptor Flags". | [[info:libc:Descriptor%20Flags][File Descriptor Flags]] | */ +__attribute__((warn_unused_result)) static int set_cloexec_flag(int fd){ int ret = (int)TEMP_FAILURE_RETRY(fcntl(fd, F_GETFD, 0)); /* If reading the flags failed, return error indication now. */ @@ -249,6 +257,7 @@ return (int)TEMP_FAILURE_RETRY(fcntl(fd, F_SETFD, ret | FD_CLOEXEC)); } +#endif /* not O_CLOEXEC */ /* Mark processes as completed when they exit, and save their exit @@ -286,6 +295,7 @@ } /* Prints out a password to stdout */ +__attribute__((nonnull, warn_unused_result)) static bool print_out_password(const char *buffer, size_t length){ ssize_t ret; for(size_t written = 0; written < length; written += (size_t)ret){ @@ -299,6 +309,7 @@ } /* Removes and free a plugin from the plugin list */ +__attribute__((nonnull)) static void free_plugin(plugin *plugin_node){ for(char **arg = plugin_node->argv; *arg != NULL; arg++){ @@ -338,9 +349,7 @@ char *plugindir = NULL; char *argfile = NULL; FILE *conffp; - size_t d_name_len; - DIR *dir = NULL; - struct dirent *dirst; + struct dirent **direntries = NULL; struct stat st; fd_set rfds_all; int ret, maxfd = 0; @@ -354,6 +363,7 @@ .sa_flags = SA_NOCLDSTOP }; char **custom_argv = NULL; int custom_argc = 0; + int dir_fd = -1; /* Establish a signal handler */ sigemptyset(&sigchld_action.sa_mask); @@ -416,11 +426,12 @@ { .name = NULL } }; + __attribute__((nonnull(3))) error_t parse_opt(int key, char *arg, struct argp_state *state){ errno = 0; switch(key){ char *tmp; - intmax_t tmpmax; + intmax_t tmp_id; case 'g': /* --global-options */ { char *plugin_option; @@ -429,10 +440,13 @@ break; } } + errno = 0; } break; case 'G': /* --global-env */ - add_environment(getplugin(NULL), arg, true); + if(add_environment(getplugin(NULL), arg, true)){ + errno = 0; + } break; case 'o': /* --options-for */ { @@ -455,6 +469,7 @@ break; } } + errno = 0; } break; case 'E': /* --env-for */ @@ -472,7 +487,9 @@ errno = EINVAL; break; } - add_environment(getplugin(arg), envdef, true); + if(add_environment(getplugin(arg), envdef, true)){ + errno = 0; + } } break; case 'd': /* --disable */ @@ -480,6 +497,7 @@ plugin *p = getplugin(arg); if(p != NULL){ p->disabled = true; + errno = 0; } } break; @@ -488,35 +506,41 @@ plugin *p = getplugin(arg); if(p != NULL){ p->disabled = false; + errno = 0; } } break; case 128: /* --plugin-dir */ free(plugindir); plugindir = strdup(arg); + if(plugindir != NULL){ + errno = 0; + } break; case 129: /* --config-file */ /* This is already done by parse_opt_config_file() */ break; case 130: /* --userid */ - tmpmax = strtoimax(arg, &tmp, 10); + tmp_id = strtoimax(arg, &tmp, 10); if(errno != 0 or tmp == arg or *tmp != '\0' - or tmpmax != (uid_t)tmpmax){ + or tmp_id != (uid_t)tmp_id){ argp_error(state, "Bad user ID number: \"%s\", using %" PRIdMAX, arg, (intmax_t)uid); break; } - uid = (uid_t)tmpmax; + uid = (uid_t)tmp_id; + errno = 0; break; case 131: /* --groupid */ - tmpmax = strtoimax(arg, &tmp, 10); + tmp_id = strtoimax(arg, &tmp, 10); if(errno != 0 or tmp == arg or *tmp != '\0' - or tmpmax != (gid_t)tmpmax){ + or tmp_id != (gid_t)tmp_id){ argp_error(state, "Bad group ID number: \"%s\", using %" PRIdMAX, arg, (intmax_t)gid); break; } - gid = (gid_t)tmpmax; + gid = (gid_t)tmp_id; + errno = 0; break; case 132: /* --debug */ debug = true; @@ -570,6 +594,9 @@ case 129: /* --config-file */ free(argfile); argfile = strdup(arg); + if(argfile != NULL){ + errno = 0; + } break; case 130: /* --userid */ case 131: /* --groupid */ @@ -658,13 +685,19 @@ } custom_argc += 1; - custom_argv = realloc(custom_argv, sizeof(char *) - * ((unsigned int) custom_argc + 1)); - if(custom_argv == NULL){ - error(0, errno, "realloc"); - exitstatus = EX_OSERR; - free(org_line); - goto fallback; + { + char **new_argv = realloc(custom_argv, sizeof(char *) + * ((unsigned int) + custom_argc + 1)); + if(new_argv == NULL){ + error(0, errno, "realloc"); + exitstatus = EX_OSERR; + free(new_arg); + free(org_line); + goto fallback; + } else { + custom_argv = new_argv; + } } custom_argv[custom_argc-1] = new_arg; custom_argv[custom_argc] = NULL; @@ -742,12 +775,14 @@ } } - { + if(getuid() == 0){ /* Work around Debian bug #633582: */ int plugindir_fd = open(/* plugindir or */ PDIR, O_RDONLY); if(plugindir_fd == -1){ - error(0, errno, "open"); + if(errno != ENOENT){ + error(0, errno, "open(\"" PDIR "\")"); + } } else { ret = (int)TEMP_FAILURE_RETRY(fstat(plugindir_fd, &st)); if(ret == -1){ @@ -765,7 +800,7 @@ } /* Lower permissions */ - setgid(gid); + ret = setgid(gid); if(ret == -1){ error(0, errno, "setgid"); } @@ -776,24 +811,13 @@ /* Open plugin directory with close_on_exec flag */ { - int dir_fd = -1; - if(plugindir == NULL){ - dir_fd = open(PDIR, O_RDONLY | -#ifdef O_CLOEXEC - O_CLOEXEC -#else /* not O_CLOEXEC */ - 0 -#endif /* not O_CLOEXEC */ - ); - } else { - dir_fd = open(plugindir, O_RDONLY | -#ifdef O_CLOEXEC - O_CLOEXEC -#else /* not O_CLOEXEC */ - 0 -#endif /* not O_CLOEXEC */ - ); - } + dir_fd = open(plugindir != NULL ? plugindir : PDIR, O_RDONLY | +#ifdef O_CLOEXEC + O_CLOEXEC +#else /* not O_CLOEXEC */ + 0 +#endif /* not O_CLOEXEC */ + ); if(dir_fd == -1){ error(0, errno, "Could not open plugin dir"); exitstatus = EX_UNAVAILABLE; @@ -805,129 +829,104 @@ ret = set_cloexec_flag(dir_fd); if(ret < 0){ error(0, errno, "set_cloexec_flag"); - TEMP_FAILURE_RETRY(close(dir_fd)); exitstatus = EX_OSERR; goto fallback; } #endif /* O_CLOEXEC */ - - dir = fdopendir(dir_fd); - if(dir == NULL){ - error(0, errno, "Could not open plugin dir"); - TEMP_FAILURE_RETRY(close(dir_fd)); - exitstatus = EX_OSERR; - goto fallback; + } + + int good_name(const struct dirent * const dirent){ + const char * const patterns[] = { ".*", "#*#", "*~", "*.dpkg-new", + "*.dpkg-old", "*.dpkg-bak", + "*.dpkg-divert", NULL }; +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + for(const char **pat = (const char **)patterns; + *pat != NULL; pat++){ +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + if(fnmatch(*pat, dirent->d_name, FNM_FILE_NAME | FNM_PERIOD) + != FNM_NOMATCH){ + if(debug){ + fprintf(stderr, "Ignoring plugin dir entry \"%s\"" + " matching pattern %s\n", dirent->d_name, *pat); + } + return 0; + } } + return 1; + } + +#ifdef __GLIBC__ +#if __GLIBC_PREREQ(2, 15) + int numplugins = scandirat(dir_fd, ".", &direntries, good_name, + alphasort); +#else /* not __GLIBC_PREREQ(2, 15) */ + int numplugins = scandir(plugindir != NULL ? plugindir : PDIR, + &direntries, good_name, alphasort); +#endif /* not __GLIBC_PREREQ(2, 15) */ +#else /* not __GLIBC__ */ + int numplugins = scandir(plugindir != NULL ? plugindir : PDIR, + &direntries, good_name, alphasort); +#endif /* not __GLIBC__ */ + if(numplugins == -1){ + error(0, errno, "Could not scan plugin dir"); + direntries = NULL; + exitstatus = EX_OSERR; + goto fallback; } FD_ZERO(&rfds_all); /* Read and execute any executable in the plugin directory*/ - while(true){ - do { - dirst = readdir(dir); - } while(dirst == NULL and errno == EINTR); - - /* All directory entries have been processed */ - if(dirst == NULL){ - if(errno == EBADF){ - error(0, errno, "readdir"); - exitstatus = EX_IOERR; - goto fallback; - } - break; - } - - d_name_len = strlen(dirst->d_name); - - /* Ignore dotfiles, backup files and other junk */ - { - bool bad_name = false; - - const char const *bad_prefixes[] = { ".", "#", NULL }; - - const char const *bad_suffixes[] = { "~", "#", ".dpkg-new", - ".dpkg-old", - ".dpkg-bak", - ".dpkg-divert", NULL }; - for(const char **pre = bad_prefixes; *pre != NULL; pre++){ - size_t pre_len = strlen(*pre); - if((d_name_len >= pre_len) - and strncmp((dirst->d_name), *pre, pre_len) == 0){ - if(debug){ - fprintf(stderr, "Ignoring plugin dir entry \"%s\"" - " with bad prefix %s\n", dirst->d_name, *pre); - } - bad_name = true; - break; - } - } - if(bad_name){ - continue; - } - for(const char **suf = bad_suffixes; *suf != NULL; suf++){ - size_t suf_len = strlen(*suf); - if((d_name_len >= suf_len) - and (strcmp((dirst->d_name) + d_name_len-suf_len, *suf) - == 0)){ - if(debug){ - fprintf(stderr, "Ignoring plugin dir entry \"%s\"" - " with bad suffix %s\n", dirst->d_name, *suf); - } - bad_name = true; - break; - } - } - - if(bad_name){ - continue; - } - } - - char *filename; - if(plugindir == NULL){ - ret = (int)TEMP_FAILURE_RETRY(asprintf(&filename, PDIR "/%s", - dirst->d_name)); - } else { - ret = (int)TEMP_FAILURE_RETRY(asprintf(&filename, "%s/%s", - plugindir, - dirst->d_name)); - } - if(ret < 0){ - error(0, errno, "asprintf"); + for(int i = 0; i < numplugins; i++){ + + int plugin_fd = openat(dir_fd, direntries[i]->d_name, O_RDONLY); + if(plugin_fd == -1){ + error(0, errno, "Could not open plugin"); + free(direntries[i]); continue; } - - ret = (int)TEMP_FAILURE_RETRY(stat(filename, &st)); + ret = (int)TEMP_FAILURE_RETRY(fstat(plugin_fd, &st)); if(ret == -1){ error(0, errno, "stat"); - free(filename); + TEMP_FAILURE_RETRY(close(plugin_fd)); + free(direntries[i]); continue; } /* Ignore non-executable files */ if(not S_ISREG(st.st_mode) - or (TEMP_FAILURE_RETRY(access(filename, X_OK)) != 0)){ + or (TEMP_FAILURE_RETRY(faccessat(dir_fd, direntries[i]->d_name, + X_OK, 0)) != 0)){ if(debug){ - fprintf(stderr, "Ignoring plugin dir entry \"%s\"" - " with bad type or mode\n", filename); + fprintf(stderr, "Ignoring plugin dir entry \"%s/%s\"" + " with bad type or mode\n", + plugindir != NULL ? plugindir : PDIR, + direntries[i]->d_name); } - free(filename); + TEMP_FAILURE_RETRY(close(plugin_fd)); + free(direntries[i]); continue; } - plugin *p = getplugin(dirst->d_name); + plugin *p = getplugin(direntries[i]->d_name); if(p == NULL){ error(0, errno, "getplugin"); - free(filename); + TEMP_FAILURE_RETRY(close(plugin_fd)); + free(direntries[i]); continue; } if(p->disabled){ if(debug){ fprintf(stderr, "Ignoring disabled plugin \"%s\"\n", - dirst->d_name); + direntries[i]->d_name); } - free(filename); + TEMP_FAILURE_RETRY(close(plugin_fd)); + free(direntries[i]); continue; } { @@ -947,9 +946,8 @@ } } } - /* If this plugin has any environment variables, we will call - using execve and need to duplicate the environment from this - process, too. */ + /* If this plugin has any environment variables, we need to + duplicate the environment from this process, too. */ if(p->environ[0] != NULL){ for(char **e = environ; *e != NULL; e++){ if(not add_environment(p, *e, false)){ @@ -959,25 +957,47 @@ } int pipefd[2]; +#ifndef O_CLOEXEC ret = (int)TEMP_FAILURE_RETRY(pipe(pipefd)); +#else /* O_CLOEXEC */ + ret = (int)TEMP_FAILURE_RETRY(pipe2(pipefd, O_CLOEXEC)); +#endif /* O_CLOEXEC */ if(ret == -1){ error(0, errno, "pipe"); exitstatus = EX_OSERR; - goto fallback; - } + free(direntries[i]); + goto fallback; + } + 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])); + exitstatus = EX_OSERR; + free(direntries[i]); + goto fallback; + } +#ifndef O_CLOEXEC /* Ask OS to automatic close the pipe on exec */ 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])); exitstatus = EX_OSERR; + free(direntries[i]); goto fallback; } 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])); exitstatus = EX_OSERR; + free(direntries[i]); goto fallback; } +#endif /* not O_CLOEXEC */ /* Block SIGCHLD until process is safely in process list */ ret = (int)TEMP_FAILURE_RETRY(sigprocmask(SIG_BLOCK, &sigchld_action.sa_mask, @@ -985,6 +1005,7 @@ if(ret < 0){ error(0, errno, "sigprocmask"); exitstatus = EX_OSERR; + free(direntries[i]); goto fallback; } /* Starting a new process to be watched */ @@ -994,7 +1015,12 @@ } while(pid == -1 and errno == EINTR); if(pid == -1){ 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])); exitstatus = EX_OSERR; + free(direntries[i]); goto fallback; } if(pid == 0){ @@ -1016,29 +1042,20 @@ _exit(EX_OSERR); } - if(dirfd(dir) < 0){ - /* If dir has no file descriptor, we could not set FD_CLOEXEC - above and must now close it manually here. */ - closedir(dir); - } - if(p->environ[0] == NULL){ - if(execv(filename, p->argv) < 0){ - error(0, errno, "execv for %s", filename); - _exit(EX_OSERR); - } - } else { - if(execve(filename, p->argv, p->environ) < 0){ - error(0, errno, "execve for %s", filename); - _exit(EX_OSERR); - } + if(fexecve(plugin_fd, p->argv, + (p->environ[0] != NULL) ? p->environ : environ) < 0){ + error(0, errno, "fexecve for %s/%s", + plugindir != NULL ? plugindir : PDIR, + direntries[i]->d_name); + _exit(EX_OSERR); } /* no return */ } /* Parent process */ TEMP_FAILURE_RETRY(close(pipefd[1])); /* Close unused write end of pipe */ - free(filename); - plugin *new_plugin = getplugin(dirst->d_name); + TEMP_FAILURE_RETRY(close(plugin_fd)); + plugin *new_plugin = getplugin(direntries[i]->d_name); if(new_plugin == NULL){ error(0, errno, "getplugin"); ret = (int)(TEMP_FAILURE_RETRY @@ -1048,8 +1065,10 @@ error(0, errno, "sigprocmask"); } exitstatus = EX_OSERR; + free(direntries[i]); goto fallback; } + free(direntries[i]); new_plugin->pid = pid; new_plugin->fd = pipefd[0]; @@ -1065,16 +1084,30 @@ goto fallback; } +#if defined (__GNUC__) and defined (__GLIBC__) +#if not __GLIBC_PREREQ(2, 16) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-conversion" +#endif +#endif FD_SET(new_plugin->fd, &rfds_all); /* Spurious warning from - -Wconversion */ + -Wconversion in GNU libc + before 2.16 */ +#if defined (__GNUC__) and defined (__GLIBC__) +#if not __GLIBC_PREREQ(2, 16) +#pragma GCC diagnostic pop +#endif +#endif if(maxfd < new_plugin->fd){ maxfd = new_plugin->fd; } } - TEMP_FAILURE_RETRY(closedir(dir)); - dir = NULL; + free(direntries); + direntries = NULL; + TEMP_FAILURE_RETRY(close(dir_fd)); + dir_fd = -1; free_plugin(getplugin(NULL)); for(plugin *p = plugin_list; p != NULL; p = p->next){ @@ -1126,8 +1159,20 @@ } /* Remove the plugin */ +#if defined (__GNUC__) and defined (__GLIBC__) +#if not __GLIBC_PREREQ(2, 16) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-conversion" +#endif +#endif FD_CLR(proc->fd, &rfds_all); /* Spurious warning from - -Wconversion */ + -Wconversion in GNU libc + before 2.16 */ +#if defined (__GNUC__) and defined (__GLIBC__) +#if not __GLIBC_PREREQ(2, 16) +#pragma GCC diagnostic pop +#endif +#endif /* Block signal while modifying process_list */ ret = (int)TEMP_FAILURE_RETRY(sigprocmask @@ -1173,22 +1218,37 @@ } /* This process has not completed. Does it have any output? */ +#if defined (__GNUC__) and defined (__GLIBC__) +#if not __GLIBC_PREREQ(2, 16) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-conversion" +#endif +#endif if(proc->eof or not FD_ISSET(proc->fd, &rfds)){ /* Spurious warning from - -Wconversion */ + -Wconversion + in GNU libc + before + 2.16 */ +#if defined (__GNUC__) and defined (__GLIBC__) +#if not __GLIBC_PREREQ(2, 16) +#pragma GCC diagnostic pop +#endif +#endif /* This process had nothing to say at this time */ proc = proc->next; continue; } /* Before reading, make the process' data buffer large enough */ if(proc->buffer_length + BUFFER_SIZE > proc->buffer_size){ - proc->buffer = realloc(proc->buffer, proc->buffer_size - + (size_t) BUFFER_SIZE); - if(proc->buffer == NULL){ + char *new_buffer = realloc(proc->buffer, proc->buffer_size + + (size_t) BUFFER_SIZE); + if(new_buffer == NULL){ error(0, errno, "malloc"); exitstatus = EX_OSERR; goto fallback; } + proc->buffer = new_buffer; proc->buffer_size += BUFFER_SIZE; } /* Read from the process */ @@ -1247,8 +1307,10 @@ free(custom_argv); } - if(dir != NULL){ - closedir(dir); + free(direntries); + + if(dir_fd != -1){ + TEMP_FAILURE_RETRY(close(dir_fd)); } /* Kill the processes */ === modified file 'plugin-runner.xml' --- plugin-runner.xml 2011-10-05 16:56:06 +0000 +++ plugin-runner.xml 2011-12-31 23:05:34 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,7 +33,7 @@ 2008 2009 - 2011 + 2012 Teddy Hogeborn Björn Påhlsson === modified file 'plugins.d/askpass-fifo.c' --- plugins.d/askpass-fifo.c 2011-10-05 16:00:56 +0000 +++ plugins.d/askpass-fifo.c 2014-08-09 23:37:07 +0000 @@ -2,8 +2,8 @@ /* * Askpass-FIFO - Read a password from a FIFO and output it * - * Copyright © 2008-2011 Teddy Hogeborn - * Copyright © 2008-2011 Björn Påhlsson + * Copyright © 2008-2014 Teddy Hogeborn + * Copyright © 2008-2014 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 @@ -23,7 +23,7 @@ */ #define _GNU_SOURCE /* TEMP_FAILURE_RETRY() */ -#include /* ssize_t */ +#include /* uid_t, gid_t, ssize_t */ #include /* mkfifo(), S_IRUSR, S_IWUSR */ #include /* and */ #include /* errno, EACCES, ENOTDIR, ELOOP, @@ -44,8 +44,11 @@ #include /* strerror() */ #include /* va_list, va_start(), ... */ +uid_t uid = 65534; +gid_t gid = 65534; /* Function to use when printing errors */ +__attribute__((format (gnu_printf, 3, 4))) void error_plus(int status, int errnum, const char *formatstring, ...){ va_list ap; @@ -54,7 +57,7 @@ va_start(ap, formatstring); ret = vasprintf(&text, formatstring, ap); - if (ret == -1){ + if(ret == -1){ fprintf(stderr, "Mandos plugin %s: ", program_invocation_short_name); vfprintf(stderr, formatstring, ap); @@ -73,6 +76,9 @@ int ret = 0; ssize_t sret; + uid = getuid(); + gid = getgid(); + /* Create FIFO */ const char passfifo[] = "/lib/cryptsetup/passfifo"; ret = mkfifo(passfifo, S_IRUSR | S_IWUSR); @@ -118,6 +124,16 @@ } } + /* Lower group privileges */ + if(setgid(gid) == -1){ + error_plus(0, errno, "setgid"); + } + + /* Lower user privileges */ + if(setuid(uid) == -1){ + error_plus(0, errno, "setuid"); + } + /* Read from FIFO */ char *buf = NULL; size_t buf_len = 0; === modified file 'plugins.d/askpass-fifo.xml' --- plugins.d/askpass-fifo.xml 2011-10-05 16:00:56 +0000 +++ plugins.d/askpass-fifo.xml 2011-12-31 23:05:34 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -34,6 +34,7 @@ 2008 2009 2011 + 2012 Teddy Hogeborn Björn Påhlsson === modified file 'plugins.d/mandos-client.c' --- plugins.d/mandos-client.c 2011-11-14 20:55:32 +0000 +++ plugins.d/mandos-client.c 2015-03-10 18:52:09 +0000 @@ -9,8 +9,8 @@ * "browse_callback", and parts of "main". * * Everything else is - * Copyright © 2008-2011 Teddy Hogeborn - * Copyright © 2008-2011 Björn Påhlsson + * Copyright © 2008-2014 Teddy Hogeborn + * Copyright © 2008-2014 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 @@ -32,16 +32,16 @@ /* Needed by GPGME, specifically gpgme_data_seek() */ #ifndef _LARGEFILE_SOURCE #define _LARGEFILE_SOURCE -#endif +#endif /* not _LARGEFILE_SOURCE */ #ifndef _FILE_OFFSET_BITS #define _FILE_OFFSET_BITS 64 -#endif +#endif /* not _FILE_OFFSET_BITS */ #define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), asprintf() */ #include /* fprintf(), stderr, fwrite(), - stdout, ferror(), remove() */ -#include /* uint16_t, uint32_t */ + stdout, ferror() */ +#include /* uint16_t, uint32_t, intptr_t */ #include /* NULL, size_t, ssize_t */ #include /* free(), EXIT_SUCCESS, srand(), strtof(), abort() */ @@ -55,13 +55,13 @@ opendir(), DIR */ #include /* open(), S_ISREG */ #include /* socket(), struct sockaddr_in6, - inet_pton(), connect() */ -#include /* open() */ + inet_pton(), connect(), + getnameinfo() */ +#include /* open(), unlinkat() */ #include /* opendir(), struct dirent, readdir() */ #include /* PRIu16, PRIdMAX, intmax_t, strtoimax() */ -#include /* assert() */ #include /* perror(), errno, program_invocation_short_name */ #include /* nanosleep(), time(), sleep() */ @@ -73,8 +73,9 @@ */ #include /* close(), SEEK_SET, off_t, write(), getuid(), getgid(), seteuid(), - setgid(), pause() */ -#include /* inet_pton(), htons, inet_ntop() */ + setgid(), pause(), _exit(), + unlinkat() */ +#include /* inet_pton(), htons() */ #include /* not, or, and */ #include /* struct argp_option, error_t, struct argp_state, struct argp, @@ -87,6 +88,13 @@ EX_NOHOST, EX_IOERR, EX_PROTOCOL */ #include /* waitpid(), WIFEXITED(), WEXITSTATUS(), WTERMSIG() */ +#include /* setgroups() */ +#include /* argz_add_sep(), argz_next(), + argz_delete(), argz_append(), + argz_stringify(), argz_add(), + argz_count() */ +#include /* getnameinfo(), NI_NUMERICHOST, + EAI_SYSTEM, gai_strerror() */ #ifdef __linux__ #include /* klogctl() */ @@ -134,11 +142,14 @@ static const char sys_class_net[] = "/sys/class/net"; char *connect_to = NULL; const char *hookdir = HOOKDIR; +int hookdir_fd = -1; +uid_t uid = 65534; +gid_t gid = 65534; /* Doubly linked list that need to be circularly linked when used */ typedef struct server{ const char *ip; - uint16_t port; + in_port_t port; AvahiIfIndex if_index; int af; struct timespec last_seen; @@ -148,7 +159,6 @@ /* Used for passing in values through the Avahi callback functions */ typedef struct { - AvahiSimplePoll *simple_poll; AvahiServer *server; gnutls_certificate_credentials_t cred; unsigned int dh_bits; @@ -156,31 +166,33 @@ const char *priority; gpgme_ctx_t ctx; server *current_server; + char *interfaces; + size_t interfaces_size; } mandos_context; -/* global context so signal handler can reach it*/ -mandos_context mc = { .simple_poll = NULL, .server = NULL, - .dh_bits = 1024, .priority = "SECURE256" - ":!CTYPE-X.509:+CTYPE-OPENPGP", - .current_server = NULL }; +/* global so signal handler can reach it*/ +AvahiSimplePoll *simple_poll; sig_atomic_t quit_now = 0; int signal_received = 0; /* Function to use when printing errors */ void perror_plus(const char *print_text){ + int e = errno; fprintf(stderr, "Mandos plugin %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); TEMP_FAILURE_RETRY(fprintf(stream, "Mandos plugin %s: ", program_invocation_short_name)); - return TEMP_FAILURE_RETRY(vfprintf(stream, format, ap)); + return (int)TEMP_FAILURE_RETRY(vfprintf(stream, format, ap)); } /* @@ -188,26 +200,33 @@ * bytes. "buffer_capacity" is how much is currently allocated, * "buffer_length" is how much is already used. */ +__attribute__((nonnull, warn_unused_result)) size_t incbuffer(char **buffer, size_t buffer_length, size_t buffer_capacity){ if(buffer_length + BUFFER_SIZE > buffer_capacity){ - *buffer = realloc(*buffer, buffer_capacity + BUFFER_SIZE); - if(buffer == NULL){ + char *new_buf = realloc(*buffer, buffer_capacity + BUFFER_SIZE); + if(new_buf == NULL){ + int old_errno = errno; + free(*buffer); + errno = old_errno; + *buffer = NULL; return 0; } + *buffer = new_buf; buffer_capacity += BUFFER_SIZE; } return buffer_capacity; } /* Add server to set of servers to retry periodically */ -int add_server(const char *ip, uint16_t port, AvahiIfIndex if_index, - int af){ +__attribute__((nonnull, warn_unused_result)) +bool add_server(const char *ip, in_port_t port, AvahiIfIndex if_index, + int af, server **current_server){ int ret; server *new_server = malloc(sizeof(server)); if(new_server == NULL){ perror_plus("malloc"); - return -1; + return false; } *new_server = (server){ .ip = strdup(ip), .port = port, @@ -215,41 +234,53 @@ .af = af }; if(new_server->ip == NULL){ perror_plus("strdup"); - return -1; + free(new_server); + return false; + } + ret = clock_gettime(CLOCK_MONOTONIC, &(new_server->last_seen)); + if(ret == -1){ + perror_plus("clock_gettime"); +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + free((char *)(new_server->ip)); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif + free(new_server); + return false; } /* Special case of first server */ - if (mc.current_server == NULL){ + if(*current_server == NULL){ new_server->next = new_server; new_server->prev = new_server; - mc.current_server = new_server; - /* Place the new server last in the list */ + *current_server = new_server; } else { - new_server->next = mc.current_server; - new_server->prev = mc.current_server->prev; + /* Place the new server last in the list */ + new_server->next = *current_server; + new_server->prev = (*current_server)->prev; new_server->prev->next = new_server; - mc.current_server->prev = new_server; - } - ret = clock_gettime(CLOCK_MONOTONIC, &mc.current_server->last_seen); - if(ret == -1){ - perror_plus("clock_gettime"); - return -1; - } - return 0; + (*current_server)->prev = new_server; + } + return true; } /* * Initialize GPGME. */ -static bool init_gpgme(const char *seckey, const char *pubkey, - const char *tempdir){ +__attribute__((nonnull, warn_unused_result)) +static bool init_gpgme(const char * const seckey, + const char * const pubkey, + const char * const tempdir, + mandos_context *mc){ gpgme_error_t rc; gpgme_engine_info_t engine_info; - /* * Helper function to insert pub and seckey to the engine keyring. */ - bool import_key(const char *filename){ + bool import_key(const char * const filename){ int ret; int fd; gpgme_data_t pgp_data; @@ -267,7 +298,7 @@ return false; } - rc = gpgme_op_import(mc.ctx, pgp_data); + rc = gpgme_op_import(mc->ctx, pgp_data); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "bad gpgme_op_import: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); @@ -317,7 +348,7 @@ } /* Create new GPGME "context" */ - rc = gpgme_new(&(mc.ctx)); + rc = gpgme_new(&(mc->ctx)); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "Mandos plugin mandos-client: " "bad gpgme_new: %s: %s\n", gpgme_strsource(rc), @@ -336,9 +367,11 @@ * Decrypt OpenPGP data. * Returns -1 on error */ +__attribute__((nonnull, warn_unused_result)) static ssize_t pgp_packet_decrypt(const char *cryptotext, size_t crypto_size, - char **plaintext){ + char **plaintext, + mandos_context *mc){ gpgme_data_t dh_crypto, dh_plain; gpgme_error_t rc; ssize_t ret; @@ -370,14 +403,14 @@ /* Decrypt data from the cryptotext data buffer to the plaintext data buffer */ - rc = gpgme_op_decrypt(mc.ctx, dh_crypto, dh_plain); + rc = gpgme_op_decrypt(mc->ctx, dh_crypto, dh_plain); if(rc != GPG_ERR_NO_ERROR){ fprintf_plus(stderr, "bad gpgme_op_decrypt: %s: %s\n", gpgme_strsource(rc), gpgme_strerror(rc)); plaintext_length = -1; if(debug){ gpgme_decrypt_result_t result; - result = gpgme_op_decrypt_result(mc.ctx); + result = gpgme_op_decrypt_result(mc->ctx); if(result == NULL){ fprintf_plus(stderr, "gpgme_op_decrypt_result failed\n"); } else { @@ -460,23 +493,32 @@ return plaintext_length; } -static const char * safer_gnutls_strerror(int value){ - const char *ret = gnutls_strerror(value); /* Spurious warning from - -Wunreachable-code */ - if(ret == NULL) - ret = "(unknown)"; - return ret; +__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); + return safe_string(ret); } /* GnuTLS log function callback */ +__attribute__((nonnull)) static void debuggnutls(__attribute__((unused)) int level, const char* string){ fprintf_plus(stderr, "GnuTLS: %s", string); } +__attribute__((nonnull, warn_unused_result)) static int init_gnutls_global(const char *pubkeyfilename, - const char *seckeyfilename){ + const char *seckeyfilename, + mandos_context *mc){ int ret; + unsigned int uret; if(debug){ fprintf_plus(stderr, "Initializing GnuTLS\n"); @@ -498,7 +540,7 @@ } /* OpenPGP credentials */ - ret = gnutls_certificate_allocate_credentials(&mc.cred); + ret = gnutls_certificate_allocate_credentials(&mc->cred); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, "GnuTLS memory error: %s\n", safer_gnutls_strerror(ret)); @@ -514,7 +556,7 @@ } ret = gnutls_certificate_set_openpgp_key_file - (mc.cred, pubkeyfilename, seckeyfilename, + (mc->cred, pubkeyfilename, seckeyfilename, GNUTLS_OPENPGP_FMT_BASE64); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, @@ -526,33 +568,136 @@ } /* GnuTLS server initialization */ - ret = gnutls_dh_params_init(&mc.dh_params); + ret = gnutls_dh_params_init(&mc->dh_params); if(ret != GNUTLS_E_SUCCESS){ fprintf_plus(stderr, "Error in GnuTLS DH parameter" " initialization: %s\n", safer_gnutls_strerror(ret)); goto globalfail; } - ret = gnutls_dh_params_generate2(mc.dh_params, mc.dh_bits); + 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 }; + { + int secfile = open(seckeyfilename, O_RDONLY); + 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); + } + /* 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: %s\n", - safer_gnutls_strerror(ret)); + 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); + gnutls_certificate_set_dh_params(mc->cred, mc->dh_params); return 0; globalfail: - gnutls_certificate_free_credentials(mc.cred); + gnutls_certificate_free_credentials(mc->cred); gnutls_global_deinit(); - gnutls_dh_params_deinit(mc.dh_params); + gnutls_dh_params_deinit(mc->dh_params); return -1; } -static int init_gnutls_session(gnutls_session_t *session){ +__attribute__((nonnull, warn_unused_result)) +static int init_gnutls_session(gnutls_session_t *session, + mandos_context *mc){ int ret; /* GnuTLS session creation */ do { @@ -570,7 +715,7 @@ { const char *err; do { - ret = gnutls_priority_set_direct(*session, mc.priority, &err); + ret = gnutls_priority_set_direct(*session, mc->priority, &err); if(quit_now){ gnutls_deinit(*session); return -1; @@ -587,7 +732,7 @@ do { ret = gnutls_credentials_set(*session, GNUTLS_CRD_CERTIFICATE, - mc.cred); + mc->cred); if(quit_now){ gnutls_deinit(*session); return -1; @@ -603,7 +748,7 @@ /* ignore client certificate if any. */ gnutls_certificate_server_set_request(*session, GNUTLS_CERT_IGNORE); - gnutls_dh_set_prime_bits(*session, mc.dh_bits); + gnutls_dh_set_prime_bits(*session, mc->dh_bits); return 0; } @@ -613,15 +758,13 @@ __attribute__((unused)) const char *txt){} /* Called when a Mandos server is found */ -static int start_mandos_communication(const char *ip, uint16_t port, +__attribute__((nonnull, warn_unused_result)) +static int start_mandos_communication(const char *ip, in_port_t port, AvahiIfIndex if_index, - int af){ + int af, mandos_context *mc){ int ret, tcp_sd = -1; ssize_t sret; - union { - struct sockaddr_in in; - struct sockaddr_in6 in6; - } to; + struct sockaddr_storage to; char *buffer = NULL; char *decrypted_buffer = NULL; size_t buffer_length = 0; @@ -651,14 +794,46 @@ return -1; } - ret = init_gnutls_session(&session); + /* If the interface is specified and we have a list of interfaces */ + if(if_index != AVAHI_IF_UNSPEC and mc->interfaces != NULL){ + /* Check if the interface is one of the interfaces we are using */ + bool match = false; + { + char *interface = NULL; + while((interface=argz_next(mc->interfaces, mc->interfaces_size, + interface))){ + if(if_nametoindex(interface) == (unsigned int)if_index){ + match = true; + break; + } + } + } + if(not match){ + /* This interface does not match any in the list, so we don't + connect to the server */ + if(debug){ + char interface[IF_NAMESIZE]; + if(if_indextoname((unsigned int)if_index, interface) == NULL){ + perror_plus("if_indextoname"); + } else { + fprintf_plus(stderr, "Skipping server on non-used interface" + " \"%s\"\n", + if_indextoname((unsigned int)if_index, + interface)); + } + } + return -1; + } + } + + ret = init_gnutls_session(&session, mc); if(ret != 0){ return -1; } if(debug){ fprintf_plus(stderr, "Setting up a TCP connection to %s, port %" - PRIu16 "\n", ip, port); + PRIuMAX "\n", ip, (uintmax_t)port); } tcp_sd = socket(pf, SOCK_STREAM, 0); @@ -676,11 +851,11 @@ memset(&to, 0, sizeof(to)); if(af == AF_INET6){ - to.in6.sin6_family = (sa_family_t)af; - ret = inet_pton(af, ip, &to.in6.sin6_addr); + ((struct sockaddr_in6 *)&to)->sin6_family = (sa_family_t)af; + ret = inet_pton(af, ip, &((struct sockaddr_in6 *)&to)->sin6_addr); } else { /* IPv4 */ - to.in.sin_family = (sa_family_t)af; - ret = inet_pton(af, ip, &to.in.sin_addr); + ((struct sockaddr_in *)&to)->sin_family = (sa_family_t)af; + ret = inet_pton(af, ip, &((struct sockaddr_in *)&to)->sin_addr); } if(ret < 0 ){ int e = errno; @@ -695,13 +870,9 @@ goto mandos_end; } if(af == AF_INET6){ - to.in6.sin6_port = htons(port); /* Spurious warnings from - -Wconversion and - -Wunreachable-code */ - - if(IN6_IS_ADDR_LINKLOCAL /* Spurious warnings from */ - (&to.in6.sin6_addr)){ /* -Wstrict-aliasing=2 or lower and - -Wunreachable-code*/ + ((struct sockaddr_in6 *)&to)->sin6_port = htons(port); + if(IN6_IS_ADDR_LINKLOCAL + (&((struct sockaddr_in6 *)&to)->sin6_addr)){ if(if_index == AVAHI_IF_UNSPEC){ fprintf_plus(stderr, "An IPv6 link-local address is" " incomplete without a network interface\n"); @@ -709,12 +880,10 @@ goto mandos_end; } /* Set the network interface number as scope */ - to.in6.sin6_scope_id = (uint32_t)if_index; + ((struct sockaddr_in6 *)&to)->sin6_scope_id = (uint32_t)if_index; } } else { - to.in.sin_port = htons(port); /* Spurious warnings from - -Wconversion and - -Wunreachable-code */ + ((struct sockaddr_in *)&to)->sin_port = htons(port); } if(quit_now){ @@ -728,29 +897,32 @@ if(if_indextoname((unsigned int)if_index, interface) == NULL){ perror_plus("if_indextoname"); } else { - fprintf_plus(stderr, "Connection to: %s%%%s, port %" PRIu16 - "\n", ip, interface, port); + fprintf_plus(stderr, "Connection to: %s%%%s, port %" PRIuMAX + "\n", ip, interface, (uintmax_t)port); } } else { - fprintf_plus(stderr, "Connection to: %s, port %" PRIu16 "\n", - ip, port); + fprintf_plus(stderr, "Connection to: %s, port %" PRIuMAX "\n", + ip, (uintmax_t)port); } char addrstr[(INET_ADDRSTRLEN > INET6_ADDRSTRLEN) ? INET_ADDRSTRLEN : INET6_ADDRSTRLEN] = ""; - const char *pcret; if(af == AF_INET6){ - pcret = inet_ntop(af, &(to.in6.sin6_addr), addrstr, - sizeof(addrstr)); + ret = getnameinfo((struct sockaddr *)&to, + sizeof(struct sockaddr_in6), + addrstr, sizeof(addrstr), NULL, 0, + NI_NUMERICHOST); } else { - pcret = inet_ntop(af, &(to.in.sin_addr), addrstr, - sizeof(addrstr)); + ret = getnameinfo((struct sockaddr *)&to, + sizeof(struct sockaddr_in), + addrstr, sizeof(addrstr), NULL, 0, + NI_NUMERICHOST); } - if(pcret == NULL){ - perror_plus("inet_ntop"); - } else { - if(strcmp(addrstr, ip) != 0){ - fprintf_plus(stderr, "Canonical address form: %s\n", addrstr); - } + if(ret == EAI_SYSTEM){ + perror_plus("getnameinfo"); + } else if(ret != 0) { + fprintf_plus(stderr, "getnameinfo: %s", gai_strerror(ret)); + } else if(strcmp(addrstr, ip) != 0){ + fprintf_plus(stderr, "Canonical address form: %s\n", addrstr); } } @@ -760,12 +932,14 @@ } if(af == AF_INET6){ - ret = connect(tcp_sd, &to.in6, sizeof(to)); + ret = connect(tcp_sd, (struct sockaddr *)&to, + sizeof(struct sockaddr_in6)); } else { - ret = connect(tcp_sd, &to.in, sizeof(to)); /* IPv4 */ + ret = connect(tcp_sd, (struct sockaddr *)&to, /* IPv4 */ + sizeof(struct sockaddr_in)); } if(ret < 0){ - if ((errno != ECONNREFUSED and errno != ENETUNREACH) or debug){ + if((errno != ECONNREFUSED and errno != ENETUNREACH) or debug){ int e = errno; perror_plus("connect"); errno = e; @@ -817,8 +991,11 @@ goto mandos_end; } - /* Spurious warning from -Wint-to-pointer-cast */ - gnutls_transport_set_ptr(session, (gnutls_transport_ptr_t) tcp_sd); + /* This casting via intptr_t is to eliminate warning about casting + an int to a pointer type. This is exactly how the GnuTLS Guile + function "set-session-transport-fd!" does it. */ + gnutls_transport_set_ptr(session, + (gnutls_transport_ptr_t)(intptr_t)tcp_sd); if(quit_now){ errno = EINTR; @@ -929,7 +1106,7 @@ if(buffer_length > 0){ ssize_t decrypted_buffer_size; decrypted_buffer_size = pgp_packet_decrypt(buffer, buffer_length, - &decrypted_buffer); + &decrypted_buffer, mc); if(decrypted_buffer_size >= 0){ written = 0; @@ -983,6 +1160,7 @@ return retval; } +__attribute__((nonnull)) static void resolve_callback(AvahiSServiceResolver *r, AvahiIfIndex interface, AvahiProtocol proto, @@ -996,13 +1174,16 @@ AVAHI_GCC_UNUSED AvahiStringList *txt, AVAHI_GCC_UNUSED AvahiLookupResultFlags flags, - AVAHI_GCC_UNUSED void* userdata){ - assert(r); + void *mc){ + if(r == NULL){ + return; + } /* Called whenever a service has been resolved successfully or timed out */ if(quit_now){ + avahi_s_service_resolver_free(r); return; } @@ -1012,7 +1193,8 @@ fprintf_plus(stderr, "(Avahi Resolver) Failed to resolve service " "'%s' of type '%s' in domain '%s': %s\n", name, type, domain, - avahi_strerror(avahi_server_errno(mc.server))); + avahi_strerror(avahi_server_errno + (((mandos_context*)mc)->server))); break; case AVAHI_RESOLVER_FOUND: @@ -1024,13 +1206,19 @@ PRIdMAX ") on port %" PRIu16 "\n", name, host_name, ip, (intmax_t)interface, port); } - int ret = start_mandos_communication(ip, port, interface, - avahi_proto_to_af(proto)); + int ret = start_mandos_communication(ip, (in_port_t)port, + interface, + avahi_proto_to_af(proto), + mc); if(ret == 0){ - avahi_simple_poll_quit(mc.simple_poll); + avahi_simple_poll_quit(simple_poll); } else { - ret = add_server(ip, port, interface, - avahi_proto_to_af(proto)); + if(not add_server(ip, (in_port_t)port, interface, + avahi_proto_to_af(proto), + &((mandos_context*)mc)->current_server)){ + fprintf_plus(stderr, "Failed to add server \"%s\" to server" + " list\n", name); + } } } } @@ -1046,8 +1234,10 @@ const char *domain, AVAHI_GCC_UNUSED AvahiLookupResultFlags flags, - AVAHI_GCC_UNUSED void* userdata){ - assert(b); + void *mc){ + if(b == NULL){ + return; + } /* Called whenever a new services becomes available on the LAN or is removed from the LAN */ @@ -1061,8 +1251,9 @@ case AVAHI_BROWSER_FAILURE: fprintf_plus(stderr, "(Avahi browser) %s\n", - avahi_strerror(avahi_server_errno(mc.server))); - avahi_simple_poll_quit(mc.simple_poll); + avahi_strerror(avahi_server_errno + (((mandos_context*)mc)->server))); + avahi_simple_poll_quit(simple_poll); return; case AVAHI_BROWSER_NEW: @@ -1071,12 +1262,14 @@ the callback function is called the Avahi server will free the resolver for us. */ - if(avahi_s_service_resolver_new(mc.server, interface, protocol, - name, type, domain, protocol, 0, - resolve_callback, NULL) == NULL) + if(avahi_s_service_resolver_new(((mandos_context*)mc)->server, + interface, protocol, name, type, + domain, protocol, 0, + resolve_callback, mc) == NULL) fprintf_plus(stderr, "Avahi: Failed to resolve service '%s':" " %s\n", name, - avahi_strerror(avahi_server_errno(mc.server))); + avahi_strerror(avahi_server_errno + (((mandos_context*)mc)->server))); break; case AVAHI_BROWSER_REMOVE: @@ -1101,31 +1294,38 @@ signal_received = sig; int old_errno = errno; /* set main loop to exit */ - if(mc.simple_poll != NULL){ - avahi_simple_poll_quit(mc.simple_poll); + if(simple_poll != NULL){ + avahi_simple_poll_quit(simple_poll); } errno = old_errno; } +__attribute__((nonnull, warn_unused_result)) bool get_flags(const char *ifname, struct ifreq *ifr){ int ret; + error_t ret_errno; int s = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); if(s < 0){ + ret_errno = errno; perror_plus("socket"); + errno = ret_errno; return false; } strcpy(ifr->ifr_name, ifname); ret = ioctl(s, SIOCGIFFLAGS, ifr); if(ret == -1){ if(debug){ + ret_errno = errno; perror_plus("ioctl SIOCGIFFLAGS"); + errno = ret_errno; } return false; } return true; } +__attribute__((nonnull, warn_unused_result)) bool good_flags(const char *ifname, const struct ifreq *ifr){ /* Reject the loopback device */ @@ -1173,6 +1373,7 @@ * corresponds to an acceptable network device. * (This function is passed to scandir(3) as a filter function.) */ +__attribute__((nonnull, warn_unused_result)) int good_interface(const struct dirent *if_entry){ if(if_entry->d_name[0] == '.'){ return 0; @@ -1194,48 +1395,40 @@ } /* - * This function determines if a directory entry in /sys/class/net - * corresponds to an acceptable network device which is up. - * (This function is passed to scandir(3) as a filter function.) - */ -int up_interface(const struct dirent *if_entry){ - if(if_entry->d_name[0] == '.'){ - return 0; - } - - struct ifreq ifr; - if(not get_flags(if_entry->d_name, &ifr)){ - if(debug){ - fprintf_plus(stderr, "Failed to get flags for interface " - "\"%s\"\n", if_entry->d_name); - } - return 0; - } - - /* Reject down interfaces */ - if(not (ifr.ifr_flags & IFF_UP)){ - if(debug){ - fprintf_plus(stderr, "Rejecting down interface \"%s\"\n", - if_entry->d_name); - } - return 0; - } - - /* Reject non-running interfaces */ - if(not (ifr.ifr_flags & IFF_RUNNING)){ - if(debug){ - fprintf_plus(stderr, "Rejecting non-running interface \"%s\"\n", - if_entry->d_name); - } - return 0; - } - - if(not good_flags(if_entry->d_name, &ifr)){ - return 0; - } - return 1; -} - + * This function determines if a network interface is up. + */ +__attribute__((nonnull, warn_unused_result)) +bool interface_is_up(const char *interface){ + struct ifreq ifr; + if(not get_flags(interface, &ifr)){ + if(debug){ + fprintf_plus(stderr, "Failed to get flags for interface " + "\"%s\"\n", interface); + } + return false; + } + + return (bool)(ifr.ifr_flags & IFF_UP); +} + +/* + * This function determines if a network interface is running + */ +__attribute__((nonnull, warn_unused_result)) +bool interface_is_running(const char *interface){ + struct ifreq ifr; + if(not get_flags(interface, &ifr)){ + if(debug){ + fprintf_plus(stderr, "Failed to get flags for interface " + "\"%s\"\n", interface); + } + return false; + } + + return (bool)(ifr.ifr_flags & IFF_RUNNING); +} + +__attribute__((nonnull, pure, warn_unused_result)) int notdotentries(const struct dirent *direntry){ /* Skip "." and ".." */ if(direntry->d_name[0] == '.' @@ -1248,6 +1441,7 @@ } /* Is this directory entry a runnable program? */ +__attribute__((nonnull, warn_unused_result)) int runnable_hook(const struct dirent *direntry){ int ret; size_t sret; @@ -1261,7 +1455,7 @@ sret = strspn(direntry->d_name, "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789" - "_-"); + "_.-"); if((direntry->d_name)[sret] != '\0'){ /* Contains non-allowed characters */ if(debug){ @@ -1271,14 +1465,7 @@ return 0; } - char *fullname = NULL; - ret = asprintf(&fullname, "%s/%s", hookdir, direntry->d_name); - if(ret < 0){ - perror_plus("asprintf"); - return 0; - } - - ret = stat(fullname, &st); + ret = fstatat(hookdir_fd, direntry->d_name, &st, 0); if(ret == -1){ if(debug){ perror_plus("Could not stat hook"); @@ -1301,24 +1488,30 @@ } return 0; } + if(debug){ + fprintf_plus(stderr, "Hook \"%s\" is acceptable\n", + direntry->d_name); + } return 1; } -int avahi_loop_with_timeout(AvahiSimplePoll *s, int retry_interval){ +__attribute__((nonnull, warn_unused_result)) +int avahi_loop_with_timeout(AvahiSimplePoll *s, int retry_interval, + mandos_context *mc){ int ret; struct timespec now; struct timespec waited_time; intmax_t block_time; while(true){ - if(mc.current_server == NULL){ - if (debug){ + if(mc->current_server == NULL){ + if(debug){ fprintf_plus(stderr, "Wait until first server is found." " No timeout!\n"); } ret = avahi_simple_poll_iterate(s, -1); } else { - if (debug){ + if(debug){ fprintf_plus(stderr, "Check current_server if we should run" " it, or wait\n"); } @@ -1331,9 +1524,9 @@ /* Calculating in ms how long time between now and server who we visted longest time ago. Now - last seen. */ waited_time.tv_sec = (now.tv_sec - - mc.current_server->last_seen.tv_sec); + - mc->current_server->last_seen.tv_sec); waited_time.tv_nsec = (now.tv_nsec - - mc.current_server->last_seen.tv_nsec); + - mc->current_server->last_seen.tv_nsec); /* total time is 10s/10,000ms. Converting to s from ms by dividing by 1,000, and ns to ms by dividing by 1,000,000. */ @@ -1341,27 +1534,27 @@ - ((intmax_t)waited_time.tv_sec * 1000)) - ((intmax_t)waited_time.tv_nsec / 1000000)); - if (debug){ + if(debug){ fprintf_plus(stderr, "Blocking for %" PRIdMAX " ms\n", block_time); } if(block_time <= 0){ - ret = start_mandos_communication(mc.current_server->ip, - mc.current_server->port, - mc.current_server->if_index, - mc.current_server->af); + ret = start_mandos_communication(mc->current_server->ip, + mc->current_server->port, + mc->current_server->if_index, + mc->current_server->af, mc); if(ret == 0){ - avahi_simple_poll_quit(mc.simple_poll); + avahi_simple_poll_quit(s); return 0; } ret = clock_gettime(CLOCK_MONOTONIC, - &mc.current_server->last_seen); + &mc->current_server->last_seen); if(ret == -1){ perror_plus("clock_gettime"); return -1; } - mc.current_server = mc.current_server->next; + mc->current_server = mc->current_server->next; block_time = 0; /* Call avahi to find new Mandos servers, but don't block */ } @@ -1369,129 +1562,470 @@ ret = avahi_simple_poll_iterate(s, (int)block_time); } if(ret != 0){ - if (ret > 0 or errno != EINTR){ + if(ret > 0 or errno != EINTR){ return (ret != 1) ? ret : 0; } } } } -bool run_network_hooks(const char *mode, const char *interface, +/* 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; - struct dirent *direntry; - int ret; - int numhooks = scandir(hookdir, &direntries, runnable_hook, - alphasort); + struct dirent **direntries = NULL; + if(hookdir_fd == -1){ + hookdir_fd = open(hookdir, O_RDONLY); + if(hookdir_fd == -1){ + if(errno == ENOENT){ + if(debug){ + fprintf_plus(stderr, "Network hook directory \"%s\" not" + " found\n", hookdir); + } + } else { + perror_plus("open"); + } + return; + } + } +#ifdef __GLIBC__ +#if __GLIBC_PREREQ(2, 15) + int numhooks = scandirat(hookdir_fd, ".", &direntries, + runnable_hook, alphasort); +#else /* not __GLIBC_PREREQ(2, 15) */ + int numhooks = scandir(hookdir, &direntries, runnable_hook, + alphasort); +#endif /* not __GLIBC_PREREQ(2, 15) */ +#else /* not __GLIBC__ */ + int numhooks = scandir(hookdir, &direntries, runnable_hook, + alphasort); +#endif /* not __GLIBC__ */ if(numhooks == -1){ perror_plus("scandir"); - } else { - int devnull = open("/dev/null", O_RDONLY); - for(int i = 0; i < numhooks; i++){ - direntry = direntries[0]; - char *fullname = NULL; - ret = asprintf(&fullname, "%s/%s", hookdir, direntry->d_name); - if(ret < 0){ + return; + } + struct dirent *direntry; + int ret; + int devnull = open("/dev/null", O_RDONLY); + for(int i = 0; i < numhooks; i++){ + direntry = direntries[i]; + if(debug){ + fprintf_plus(stderr, "Running network hook \"%s\"\n", + direntry->d_name); + } + pid_t hook_pid = fork(); + if(hook_pid == 0){ + /* Child */ + /* Raise privileges */ + errno = raise_privileges_permanently(); + if(errno != 0){ + perror_plus("Failed to raise privileges"); + _exit(EX_NOPERM); + } + /* 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); + } + ret = setenv("MANDOSNETHOOKDIR", hookdir, 1); + if(ret == -1){ + perror_plus("setenv"); + _exit(EX_OSERR); + } + ret = setenv("DEVICE", interface, 1); + if(ret == -1){ + perror_plus("setenv"); + _exit(EX_OSERR); + } + ret = setenv("VERBOSITY", debug ? "1" : "0", 1); + if(ret == -1){ + perror_plus("setenv"); + _exit(EX_OSERR); + } + ret = setenv("MODE", mode, 1); + if(ret == -1){ + perror_plus("setenv"); + _exit(EX_OSERR); + } + char *delaystring; + ret = asprintf(&delaystring, "%f", (double)delay); + if(ret == -1){ perror_plus("asprintf"); - continue; + _exit(EX_OSERR); } - pid_t hook_pid = fork(); - if(hook_pid == 0){ - /* Child */ - dup2(devnull, STDIN_FILENO); - close(devnull); - dup2(STDERR_FILENO, STDOUT_FILENO); - ret = setenv("MANDOSNETHOOKDIR", hookdir, 1); - if(ret == -1){ - perror_plus("setenv"); - return false; - } - ret = setenv("DEVICE", interface, 1); - if(ret == -1){ - perror_plus("setenv"); - return false; - } - ret = setenv("VERBOSE", debug ? "1" : "0", 1); - if(ret == -1){ - perror_plus("setenv"); - return false; - } - ret = setenv("MODE", mode, 1); - if(ret == -1){ - perror_plus("setenv"); - return false; - } - char *delaystring; - ret = asprintf(&delaystring, "%f", delay); - if(ret == -1){ - perror_plus("asprintf"); - return false; - } - ret = setenv("DELAY", delaystring, 1); - if(ret == -1){ - free(delaystring); - perror_plus("setenv"); - return false; - } + ret = setenv("DELAY", delaystring, 1); + if(ret == -1){ free(delaystring); - ret = execl(fullname, direntry->d_name, mode, NULL); - perror_plus("execl"); - } else { - int status; - if(TEMP_FAILURE_RETRY(waitpid(hook_pid, &status, 0)) == -1){ - perror_plus("waitpid"); - free(fullname); - continue; - } - if(WIFEXITED(status)){ - if(WEXITSTATUS(status) != 0){ - fprintf_plus(stderr, "Warning: network hook \"%s\" exited" - " with status %d\n", direntry->d_name, - WEXITSTATUS(status)); - free(fullname); - continue; - } - } else if(WIFSIGNALED(status)){ - fprintf_plus(stderr, "Warning: network hook \"%s\" died by" - " signal %d\n", direntry->d_name, - WTERMSIG(status)); - free(fullname); - continue; - } else { - fprintf_plus(stderr, "Warning: network hook \"%s\"" - " crashed\n", direntry->d_name); - free(fullname); - continue; - } - } - free(fullname); - if(quit_now){ - break; - } - } - close(devnull); - } - return true; + perror_plus("setenv"); + _exit(EX_OSERR); + } + free(delaystring); + if(connect_to != NULL){ + ret = setenv("CONNECT", connect_to, 1); + if(ret == -1){ + perror_plus("setenv"); + _exit(EX_OSERR); + } + } + int hook_fd = 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){ + perror_plus("close"); + _exit(EXIT_FAILURE); + } + if(fexecve(hook_fd, (char *const []){ direntry->d_name, NULL }, + environ) == -1){ + perror_plus("fexecve"); + _exit(EXIT_FAILURE); + } + } else { + if(hook_pid == -1){ + perror_plus("fork"); + free(direntry); + continue; + } + int status; + if(TEMP_FAILURE_RETRY(waitpid(hook_pid, &status, 0)) == -1){ + perror_plus("waitpid"); + free(direntry); + continue; + } + if(WIFEXITED(status)){ + if(WEXITSTATUS(status) != 0){ + fprintf_plus(stderr, "Warning: network hook \"%s\" exited" + " with status %d\n", direntry->d_name, + WEXITSTATUS(status)); + free(direntry); + continue; + } + } else if(WIFSIGNALED(status)){ + fprintf_plus(stderr, "Warning: network hook \"%s\" died by" + " signal %d\n", direntry->d_name, + WTERMSIG(status)); + free(direntry); + continue; + } else { + fprintf_plus(stderr, "Warning: network hook \"%s\"" + " crashed\n", direntry->d_name); + free(direntry); + continue; + } + } + if(debug){ + fprintf_plus(stderr, "Network hook \"%s\" ran successfully\n", + direntry->d_name); + } + free(direntry); + } + free(direntries); + if((int)TEMP_FAILURE_RETRY(close(hookdir_fd)) == -1){ + perror_plus("close"); + } else { + hookdir_fd = -1; + } + close(devnull); +} + +__attribute__((nonnull, warn_unused_result)) +error_t bring_up_interface(const char *const interface, + const float delay){ + error_t old_errno = errno; + int ret; + struct ifreq network; + unsigned int if_index = if_nametoindex(interface); + if(if_index == 0){ + fprintf_plus(stderr, "No such interface: \"%s\"\n", interface); + errno = old_errno; + return ENXIO; + } + + if(quit_now){ + errno = old_errno; + return EINTR; + } + + if(not interface_is_up(interface)){ + error_t ret_errno = 0, ioctl_errno = 0; + if(not get_flags(interface, &network)){ + ret_errno = errno; + fprintf_plus(stderr, "Failed to get flags for interface " + "\"%s\"\n", interface); + errno = old_errno; + return ret_errno; + } + network.ifr_flags |= IFF_UP; /* set flag */ + + int sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); + if(sd == -1){ + ret_errno = errno; + perror_plus("socket"); + errno = old_errno; + return ret_errno; + } + + if(quit_now){ + ret = (int)TEMP_FAILURE_RETRY(close(sd)); + if(ret == -1){ + perror_plus("close"); + } + errno = old_errno; + return EINTR; + } + + if(debug){ + fprintf_plus(stderr, "Bringing up interface \"%s\"\n", + interface); + } + + /* Raise privileges */ + ret_errno = raise_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to raise privileges"); + } + +#ifdef __linux__ + int ret_linux; + bool restore_loglevel = false; + if(ret_errno == 0){ + /* Lower kernel loglevel to KERN_NOTICE to avoid KERN_INFO + messages about the network interface to mess up the prompt */ + ret_linux = klogctl(8, NULL, 5); + if(ret_linux == -1){ + perror_plus("klogctl"); + } else { + restore_loglevel = true; + } + } +#endif /* __linux__ */ + int ret_setflags = ioctl(sd, SIOCSIFFLAGS, &network); + ioctl_errno = errno; +#ifdef __linux__ + if(restore_loglevel){ + ret_linux = klogctl(7, NULL, 0); + if(ret_linux == -1){ + perror_plus("klogctl"); + } + } +#endif /* __linux__ */ + + /* If raise_privileges() succeeded above */ + if(ret_errno == 0){ + /* Lower privileges */ + ret_errno = lower_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to lower privileges"); + } + } + + /* Close the socket */ + ret = (int)TEMP_FAILURE_RETRY(close(sd)); + if(ret == -1){ + perror_plus("close"); + } + + if(ret_setflags == -1){ + errno = ioctl_errno; + perror_plus("ioctl SIOCSIFFLAGS +IFF_UP"); + errno = old_errno; + return ioctl_errno; + } + } else if(debug){ + fprintf_plus(stderr, "Interface \"%s\" is already up; good\n", + interface); + } + + /* Sleep checking until interface is running. + Check every 0.25s, up to total time of delay */ + for(int i=0; i < delay * 4; i++){ + if(interface_is_running(interface)){ + break; + } + struct timespec sleeptime = { .tv_nsec = 250000000 }; + ret = nanosleep(&sleeptime, NULL); + if(ret == -1 and errno != EINTR){ + perror_plus("nanosleep"); + } + } + + errno = old_errno; + return 0; +} + +__attribute__((nonnull, warn_unused_result)) +error_t take_down_interface(const char *const interface){ + error_t old_errno = errno; + struct ifreq network; + unsigned int if_index = if_nametoindex(interface); + if(if_index == 0){ + fprintf_plus(stderr, "No such interface: \"%s\"\n", interface); + errno = old_errno; + return ENXIO; + } + if(interface_is_up(interface)){ + error_t ret_errno = 0, ioctl_errno = 0; + if(not get_flags(interface, &network) and debug){ + ret_errno = errno; + fprintf_plus(stderr, "Failed to get flags for interface " + "\"%s\"\n", interface); + errno = old_errno; + return ret_errno; + } + network.ifr_flags &= ~(short)IFF_UP; /* clear flag */ + + int sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); + if(sd == -1){ + ret_errno = errno; + perror_plus("socket"); + errno = old_errno; + return ret_errno; + } + + if(debug){ + fprintf_plus(stderr, "Taking down interface \"%s\"\n", + interface); + } + + /* Raise privileges */ + ret_errno = raise_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to raise privileges"); + } + + int ret_setflags = ioctl(sd, SIOCSIFFLAGS, &network); + ioctl_errno = errno; + + /* If raise_privileges() succeeded above */ + if(ret_errno == 0){ + /* Lower privileges */ + ret_errno = lower_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to lower privileges"); + } + } + + /* Close the socket */ + int ret = (int)TEMP_FAILURE_RETRY(close(sd)); + if(ret == -1){ + perror_plus("close"); + } + + if(ret_setflags == -1){ + errno = ioctl_errno; + perror_plus("ioctl SIOCSIFFLAGS -IFF_UP"); + errno = old_errno; + return ioctl_errno; + } + } else if(debug){ + fprintf_plus(stderr, "Interface \"%s\" is already down; odd\n", + interface); + } + + errno = old_errno; + return 0; } int main(int argc, char *argv[]){ + mandos_context mc = { .server = NULL, .dh_bits = 0, + .priority = "SECURE256:!CTYPE-X.509:" + "+CTYPE-OPENPGP:!RSA", .current_server = NULL, + .interfaces = NULL, .interfaces_size = 0 }; AvahiSServiceBrowser *sb = NULL; - int error; + error_t ret_errno; int ret; intmax_t tmpmax; char *tmp; int exitcode = EXIT_SUCCESS; - const char *interface = ""; - struct ifreq network; - int sd = -1; - bool take_down_interface = false; - uid_t uid; - gid_t gid; - char tempdir[] = "/tmp/mandosXXXXXX"; - bool tempdir_created = false; + char *interfaces_to_take_down = NULL; + size_t interfaces_to_take_down_size = 0; + char run_tempdir[] = "/run/tmp/mandosXXXXXX"; + char old_tempdir[] = "/tmp/mandosXXXXXX"; + char *tempdir = NULL; AvahiIfIndex if_index = AVAHI_IF_UNSPEC; const char *seckey = PATHDIR "/" SECKEY; const char *pubkey = PATHDIR "/" PUBKEY; + char *interfaces_hooks = NULL; bool gnutls_initialized = false; bool gpgme_initialized = false; @@ -1559,7 +2093,7 @@ .group = 2 }, { .name = "retry", .key = 132, .arg = "SECONDS", - .doc = "Retry interval used when denied by the mandos server", + .doc = "Retry interval used when denied by the Mandos server", .group = 2 }, { .name = "network-hook-dir", .key = 133, .arg = "DIR", @@ -1588,7 +2122,11 @@ connect_to = arg; break; case 'i': /* --interface */ - interface = arg; + ret_errno = argz_add_sep(&mc.interfaces, &mc.interfaces_size, + arg, (int)','); + if(ret_errno != 0){ + argp_error(state, "%s", strerror(ret_errno)); + } break; case 's': /* --seckey */ seckey = arg; @@ -1637,8 +2175,6 @@ argp_state_help(state, state->out_stream, ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR); case 'V': /* --version */ - fprintf_plus(state->out_stream, - "Mandos plugin mandos-client: "); fprintf_plus(state->out_stream, "%s\n", argp_program_version); exit(argp_err_exit_status); break; @@ -1672,126 +2208,108 @@ { /* Work around Debian bug #633582: */ - struct stat st; - - /* Re-raise priviliges */ - errno = 0; - ret = seteuid(0); - if(ret == -1){ - perror_plus("seteuid"); - } - - if(strcmp(seckey, PATHDIR "/" SECKEY) == 0){ - int seckey_fd = open(seckey, O_RDONLY); - if(seckey_fd == -1){ - perror_plus("open"); - } else { - ret = (int)TEMP_FAILURE_RETRY(fstat(seckey_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(seckey_fd, uid, gid); - if(ret == -1){ - perror_plus("fchown"); - } - } - } - TEMP_FAILURE_RETRY(close(seckey_fd)); - } - } - - if(strcmp(pubkey, PATHDIR "/" PUBKEY) == 0){ - int pubkey_fd = open(pubkey, O_RDONLY); - if(pubkey_fd == -1){ - perror_plus("open"); - } else { - ret = (int)TEMP_FAILURE_RETRY(fstat(pubkey_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(pubkey_fd, uid, gid); - if(ret == -1){ - perror_plus("fchown"); - } - } - } - TEMP_FAILURE_RETRY(close(pubkey_fd)); - } - } - - /* Lower privileges */ - errno = 0; - ret = seteuid(uid); - if(ret == -1){ - perror_plus("seteuid"); + + /* Re-raise privileges */ + ret_errno = raise_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to raise privileges"); + } else { + struct stat st; + + if(strcmp(seckey, PATHDIR "/" SECKEY) == 0){ + int seckey_fd = open(seckey, O_RDONLY); + if(seckey_fd == -1){ + perror_plus("open"); + } else { + ret = (int)TEMP_FAILURE_RETRY(fstat(seckey_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(seckey_fd, uid, gid); + if(ret == -1){ + perror_plus("fchown"); + } + } + } + TEMP_FAILURE_RETRY(close(seckey_fd)); + } + } + + if(strcmp(pubkey, PATHDIR "/" PUBKEY) == 0){ + int pubkey_fd = open(pubkey, O_RDONLY); + if(pubkey_fd == -1){ + perror_plus("open"); + } else { + ret = (int)TEMP_FAILURE_RETRY(fstat(pubkey_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(pubkey_fd, uid, gid); + if(ret == -1){ + perror_plus("fchown"); + } + } + } + TEMP_FAILURE_RETRY(close(pubkey_fd)); + } + } + + /* Lower privileges */ + ret_errno = lower_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to lower privileges"); + } + } + } + + /* Remove invalid interface names (except "none") */ + { + char *interface = NULL; + while((interface = argz_next(mc.interfaces, mc.interfaces_size, + interface))){ + if(strcmp(interface, "none") != 0 + and if_nametoindex(interface) == 0){ + if(interface[0] != '\0'){ + fprintf_plus(stderr, "Not using nonexisting interface" + " \"%s\"\n", interface); + } + argz_delete(&mc.interfaces, &mc.interfaces_size, interface); + interface = NULL; + } } } /* Run network hooks */ { - /* Re-raise priviliges */ - errno = 0; - ret = seteuid(0); - if(ret == -1){ - perror_plus("seteuid"); - } - if(not run_network_hooks("start", interface, delay)){ - goto end; - } - /* Lower privileges */ - errno = 0; - ret = seteuid(uid); - if(ret == -1){ - perror_plus("seteuid"); - } + if(mc.interfaces != NULL){ + interfaces_hooks = malloc(mc.interfaces_size); + if(interfaces_hooks == NULL){ + perror_plus("malloc"); + goto end; + } + memcpy(interfaces_hooks, mc.interfaces, mc.interfaces_size); + argz_stringify(interfaces_hooks, mc.interfaces_size, (int)','); + } + run_network_hooks("start", interfaces_hooks != NULL ? + interfaces_hooks : "", delay); } if(not debug){ avahi_set_log_function(empty_log); } - if(interface[0] == '\0'){ - struct dirent **direntries; - /* First look for interfaces that are up */ - ret = scandir(sys_class_net, &direntries, up_interface, - alphasort); - if(ret == 0){ - /* No up interfaces, look for any good interfaces */ - free(direntries); - ret = scandir(sys_class_net, &direntries, good_interface, - alphasort); - } - if(ret >= 1){ - /* Pick the first interface returned */ - interface = strdup(direntries[0]->d_name); - if(debug){ - fprintf_plus(stderr, "Using interface \"%s\"\n", interface); - } - if(interface == NULL){ - perror_plus("malloc"); - free(direntries); - exitcode = EXIT_FAILURE; - goto end; - } - free(direntries); - } else { - free(direntries); - fprintf_plus(stderr, "Could not find a network interface\n"); - exitcode = EXIT_FAILURE; - goto end; - } - } - /* Initialize Avahi early so avahi_simple_poll_quit() can be called from the signal handler */ /* Initialize the pseudo-RNG for Avahi */ srand((unsigned int) time(NULL)); - mc.simple_poll = avahi_simple_poll_new(); - if(mc.simple_poll == NULL){ + simple_poll = avahi_simple_poll_new(); + if(simple_poll == NULL){ fprintf_plus(stderr, "Avahi: Failed to create simple poll object.\n"); exitcode = EX_UNAVAILABLE; @@ -1861,148 +2379,95 @@ } } - /* If the interface is down, bring it up */ - if(strcmp(interface, "none") != 0){ - if_index = (AvahiIfIndex) if_nametoindex(interface); - if(if_index == 0){ - fprintf_plus(stderr, "No such interface: \"%s\"\n", interface); - exitcode = EX_UNAVAILABLE; - goto end; - } - - if(quit_now){ - goto end; - } - - /* Re-raise priviliges */ - errno = 0; - ret = seteuid(0); - if(ret == -1){ - perror_plus("seteuid"); - } - -#ifdef __linux__ - /* Lower kernel loglevel to KERN_NOTICE to avoid KERN_INFO - messages about the network interface to mess up the prompt */ - ret = klogctl(8, NULL, 5); - bool restore_loglevel = true; - if(ret == -1){ - restore_loglevel = false; - perror_plus("klogctl"); - } -#endif /* __linux__ */ - - sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); - if(sd < 0){ - perror_plus("socket"); - exitcode = EX_OSERR; -#ifdef __linux__ - if(restore_loglevel){ - ret = klogctl(7, NULL, 0); - if(ret == -1){ - perror_plus("klogctl"); - } - } -#endif /* __linux__ */ - /* Lower privileges */ - errno = 0; - ret = seteuid(uid); - if(ret == -1){ - perror_plus("seteuid"); - } - goto end; - } - strcpy(network.ifr_name, interface); - ret = ioctl(sd, SIOCGIFFLAGS, &network); - if(ret == -1){ - perror_plus("ioctl SIOCGIFFLAGS"); -#ifdef __linux__ - if(restore_loglevel){ - ret = klogctl(7, NULL, 0); - if(ret == -1){ - perror_plus("klogctl"); - } - } -#endif /* __linux__ */ - exitcode = EX_OSERR; - /* Lower privileges */ - errno = 0; - ret = seteuid(uid); - if(ret == -1){ - perror_plus("seteuid"); - } - goto end; - } - if((network.ifr_flags & IFF_UP) == 0){ - network.ifr_flags |= IFF_UP; - take_down_interface = true; - ret = ioctl(sd, SIOCSIFFLAGS, &network); - if(ret == -1){ - take_down_interface = false; - perror_plus("ioctl SIOCSIFFLAGS +IFF_UP"); - exitcode = EX_OSERR; -#ifdef __linux__ - if(restore_loglevel){ - ret = klogctl(7, NULL, 0); - if(ret == -1){ - perror_plus("klogctl"); + /* If no interfaces were specified, make a list */ + if(mc.interfaces == NULL){ + struct dirent **direntries = NULL; + /* Look for any good interfaces */ + ret = scandir(sys_class_net, &direntries, good_interface, + alphasort); + if(ret >= 1){ + /* Add all found interfaces to interfaces list */ + for(int i = 0; i < ret; ++i){ + ret_errno = argz_add(&mc.interfaces, &mc.interfaces_size, + direntries[i]->d_name); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("argz_add"); + free(direntries[i]); + continue; + } + if(debug){ + fprintf_plus(stderr, "Will use interface \"%s\"\n", + direntries[i]->d_name); + } + free(direntries[i]); + } + free(direntries); + } else { + if(ret == 0){ + free(direntries); + } + fprintf_plus(stderr, "Could not find a network interface\n"); + exitcode = EXIT_FAILURE; + goto end; + } + } + + /* Bring up interfaces which are down, and remove any "none"s */ + { + char *interface = NULL; + while((interface = argz_next(mc.interfaces, mc.interfaces_size, + interface))){ + /* If interface name is "none", stop bringing up interfaces. + Also remove all instances of "none" from the list */ + if(strcmp(interface, "none") == 0){ + argz_delete(&mc.interfaces, &mc.interfaces_size, + interface); + interface = NULL; + while((interface = argz_next(mc.interfaces, + mc.interfaces_size, interface))){ + if(strcmp(interface, "none") == 0){ + argz_delete(&mc.interfaces, &mc.interfaces_size, + interface); + interface = NULL; } } -#endif /* __linux__ */ - /* Lower privileges */ - errno = 0; - ret = seteuid(uid); - if(ret == -1){ - perror_plus("seteuid"); - } - goto end; - } - } - /* Sleep checking until interface is running. - Check every 0.25s, up to total time of delay */ - for(int i=0; i < delay * 4; i++){ - ret = ioctl(sd, SIOCGIFFLAGS, &network); - if(ret == -1){ - perror_plus("ioctl SIOCGIFFLAGS"); - } else if(network.ifr_flags & IFF_RUNNING){ break; } - struct timespec sleeptime = { .tv_nsec = 250000000 }; - ret = nanosleep(&sleeptime, NULL); - if(ret == -1 and errno != EINTR){ - perror_plus("nanosleep"); - } - } - if(not take_down_interface){ - /* We won't need the socket anymore */ - ret = (int)TEMP_FAILURE_RETRY(close(sd)); - if(ret == -1){ - perror_plus("close"); - } - } -#ifdef __linux__ - if(restore_loglevel){ - /* Restores kernel loglevel to default */ - ret = klogctl(7, NULL, 0); - if(ret == -1){ - perror_plus("klogctl"); - } - } -#endif /* __linux__ */ - /* Lower privileges */ - errno = 0; - /* Lower privileges */ - ret = seteuid(uid); - if(ret == -1){ - perror_plus("seteuid"); - } + bool interface_was_up = interface_is_up(interface); + errno = bring_up_interface(interface, delay); + if(not interface_was_up){ + if(errno != 0){ + perror_plus("Failed to bring up interface"); + } else { + errno = argz_add(&interfaces_to_take_down, + &interfaces_to_take_down_size, + interface); + if(errno != 0){ + perror_plus("argz_add"); + } + } + } + } + if(debug and (interfaces_to_take_down == NULL)){ + fprintf_plus(stderr, "No interfaces were brought up\n"); + } + } + + /* If we only got one interface, explicitly use only that one */ + if(argz_count(mc.interfaces, mc.interfaces_size) == 1){ + if(debug){ + fprintf_plus(stderr, "Using only interface \"%s\"\n", + mc.interfaces); + } + if_index = (AvahiIfIndex)if_nametoindex(mc.interfaces); } if(quit_now){ goto end; } - ret = init_gnutls_global(pubkey, seckey); + ret = init_gnutls_global(pubkey, seckey, &mc); if(ret == -1){ fprintf_plus(stderr, "init_gnutls_global failed\n"); exitcode = EX_UNAVAILABLE; @@ -2015,17 +2480,25 @@ goto end; } - if(mkdtemp(tempdir) == NULL){ + /* Try /run/tmp before /tmp */ + tempdir = mkdtemp(run_tempdir); + if(tempdir == NULL and errno == ENOENT){ + if(debug){ + fprintf_plus(stderr, "Tempdir %s did not work, trying %s\n", + run_tempdir, old_tempdir); + } + tempdir = mkdtemp(old_tempdir); + } + if(tempdir == NULL){ perror_plus("mkdtemp"); goto end; } - tempdir_created = true; if(quit_now){ goto end; } - if(not init_gpgme(pubkey, seckey, tempdir)){ + if(not init_gpgme(pubkey, seckey, tempdir, &mc)){ fprintf_plus(stderr, "init_gpgme failed\n"); exitcode = EX_UNAVAILABLE; goto end; @@ -2041,6 +2514,7 @@ /* Connect directly, do not use Zeroconf */ /* (Mainly meant for debugging) */ char *address = strrchr(connect_to, ':'); + if(address == NULL){ fprintf_plus(stderr, "No colon in address\n"); exitcode = EX_USAGE; @@ -2051,21 +2525,21 @@ goto end; } - uint16_t port; + in_port_t port; errno = 0; tmpmax = strtoimax(address+1, &tmp, 10); if(errno != 0 or tmp == address+1 or *tmp != '\0' - or tmpmax != (uint16_t)tmpmax){ + or tmpmax != (in_port_t)tmpmax){ fprintf_plus(stderr, "Bad port number\n"); exitcode = EX_USAGE; goto end; } - + if(quit_now){ goto end; } - port = (uint16_t)tmpmax; + port = (in_port_t)tmpmax; *address = '\0'; /* Colon in address indicates IPv6 */ int af; @@ -2087,7 +2561,8 @@ } while(not quit_now){ - ret = start_mandos_communication(address, port, if_index, af); + ret = start_mandos_communication(address, port, if_index, af, + &mc); if(quit_now or ret == 0){ break; } @@ -2095,10 +2570,10 @@ fprintf_plus(stderr, "Retrying in %d seconds\n", (int)retry_interval); } - sleep((int)retry_interval); + sleep((unsigned int)retry_interval); } - if (not quit_now){ + if(not quit_now){ exitcode = EXIT_SUCCESS; } @@ -2119,9 +2594,8 @@ config.publish_domain = 0; /* Allocate a new server */ - mc.server = avahi_server_new(avahi_simple_poll_get - (mc.simple_poll), &config, NULL, - NULL, &error); + mc.server = avahi_server_new(avahi_simple_poll_get(simple_poll), + &config, NULL, NULL, &ret_errno); /* Free the Avahi configuration data */ avahi_server_config_free(&config); @@ -2130,7 +2604,7 @@ /* Check if creating the Avahi server object succeeded */ if(mc.server == NULL){ fprintf_plus(stderr, "Failed to create Avahi server: %s\n", - avahi_strerror(error)); + avahi_strerror(ret_errno)); exitcode = EX_UNAVAILABLE; goto end; } @@ -2142,7 +2616,8 @@ /* Create the Avahi service browser */ sb = avahi_s_service_browser_new(mc.server, if_index, AVAHI_PROTO_UNSPEC, "_mandos._tcp", - NULL, 0, browse_callback, NULL); + NULL, 0, browse_callback, + (void *)&mc); if(sb == NULL){ fprintf_plus(stderr, "Failed to create service browser: %s\n", avahi_strerror(avahi_server_errno(mc.server))); @@ -2159,9 +2634,9 @@ if(debug){ fprintf_plus(stderr, "Starting Avahi loop search\n"); } - - ret = avahi_loop_with_timeout(mc.simple_poll, - (int)(retry_interval * 1000)); + + ret = avahi_loop_with_timeout(simple_poll, + (int)(retry_interval * 1000), &mc); if(debug){ fprintf_plus(stderr, "avahi_loop_with_timeout exited %s\n", (ret == 0) ? "successfully" : "with error"); @@ -2174,14 +2649,16 @@ } /* Cleanup things */ + free(mc.interfaces); + if(sb != NULL) avahi_s_service_browser_free(sb); if(mc.server != NULL) avahi_server_free(mc.server); - if(mc.simple_poll != NULL) - avahi_simple_poll_free(mc.simple_poll); + if(simple_poll != NULL) + avahi_simple_poll_free(simple_poll); if(gnutls_initialized){ gnutls_certificate_free_credentials(mc.cred); @@ -2192,88 +2669,109 @@ if(gpgme_initialized){ gpgme_release(mc.ctx); } - + /* Cleans up the circular linked list of Mandos servers the client has seen */ if(mc.current_server != NULL){ mc.current_server->prev->next = NULL; while(mc.current_server != NULL){ server *next = mc.current_server->next; +#ifdef __GNUC__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-qual" +#endif + free((char *)(mc.current_server->ip)); +#ifdef __GNUC__ +#pragma GCC diagnostic pop +#endif free(mc.current_server); mc.current_server = next; } } - /* Re-raise priviliges */ + /* Re-raise privileges */ { - errno = 0; - ret = seteuid(0); - if(ret == -1){ - perror_plus("seteuid"); - } - /* Run network hooks */ - if(not run_network_hooks("stop", interface, delay)){ - goto end; + ret_errno = raise_privileges(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to raise privileges"); + } else { + + /* Run network hooks */ + run_network_hooks("stop", interfaces_hooks != NULL ? + interfaces_hooks : "", delay); + + /* Take down the network interfaces which were brought up */ + { + char *interface = NULL; + while((interface=argz_next(interfaces_to_take_down, + interfaces_to_take_down_size, + interface))){ + ret_errno = take_down_interface(interface); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to take down interface"); + } + } + if(debug and (interfaces_to_take_down == NULL)){ + fprintf_plus(stderr, "No interfaces needed to be taken" + " down\n"); + } + } } - /* Take down the network interface */ - if(take_down_interface and geteuid() == 0){ - ret = ioctl(sd, SIOCGIFFLAGS, &network); - if(ret == -1){ - perror_plus("ioctl SIOCGIFFLAGS"); - } else if(network.ifr_flags & IFF_UP){ - network.ifr_flags &= ~(short)IFF_UP; /* clear flag */ - ret = ioctl(sd, SIOCSIFFLAGS, &network); - if(ret == -1){ - perror_plus("ioctl SIOCSIFFLAGS -IFF_UP"); - } - } - ret = (int)TEMP_FAILURE_RETRY(close(sd)); - if(ret == -1){ - perror_plus("close"); - } + ret_errno = lower_privileges_permanently(); + if(ret_errno != 0){ + errno = ret_errno; + perror_plus("Failed to lower privileges permanently"); } } - /* Lower privileges permanently */ - errno = 0; - ret = setuid(uid); - if(ret == -1){ - perror_plus("setuid"); - } + + free(interfaces_to_take_down); + free(interfaces_hooks); /* Removes the GPGME temp directory and all files inside */ - if(tempdir_created){ + if(tempdir != NULL){ struct dirent **direntries = NULL; - struct dirent *direntry = NULL; - int numentries = scandir(tempdir, &direntries, notdotentries, - alphasort); - if (numentries > 0){ - for(int i = 0; i < numentries; i++){ - direntry = direntries[i]; - char *fullname = NULL; - ret = asprintf(&fullname, "%s/%s", tempdir, - direntry->d_name); - if(ret < 0){ - perror_plus("asprintf"); - continue; - } - ret = remove(fullname); - if(ret == -1){ - fprintf_plus(stderr, "remove(\"%s\"): %s\n", fullname, - strerror(errno)); - } - free(fullname); + int tempdir_fd = (int)TEMP_FAILURE_RETRY(open(tempdir, O_RDONLY | + O_NOFOLLOW)); + if(tempdir_fd == -1){ + perror_plus("open"); + } else { +#ifdef __GLIBC__ +#if __GLIBC_PREREQ(2, 15) + int numentries = scandirat(tempdir_fd, ".", &direntries, + notdotentries, alphasort); +#else /* not __GLIBC_PREREQ(2, 15) */ + int numentries = scandir(tempdir, &direntries, notdotentries, + alphasort); +#endif /* not __GLIBC_PREREQ(2, 15) */ +#else /* not __GLIBC__ */ + int numentries = scandir(tempdir, &direntries, notdotentries, + alphasort); +#endif /* not __GLIBC__ */ + if(numentries >= 0){ + for(int i = 0; i < numentries; i++){ + ret = unlinkat(tempdir_fd, direntries[i]->d_name, 0); + if(ret == -1){ + fprintf_plus(stderr, "unlinkat(open(\"%s\", O_RDONLY)," + " \"%s\", 0): %s\n", tempdir, + direntries[i]->d_name, strerror(errno)); + } + free(direntries[i]); + } + + /* need to clean even if 0 because man page doesn't specify */ + free(direntries); + if(numentries == -1){ + perror_plus("scandir"); + } + ret = rmdir(tempdir); + if(ret == -1 and errno != ENOENT){ + perror_plus("rmdir"); + } } - } - - /* need to clean even if 0 because man page doesn't specify */ - free(direntries); - if (numentries == -1){ - perror_plus("scandir"); - } - ret = rmdir(tempdir); - if(ret == -1 and errno != ENOENT){ - perror_plus("rmdir"); + TEMP_FAILURE_RETRY(close(tempdir_fd)); } } === modified file 'plugins.d/mandos-client.xml' --- plugins.d/mandos-client.xml 2011-11-13 20:49:21 +0000 +++ plugins.d/mandos-client.xml 2015-03-10 18:52:09 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,7 +33,9 @@ 2008 2009 - 2011 + 2012 + 2013 + 2014 Teddy Hogeborn Björn Påhlsson @@ -64,11 +66,13 @@ >PORT - + - + NAME,NAME + @@ -102,7 +106,8 @@ - + @@ -136,10 +141,10 @@ communicates with mandos8 to get a password. In slightly more detail, this client program - brings up a network interface, uses the interface’s IPv6 - link-local address to get network connectivity, uses Zeroconf to - find servers on the local network, and communicates with servers - using TLS with an OpenPGP key to ensure authenticity and + brings up network interfaces, uses the interfaces’ IPv6 + link-local addresses to get network connectivity, uses Zeroconf + to find servers on the local network, and communicates with + servers using TLS with an OpenPGP key to ensure authenticity and confidentiality. This client program keeps running, trying all servers on the network, until it receives a satisfactory reply or a TERM signal. After all servers have been tried, all @@ -147,6 +152,24 @@ will wait indefinitely for new servers to appear. + The network interfaces are selected like this: If any interfaces + are specified using the option, + those interface are used. Otherwise, + &COMMANDNAME; will use all interfaces that + are not loopback interfaces, are not point-to-point interfaces, + are capable of broadcasting and do not have the NOARP flag (see + netdevice + 7). (If the + option is used, point-to-point + interfaces and non-broadcast interfaces are accepted.) If any + used interfaces are not up and running, they are first taken up + (and later taken down again on program exit). + + + Before network interfaces are selected, all network + hooks are run; see . + + This program is not meant to be run directly; it is really meant to run as a plugin of the Mandos plugin-runner @@ -197,44 +220,50 @@ assumed to separate the address from the port number. - This option is normally only useful for testing and - debugging. + Normally, Zeroconf would be used to locate Mandos servers, + in which case this option would only be used when testing + and debugging. + >NAME,NAME + NAME,NAME - Network interface that will be brought up and scanned for - Mandos servers to connect to. The default is the empty - string, which will automatically choose an appropriate - interface. + Comma separated list of network interfaces that will be + brought up and scanned for Mandos servers to connect to. + The default is the empty string, which will automatically + use all appropriate interfaces. - If the option is used, this - specifies the interface to use to connect to the address - given. + If the option is used, and + exactly one interface name is specified (except + none), this specifies + the interface to use to connect to the address given. Note that since this program will normally run in the initial RAM disk environment, the interface must be an interface which exists at that stage. Thus, the interface - can not be a pseudo-interface such as br0 - or tun0; such interfaces will not exist - until much later in the boot process, and can not be used - by this program. + can normally not be a pseudo-interface such as + br0 or tun0; such interfaces + will not exist until much later in the boot process, and + can not be used by this program, unless created by a + network hook — see . NAME can be the string - none; this will not use - any specific interface, and will not bring up an interface - on startup. This is not recommended, and only meant for - advanced users. + none; this will make + &COMMANDNAME; only bring up interfaces + specified before this string. This + is not recommended, and only meant for advanced users. @@ -282,7 +311,8 @@ 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. @@ -292,7 +322,7 @@ >SECONDS - After bringing the network interface up, the program waits + After bringing a network interface up, the program waits for the interface to arrive in a running state before proceeding. During this time, the kernel log level will be lowered to reduce clutter on the system @@ -393,8 +423,10 @@ plugin-runner 8mandos) is used to run both this program and others in in parallel, - one of which will prompt for passwords on - the system console. + one of which ( + password-prompt + 8mandos) will prompt for + passwords on the system console. @@ -421,6 +453,175 @@ + + NETWORK HOOKS + + If a network interface like a bridge or tunnel is required to + find a Mandos server, this requires the interface to be up and + running before &COMMANDNAME; starts looking + for Mandos servers. This can be accomplished by creating a + network hook program, and placing it in a special + directory. + + + Before the network is used (and again before program exit), any + runnable programs found in the network hook directory are run + with the argument start or + stop. This should bring up or + down, respectively, any network interface which + &COMMANDNAME; should use. + + + REQUIREMENTS + + A network hook must be an executable file, and its name must + consist entirely of upper and lower case letters, digits, + underscores, periods, and hyphens. + + + A network hook will receive one argument, which can be one of + the following: + + + + start + + + This should make the network hook create (if necessary) + and bring up a network interface. + + + + + stop + + + This should make the network hook take down a network + interface, and delete it if it did not exist previously. + + + + + files + + + This should make the network hook print, one + file per line, all the files needed for it to + run. (These files will be copied into the initial RAM + filesystem.) Typical use is for a network hook which is + a shell script to print its needed binaries. + + + It is not necessary to print any non-executable files + already in the network hook directory, these will be + copied implicitly if they otherwise satisfy the name + requirements. + + + + + modules + + + This should make the network hook print, on + separate lines, all the kernel modules needed + for it to run. (These modules will be copied into the + initial RAM filesystem.) For instance, a tunnel + interface needs the + tun module. + + + + + + The network hook will be provided with a number of environment + variables: + + + + MANDOSNETHOOKDIR + + + The network hook directory, specified to + &COMMANDNAME; by the + option. Note: this + should always be used by the + network hook to refer to itself or any files in the hook + directory it may require. + + + + + DEVICE + + + The network interfaces, as specified to + &COMMANDNAME; by the + option, combined to one + string and separated by commas. If this is set, and + does not contain the interface a hook will bring up, + there is no reason for a hook to continue. + + + + + MODE + + + This will be the same as the first argument; + i.e. start, + stop, + files, or + modules. + + + + + VERBOSITY + + + This will be the 1 if + the option is passed to + &COMMANDNAME;, otherwise + 0. + + + + + DELAY + + + This will be the same as the + option passed to &COMMANDNAME;. Is + only set if MODE is + start or + stop. + + + + + CONNECT + + + This will be the same as the + option passed to &COMMANDNAME;. Is + only set if is passed and + MODE is + start or + stop. + + + + + + A hook may not read from standard input, and should be + restrictive in printing to standard output or standard error + unless VERBOSITY is + 1. + + + + FILES @@ -438,6 +639,17 @@ + + /lib/mandos/network-hooks.d + + + Directory where network hooks are located. Change this + with the option. See + . + + + @@ -457,8 +669,8 @@ - Normal invocation needs no options, if the network interface - is eth0: + Normal invocation needs no options, if the network interfaces + can be automatically determined: &COMMANDNAME; @@ -466,8 +678,8 @@ - Search for Mandos servers (and connect to them) using another - interface: + Search for Mandos servers (and connect to them) using one + specific interface: @@ -537,8 +749,9 @@ It will also help if the checker program on the server is configured to request something from the client which can not be - spoofed by someone else on the network, unlike unencrypted - ICMP echo (ping) replies. + spoofed by someone else on the network, like SSH server key + fingerprints, and unlike unencrypted ICMP + echo (ping) replies. Note: This makes it completely insecure to @@ -637,7 +850,7 @@ This client uses IPv6 link-local addresses, which are immediately usable since a link-local addresses is - automatically assigned to a network interfaces when it + automatically assigned to a network interface when it is brought up. === modified file 'plugins.d/password-prompt.c' --- plugins.d/password-prompt.c 2011-10-09 12:32:13 +0000 +++ plugins.d/password-prompt.c 2014-03-29 02:38:15 +0000 @@ -2,8 +2,8 @@ /* * Password-prompt - Read a password from the terminal and print it * - * Copyright © 2008-2011 Teddy Hogeborn - * Copyright © 2008-2011 Björn Påhlsson + * Copyright © 2008-2014 Teddy Hogeborn + * Copyright © 2008-2014 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 @@ -73,6 +73,7 @@ const char plymouth_name[] = "plymouthd"; /* Function to use when printing errors */ +__attribute__((format (gnu_printf, 3, 4))) void error_plus(int status, int errnum, const char *formatstring, ...){ va_list ap; @@ -81,12 +82,11 @@ va_start(ap, formatstring); ret = vasprintf(&text, formatstring, ap); - if (ret == -1){ + if(ret == -1){ fprintf(stderr, "Mandos plugin %s: ", program_invocation_short_name); vfprintf(stderr, formatstring, ap); - fprintf(stderr, ": "); - fprintf(stderr, "%s\n", strerror(errnum)); + fprintf(stderr, ": %s\n", strerror(errnum)); error(status, errno, "vasprintf while printing error"); return; } @@ -109,17 +109,18 @@ from the terminal. Password-prompt will exit if it detects plymouth since plymouth performs the same functionality. */ + __attribute__((nonnull)) int is_plymouth(const struct dirent *proc_entry){ int ret; int cl_fd; { - uintmax_t maxvalue; + uintmax_t proc_id; char *tmp; errno = 0; - maxvalue = strtoumax(proc_entry->d_name, &tmp, 10); + proc_id = strtoumax(proc_entry->d_name, &tmp, 10); if(errno != 0 or *tmp != '\0' - or maxvalue != (uintmax_t)((pid_t)maxvalue)){ + or proc_id != (uintmax_t)((pid_t)proc_id)){ return 0; } } @@ -128,7 +129,7 @@ ret = asprintf(&cmdline_filename, "/proc/%s/cmdline", proc_entry->d_name); if(ret == -1){ - error(0, errno, "asprintf"); + error_plus(0, errno, "asprintf"); return 0; } @@ -137,7 +138,7 @@ free(cmdline_filename); if(cl_fd == -1){ if(errno != ENOENT){ - error(0, errno, "open"); + error_plus(0, errno, "open"); } return 0; } @@ -154,7 +155,7 @@ if(cmdline_len + blocksize + 1 > cmdline_allocated){ tmp = realloc(cmdline, cmdline_allocated + blocksize + 1); if(tmp == NULL){ - error(0, errno, "realloc"); + error_plus(0, errno, "realloc"); free(cmdline); close(cl_fd); return 0; @@ -167,7 +168,7 @@ sret = read(cl_fd, cmdline + cmdline_len, cmdline_allocated - cmdline_len); if(sret == -1){ - error(0, errno, "read"); + error_plus(0, errno, "read"); free(cmdline); close(cl_fd); return 0; @@ -176,7 +177,7 @@ } while(sret != 0); ret = close(cl_fd); if(ret == -1){ - error(0, errno, "close"); + error_plus(0, errno, "close"); free(cmdline); return 0; } @@ -211,8 +212,8 @@ struct dirent **direntries = NULL; int ret; ret = scandir("/proc", &direntries, is_plymouth, alphasort); - if (ret == -1){ - error(1, errno, "scandir"); + if(ret == -1){ + error_plus(1, errno, "scandir"); } free(direntries); return ret > 0; @@ -249,6 +250,7 @@ { .name = NULL } }; + __attribute__((nonnull(3))) error_t parse_opt (int key, char *arg, struct argp_state *state){ errno = 0; switch (key){ @@ -290,7 +292,7 @@ case ENOMEM: default: errno = ret; - error(0, errno, "argp_parse"); + error_plus(0, errno, "argp_parse"); return EX_OSERR; case EINVAL: return EX_USAGE; @@ -301,7 +303,7 @@ fprintf(stderr, "Starting %s\n", argv[0]); } - if (conflict_detection()){ + if(conflict_detection()){ if(debug){ fprintf(stderr, "Stopping %s because of conflict\n", argv[0]); } @@ -314,7 +316,7 @@ if(tcgetattr(STDIN_FILENO, &t_old) != 0){ int e = errno; - error(0, errno, "tcgetattr"); + error_plus(0, errno, "tcgetattr"); switch(e){ case EBADF: case ENOTTY: @@ -327,17 +329,17 @@ sigemptyset(&new_action.sa_mask); ret = sigaddset(&new_action.sa_mask, SIGINT); if(ret == -1){ - error(0, errno, "sigaddset"); + error_plus(0, errno, "sigaddset"); return EX_OSERR; } ret = sigaddset(&new_action.sa_mask, SIGHUP); if(ret == -1){ - error(0, errno, "sigaddset"); + error_plus(0, errno, "sigaddset"); return EX_OSERR; } ret = sigaddset(&new_action.sa_mask, SIGTERM); if(ret == -1){ - error(0, errno, "sigaddset"); + error_plus(0, errno, "sigaddset"); return EX_OSERR; } /* Need to check if the handler is SIG_IGN before handling: @@ -346,37 +348,37 @@ */ ret = sigaction(SIGINT, NULL, &old_action); if(ret == -1){ - error(0, errno, "sigaction"); + error_plus(0, errno, "sigaction"); return EX_OSERR; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGINT, &new_action, NULL); if(ret == -1){ - error(0, errno, "sigaction"); + error_plus(0, errno, "sigaction"); return EX_OSERR; } } ret = sigaction(SIGHUP, NULL, &old_action); if(ret == -1){ - error(0, errno, "sigaction"); + error_plus(0, errno, "sigaction"); return EX_OSERR; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGHUP, &new_action, NULL); if(ret == -1){ - error(0, errno, "sigaction"); + error_plus(0, errno, "sigaction"); return EX_OSERR; } } ret = sigaction(SIGTERM, NULL, &old_action); if(ret == -1){ - error(0, errno, "sigaction"); + error_plus(0, errno, "sigaction"); return EX_OSERR; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGTERM, &new_action, NULL); if(ret == -1){ - error(0, errno, "sigaction"); + error_plus(0, errno, "sigaction"); return EX_OSERR; } } @@ -390,7 +392,7 @@ t_new.c_lflag &= ~(tcflag_t)ECHO; if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_new) != 0){ int e = errno; - error(0, errno, "tcsetattr-echo"); + error_plus(0, errno, "tcsetattr-echo"); switch(e){ case EBADF: case ENOTTY: @@ -460,7 +462,7 @@ sret = write(STDOUT_FILENO, buffer + written, n - written); if(sret < 0){ int e = errno; - error(0, errno, "write"); + error_plus(0, errno, "write"); switch(e){ case EBADF: case EFAULT: @@ -482,7 +484,7 @@ sret = close(STDOUT_FILENO); if(sret == -1){ int e = errno; - error(0, errno, "close"); + error_plus(0, errno, "close"); switch(e){ case EBADF: status = EX_OSFILE; @@ -498,10 +500,11 @@ if(sret < 0){ int e = errno; if(errno != EINTR and not feof(stdin)){ - error(0, errno, "getline"); + error_plus(0, errno, "getline"); switch(e){ case EBADF: status = EX_UNAVAILABLE; + break; case EIO: case EINVAL: default: @@ -527,7 +530,7 @@ fprintf(stderr, "Restoring terminal attributes\n"); } if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_old) != 0){ - error(0, errno, "tcsetattr+echo"); + error_plus(0, errno, "tcsetattr+echo"); } if(quit_now){ @@ -535,7 +538,7 @@ old_action.sa_handler = SIG_DFL; ret = sigaction(signal_received, &old_action, NULL); if(ret == -1){ - error(0, errno, "sigaction"); + error_plus(0, errno, "sigaction"); } raise(signal_received); } === modified file 'plugins.d/password-prompt.xml' --- plugins.d/password-prompt.xml 2011-10-05 16:00:56 +0000 +++ plugins.d/password-prompt.xml 2011-12-31 23:05:34 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -34,6 +34,7 @@ 2008 2009 2011 + 2012 Teddy Hogeborn Björn Påhlsson === modified file 'plugins.d/plymouth.c' --- plugins.d/plymouth.c 2011-10-05 16:00:56 +0000 +++ plugins.d/plymouth.c 2014-03-29 02:38:15 +0000 @@ -2,8 +2,8 @@ /* * Plymouth - Read a password from Plymouth and output it * - * Copyright © 2010-2011 Teddy Hogeborn - * Copyright © 2010-2011 Björn Påhlsson + * Copyright © 2010-2014 Teddy Hogeborn + * Copyright © 2010-2014 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 @@ -75,6 +75,7 @@ } /* Function to use when printing errors */ +__attribute__((format (gnu_printf, 3, 4))) void error_plus(int status, int errnum, const char *formatstring, ...){ va_list ap; @@ -83,7 +84,7 @@ va_start(ap, formatstring); ret = vasprintf(&text, formatstring, ap); - if (ret == -1){ + if(ret == -1){ fprintf(stderr, "Mandos plugin %s: ", program_invocation_short_name); vfprintf(stderr, formatstring, ap); @@ -153,8 +154,9 @@ return true; } +__attribute__((nonnull (2, 3))) bool exec_and_wait(pid_t *pid_return, const char *path, - const char **argv, bool interruptable, + const char * const *argv, bool interruptable, bool daemonize){ int status; int ret; @@ -177,7 +179,7 @@ int i = 0; for (; argv[i]!=NULL; i++){ tmp = realloc(new_argv, sizeof(const char *) * ((size_t)i + 1)); - if (tmp == NULL){ + if(tmp == NULL){ error_plus(0, errno, "realloc"); free(new_argv); _exit(EX_OSERR); @@ -212,16 +214,17 @@ return false; } +__attribute__((nonnull)) int is_plymouth(const struct dirent *proc_entry){ int ret; { - uintmax_t maxvalue; + uintmax_t proc_id; char *tmp; errno = 0; - maxvalue = strtoumax(proc_entry->d_name, &tmp, 10); + proc_id = strtoumax(proc_entry->d_name, &tmp, 10); if(errno != 0 or *tmp != '\0' - or maxvalue != (uintmax_t)((pid_t)maxvalue)){ + or proc_id != (uintmax_t)((pid_t)proc_id)){ return 0; } } @@ -262,37 +265,37 @@ pid_t get_pid(void){ int ret; - uintmax_t maxvalue = 0; + uintmax_t proc_id = 0; FILE *pidfile = fopen(plymouth_pid, "r"); /* Try the new pid file location */ if(pidfile != NULL){ - ret = fscanf(pidfile, "%" SCNuMAX, &maxvalue); + ret = fscanf(pidfile, "%" SCNuMAX, &proc_id); if(ret != 1){ - maxvalue = 0; + proc_id = 0; } fclose(pidfile); } /* Try the old pid file location */ - if(maxvalue == 0){ + if(proc_id == 0){ pidfile = fopen(plymouth_pid, "r"); if(pidfile != NULL){ - ret = fscanf(pidfile, "%" SCNuMAX, &maxvalue); + ret = fscanf(pidfile, "%" SCNuMAX, &proc_id); if(ret != 1){ - maxvalue = 0; + proc_id = 0; } fclose(pidfile); } } /* Look for a plymouth process */ - if(maxvalue == 0){ + if(proc_id == 0){ struct dirent **direntries = NULL; ret = scandir("/proc", &direntries, is_plymouth, alphasort); - if (ret == -1){ + if(ret == -1){ error_plus(0, errno, "scandir"); } - if (ret > 0){ - ret = sscanf(direntries[0]->d_name, "%" SCNuMAX, &maxvalue); - if (ret < 0){ + if(ret > 0){ + ret = sscanf(direntries[0]->d_name, "%" SCNuMAX, &proc_id); + if(ret < 0){ error_plus(0, errno, "sscanf"); } } @@ -301,15 +304,15 @@ free(direntries); } pid_t pid; - pid = (pid_t)maxvalue; - if((uintmax_t)pid == maxvalue){ + pid = (pid_t)proc_id; + if((uintmax_t)pid == proc_id){ return pid; } return 0; } -const char **getargv(pid_t pid){ +const char * const * getargv(pid_t pid){ int cl_fd; char *cmdline_filename; ssize_t sret; @@ -376,7 +379,7 @@ return NULL; } argz_extract(cmdline, cmdline_len, argv); /* Create argv */ - return (const char **)argv; + return (const char * const *)argv; } int main(__attribute__((unused))int argc, @@ -457,7 +460,7 @@ } kill_and_wait(plymouth_command_pid); - const char **plymouthd_argv; + const char * const *plymouthd_argv; pid_t pid = get_pid(); if(pid == 0){ error_plus(0, 0, "plymouthd pid not found"); === modified file 'plugins.d/plymouth.xml' --- plugins.d/plymouth.xml 2011-10-05 16:00:56 +0000 +++ plugins.d/plymouth.xml 2011-12-31 23:05:34 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,6 +33,7 @@ 2010 2011 + 2012 Teddy Hogeborn Björn Påhlsson @@ -161,7 +162,7 @@ - /proc + /proc To find the running - + %common; ]> @@ -33,7 +33,7 @@ 2008 2009 - 2011 + 2012 Teddy Hogeborn Björn Påhlsson === modified file 'plugins.d/usplash.c' --- plugins.d/usplash.c 2011-10-05 16:00:56 +0000 +++ plugins.d/usplash.c 2014-03-29 02:38:15 +0000 @@ -2,8 +2,8 @@ /* * Usplash - Read a password from usplash and output it * - * Copyright © 2008-2011 Teddy Hogeborn - * Copyright © 2008-2011 Björn Påhlsson + * Copyright © 2008-2014 Teddy Hogeborn + * Copyright © 2008-2014 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 @@ -58,6 +58,7 @@ const char usplash_name[] = "/sbin/usplash"; /* Function to use when printing errors */ +__attribute__((format (gnu_printf, 3, 4))) void error_plus(int status, int errnum, const char *formatstring, ...){ va_list ap; @@ -66,7 +67,7 @@ va_start(ap, formatstring); ret = vasprintf(&text, formatstring, ap); - if (ret == -1){ + if(ret == -1){ fprintf(stderr, "Mandos plugin %s: ", program_invocation_short_name); vfprintf(stderr, formatstring, ap); === modified file 'plugins.d/usplash.xml' --- plugins.d/usplash.xml 2011-10-05 16:00:56 +0000 +++ plugins.d/usplash.xml 2011-12-31 23:05:34 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -34,6 +34,7 @@ 2008 2009 2011 + 2012 Teddy Hogeborn Björn Påhlsson @@ -178,7 +179,7 @@ - /proc + /proc To find the running