=== added file 'DBUS-API' --- DBUS-API 1970-01-01 00:00:00 +0000 +++ DBUS-API 2010-09-26 18:32:58 +0000 @@ -0,0 +1,172 @@ + -*- mode: org; coding: utf-8 -*- + + Mandos Server D-Bus Interface + +This file documents the D-Bus interface to the Mandos server. + +* Bus: System bus + Bus name: "se.bsnet.fukt.Mandos" + + +* Object Paths: + + | Path | Object | + |-----------------------+-------------------| + | "/" | The Mandos Server | + | "/clients/CLIENTNAME" | Mandos Client | + + +* Mandos Server Interface: + Interface name: "se.bsnet.fukt.Mandos" + +** Methods: +*** GetAllClients() → (ao: Clients) + Returns an array of all client D-Bus object paths + +*** GetAllClientsWithProperties() → (a{oa{sv}}: ClientProperties) + Returns an array of all clients and all their properties + +*** RemoveClient(o: ObjectPath) → nothing + Removes a client + +** Signals: +*** ClientAdded(o: ObjectPath) + A new client was added. + +*** ClientNotFound(s: Fingerprint, s: Address) + A client connected from Address using Fingerprint, but was + rejected because it was not found in the server. The fingerprint + is represented as a string of hexadecimal digits. The address is + an IPv4 or IPv6 address in its normal string format. + +*** ClientRemoved(o: ObjectPath, s: Name) + A client named Name on ObjectPath was removed. + + +* Mandos Client Interface: + Interface name: "se.bsnet.fukt.Mandos.Client" + +** Methods +*** Approve(b: Approve) → nothing + Approve or deny a connected client waiting for approval. If + denied, a client will not be sent its secret. + +*** CheckedOK() → nothing + Assert that this client has been checked and found to be alive. + This will restart the timeout before disabling this client. See + also the "LastCheckedOK" property. + +*** Disable() → nothing + Disable this client. See also the "Enabled" property. + +*** Enable() → nothing + Enable this client. See also the "Enabled" property. + +*** StartChecker() → nothing + Start a new checker for this client, if none is currently + running. See also the "CheckerRunning" property. + +*** StopChecker() → nothing + Abort a running checker process for this client, if any. See also + the "CheckerRunning" property. + +** Properties + + Note: Many of these properties directly correspond to a setting in + "clients.conf", in which case they are fully documented in + mandos-clients.conf(5). + + | Name | Type | Access | clients.conf | + |-------------------------+------+------------+---------------------| + | ApprovedByDefault | b | Read/Write | approved_by_default | + | ApprovalDelay (a) | t | Read/Write | approval_delay | + | ApprovalDuration (a) | t | Read/Write | approval_duration | + | ApprovalPending (b) | b | Read | N/A | + | Checker | s | Read/Write | checker | + | CheckerRunning (c) | b | Read/Write | N/A | + | Created (d) | s | Read | N/A | + | Enabled (e) | b | Read/Write | N/A | + | Fingerprint | s | Read | fingerprint | + | Host | s | Read/Write | host | + | Interval (a) | t | Read/Write | interval | + | LastApprovalRequest (f) | s | Read | N/A | + | LastCheckedOK (g) | s | Read/Write | N/A | + | LastEnabled (h) | s | Read | N/A | + | Name | s | Read | (Section name) | + | ObjectPath | o | Read | N/A | + | Secret (i) | ay | Write | secret (or secfile) | + | Timeout (a) | t | Read/Write | timeout | + + a) Represented as milliseconds. + + b) An approval is currently pending. + + c) Setting this property is equivalent to calling StartChecker() or + StopChecker(). + + d) The creation time of this client object, as a RFC 3339 string. + + e) Setting this property is equivalent to calling Enable() or + Disable(). + + f) The time of the last approval request, as a RFC 3339 string, or + an empty string if this has not happened. + + g) The last time a checker was successful, as a RFC 3339 string, or + an empty string if this has not happened. Setting this property + is equivalent to calling CheckedOK(), i.e. the current time is + set, regardless of the string sent. Please always use an empty + string when setting this property, to allow for possible future + expansion. + + h) The last time this client was enabled, as a RFC 3339 string, or + an empty string if this has not happened. + + i) A raw byte array, not hexadecimal digits. + +** Signals +*** CheckerCompleted(n: Exitcode, x: Waitstatus, s: Command) + A checker (Command) has completed. Exitcode is either the exit + code or -1 for abnormal exit. In any case, the full Waitstatus + (as from wait(2)) is also available. + +*** CheckerStarted(s: Command) + A checker command (Command) has just been started. + +*** GotSecret() + This client has been sent its secret. + +*** NeedApproval(t: Timeout, b: ApprovedByDefault) + This client will be approved or denied in exactly Timeout + 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 Teddy Hogeborn + Copyright © 2010 Björn Påhlsson + +** License: + + 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 + . + + +#+STARTUP: showall === modified file 'Makefile' --- Makefile 2009-10-25 19:16:56 +0000 +++ Makefile 2010-09-23 20:14:08 +0000 @@ -8,13 +8,15 @@ # -Wunreachable-code #DEBUG=-ggdb3 # For info about _FORTIFY_SOURCE, see -# +# +# and . FORTIFY=-D_FORTIFY_SOURCE=2 -fstack-protector-all -fPIC LINK_FORTIFY_LD=-z relro -z now LINK_FORTIFY= + +# If BROKEN_PIE is set, do not build with -pie ifndef BROKEN_PIE FORTIFY += -fPIE -LINK_FORTIFY_LD += -fPIE LINK_FORTIFY += -pie endif #COVERAGE=--coverage @@ -55,7 +57,7 @@ LDFLAGS=$(COVERAGE) $(LINK_FORTIFY) $(foreach flag,$(LINK_FORTIFY_LD),-Xlinker $(flag)) # Commands to format a DocBook document into a manual page -DOCBOOKTOMAN=cd $(dir $<); xsltproc --nonet --xinclude \ +DOCBOOKTOMAN=$(strip cd $(dir $<); xsltproc --nonet --xinclude \ --param man.charmap.use.subset 0 \ --param make.year.ranges 1 \ --param make.single.year.ranges 1 \ @@ -63,11 +65,11 @@ --param man.authors.section.enabled 0 \ /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl \ $(notdir $<); \ - $(MANPOST) $(notdir $@) + $(MANPOST) $(notdir $@)) # DocBook-to-man post-processing to fix a '\n' escape bug MANPOST=$(SED) --in-place --expression='s,\\\\en,\\en,g;s,\\n,\\en,g' -DOCBOOKTOHTML=xsltproc --nonet --xinclude \ +DOCBOOKTOHTML=$(strip xsltproc --nonet --xinclude \ --param make.year.ranges 1 \ --param make.single.year.ranges 1 \ --param man.output.quietly 1 \ @@ -75,20 +77,22 @@ --param citerefentry.link 1 \ --output $@ \ /usr/share/xml/docbook/stylesheet/nwalsh/xhtml/docbook.xsl \ - $<; $(HTMLPOST) $@ + $<; $(HTMLPOST) $@) # Fix citerefentry links HTMLPOST=$(SED) --in-place \ --expression='s/\(\)\([^<]*\)\(<\/span>(\)\([^)]*\)\()<\/span><\/a>\)/\1\3.\5\2\3\4\5\6/g' PLUGINS=plugins.d/password-prompt plugins.d/mandos-client \ - plugins.d/usplash plugins.d/splashy plugins.d/askpass-fifo + plugins.d/usplash plugins.d/splashy plugins.d/askpass-fifo \ + plugins.d/plymouth CPROGS=plugin-runner $(PLUGINS) -PROGS=mandos mandos-keygen mandos-ctl $(CPROGS) -DOCS=mandos.8 plugin-runner.8mandos mandos-keygen.8 \ +PROGS=mandos mandos-keygen mandos-ctl mandos-monitor $(CPROGS) +DOCS=mandos.8 mandos-keygen.8 mandos-monitor.8 mandos-ctl.8 \ + mandos.conf.5 mandos-clients.conf.5 plugin-runner.8mandos \ plugins.d/mandos-client.8mandos \ - plugins.d/password-prompt.8mandos mandos.conf.5 \ - plugins.d/usplash.8mandos plugins.d/splashy.8mandos \ - plugins.d/askpass-fifo.8mandos mandos-clients.conf.5 + plugins.d/password-prompt.8mandos plugins.d/usplash.8mandos \ + plugins.d/splashy.8mandos plugins.d/askpass-fifo.8mandos \ + plugins.d/plymouth.8mandos htmldocs=$(addsuffix .xhtml,$(DOCS)) @@ -129,6 +133,20 @@ legalnotice.xml $(DOCBOOKTOHTML) +mandos-monitor.8: mandos-monitor.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos-monitor.8.xhtml: mandos-monitor.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOHTML) + +mandos-ctl.8: mandos-ctl.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOMAN) +mandos-ctl.8.xhtml: mandos-ctl.xml common.ent overview.xml \ + legalnotice.xml + $(DOCBOOKTOHTML) + mandos.conf.5: mandos.conf.xml common.ent mandos-options.xml \ legalnotice.xml $(DOCBOOKTOMAN) @@ -156,39 +174,44 @@ # Update all these files with version number $(version) common.ent: Makefile - $(SED) --in-place \ + $(strip $(SED) --in-place \ --expression='s/^\($$/\1$(version)">/' \ - $@ + $@) mandos: Makefile - $(SED) --in-place \ + $(strip $(SED) --in-place \ --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ - $@ + $@) mandos-keygen: Makefile - $(SED) --in-place \ + $(strip $(SED) --in-place \ --expression='s/^\(VERSION="\)[^"]*"$$/\1$(version)"/' \ - $@ + $@) mandos-ctl: Makefile - $(SED) --in-place \ - --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ - $@ + $(strip $(SED) --in-place \ + --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ + $@) + +mandos-monitor: Makefile + $(strip $(SED) --in-place \ + --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \ + $@) mandos.lsm: Makefile - $(SED) --in-place \ + $(strip $(SED) --in-place \ --expression='s/^\(Version:\).*/\1\t$(version)/' \ - $@ - $(SED) --in-place \ + $@) + $(strip $(SED) --in-place \ --expression='s/^\(Entered-date:\).*/\1\t$(shell date --rfc-3339=date --reference=Makefile)/' \ - $@ - $(SED) --in-place \ + $@) + $(strip $(SED) --in-place \ --expression='s/\(mandos_\)[0-9.]\+\(\.orig\.tar\.gz\)/\1$(version)\2/' \ - $@ + $@) -plugins.d/mandos-client: plugins.d/mandos-client.o - $(LINK.o) $(GNUTLS_LIBS) $(AVAHI_LIBS) $(GPGME_LIBS) \ - $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@ +plugins.d/mandos-client: plugins.d/mandos-client.c + $(LINK.c) $(GNUTLS_LIBS) $(AVAHI_LIBS) $(GPGME_LIBS) $(strip\ + ) $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@ .PHONY : all doc html clean distclean run-client run-server install \ install-server install-client uninstall uninstall-server \ @@ -207,6 +230,17 @@ # Run the client with a local config and key run-client: all keydir/seckey.txt keydir/pubkey.txt + @echo "###################################################################" + @echo "# The following error messages are harmless and can be safely #" + @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 askpass-fifo: mkfifo: Permission denied #" + @echo "# From mandos-client: setuid: Operation not permitted #" + @echo "# seteuid: Operation not permitted #" + @echo "# klogctl: Operation not permitted #" + @echo "###################################################################" ./plugin-runner --plugin-dir=plugins.d \ --config-file=plugin-runner.conf \ --options-for=mandos-client:--seckey=keydir/seckey.txt,--pubkey=keydir/pubkey.txt \ @@ -219,6 +253,12 @@ # 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) # Used by run-server @@ -241,10 +281,16 @@ install-server: doc install --directory $(CONFDIR) install --mode=u=rwx,go=rx mandos $(PREFIX)/sbin/mandos + install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ + mandos-ctl + install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \ + mandos-monitor install --mode=u=rw,go=r --target-directory=$(CONFDIR) \ mandos.conf install --mode=u=rw --target-directory=$(CONFDIR) \ clients.conf + install --mode=u=rw,go=r dbus-mandos.conf \ + $(DESTDIR)/etc/dbus-1/system.d/mandos.conf install --mode=u=rwx,go=rx init.d-mandos \ $(DESTDIR)/etc/init.d/mandos install --mode=u=rw,go=r default-mandos \ @@ -254,6 +300,10 @@ fi gzip --best --to-stdout mandos.8 \ > $(MANDIR)/man8/mandos.8.gz + gzip --best --to-stdout mandos-monitor.8 \ + > $(MANDIR)/man8/mandos-monitor.8.gz + gzip --best --to-stdout mandos-ctl.8 \ + > $(MANDIR)/man8/mandos-ctl.8.gz gzip --best --to-stdout mandos.conf.5 \ > $(MANDIR)/man5/mandos.conf.5.gz gzip --best --to-stdout mandos-clients.conf.5 \ @@ -286,6 +336,9 @@ install --mode=u=rwxs,go=rx \ --target-directory=$(PREFIX)/lib/mandos/plugins.d \ plugins.d/askpass-fifo + install --mode=u=rwxs,go=rx \ + --target-directory=$(PREFIX)/lib/mandos/plugins.d \ + plugins.d/plymouth install initramfs-tools-hook \ $(INITRAMFSTOOLS)/hooks/mandos install --mode=u=rw,go=r initramfs-tools-hook-conf \ @@ -297,16 +350,18 @@ > $(MANDIR)/man8/mandos-keygen.8.gz gzip --best --to-stdout plugin-runner.8mandos \ > $(MANDIR)/man8/plugin-runner.8mandos.gz + gzip --best --to-stdout plugins.d/mandos-client.8mandos \ + > $(MANDIR)/man8/mandos-client.8mandos.gz gzip --best --to-stdout plugins.d/password-prompt.8mandos \ > $(MANDIR)/man8/password-prompt.8mandos.gz - gzip --best --to-stdout plugins.d/mandos-client.8mandos \ - > $(MANDIR)/man8/mandos-client.8mandos.gz gzip --best --to-stdout plugins.d/usplash.8mandos \ > $(MANDIR)/man8/usplash.8mandos.gz gzip --best --to-stdout plugins.d/splashy.8mandos \ > $(MANDIR)/man8/splashy.8mandos.gz gzip --best --to-stdout plugins.d/askpass-fifo.8mandos \ > $(MANDIR)/man8/askpass-fifo.8mandos.gz + gzip --best --to-stdout plugins.d/plymouth.8mandos \ + > $(MANDIR)/man8/plymouth.8mandos.gz install-client: install-client-nokey # Post-installation stuff @@ -318,7 +373,11 @@ uninstall-server: -rm --force $(PREFIX)/sbin/mandos \ + $(PREFIX)/sbin/mandos-ctl \ + $(PREFIX)/sbin/mandos-monitor \ $(MANDIR)/man8/mandos.8.gz \ + $(MANDIR)/man8/mandos-monitor.8.gz \ + $(MANDIR)/man8/mandos-ctl.8.gz \ $(MANDIR)/man5/mandos.conf.5.gz \ $(MANDIR)/man5/mandos-clients.conf.5.gz update-rc.d -f mandos remove @@ -336,16 +395,18 @@ $(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 \ $(INITRAMFSTOOLS)/hooks/mandos \ $(INITRAMFSTOOLS)/conf-hooks.d/mandos \ $(INITRAMFSTOOLS)/scripts/init-premount/mandos \ + $(MANDIR)/man8/mandos-keygen.8.gz \ $(MANDIR)/man8/plugin-runner.8mandos.gz \ - $(MANDIR)/man8/mandos-keygen.8.gz \ + $(MANDIR)/man8/mandos-client.8mandos.gz $(MANDIR)/man8/password-prompt.8mandos.gz \ $(MANDIR)/man8/usplash.8mandos.gz \ $(MANDIR)/man8/splashy.8mandos.gz \ $(MANDIR)/man8/askpass-fifo.8mandos.gz \ - $(MANDIR)/man8/mandos-client.8mandos.gz + $(MANDIR)/man8/plymouth.8mandos.gz \ -rmdir $(PREFIX)/lib/mandos/plugins.d $(CONFDIR)/plugins.d \ $(PREFIX)/lib/mandos $(CONFDIR) $(KEYDIR) update-initramfs -k all -u @@ -354,6 +415,7 @@ purge-server: uninstall-server -rm --force $(CONFDIR)/mandos.conf $(CONFDIR)/clients.conf \ + $(DESTDIR)/etc/dbus-1/system.d/mandos.conf $(DESTDIR)/etc/default/mandos \ $(DESTDIR)/etc/init.d/mandos \ $(DESTDIR)/var/run/mandos.pid === modified file 'README' --- README 2009-02-23 11:52:42 +0000 +++ README 2010-09-26 18:32:58 +0000 @@ -99,7 +99,7 @@ Multiple Mandos servers can coexist on a network without any trouble. They do not clash, and clients will try all available servers. This means that if just one reboots then the other can - bring it back up, but if both reboots at the same time they will + bring it back up, but if both reboot at the same time they will stay down until someone types in the password on one of them. ** Faking ping replies? @@ -144,7 +144,9 @@ plugins to run, all competing to be the first to find a password and provide it to the plugin runner. - Three additional plugins are provided: + Four additional plugins are provided: + * plymouth(8mandos) + This prompts for a password when using plymouth(8). * usplash(8mandos) This prompts for a password when using usplash(8). * splashy(8mandos) @@ -160,8 +162,8 @@ * Copyright - Copyright © 2008,2009 Teddy Hogeborn - Copyright © 2008,2009 Björn Påhlsson + Copyright © 2008-2010 Teddy Hogeborn + Copyright © 2008-2010 Björn Påhlsson ** License: === modified file 'TODO' --- TODO 2009-09-17 11:47:22 +0000 +++ TODO 2010-09-26 18:32:58 +0000 @@ -1,71 +1,109 @@ -*- org -*- +* Use _attribute_((nonnull)) wherever possible. + * mandos-client ** TODO [#B] use scandir(3) instead of readdir(3) -** TODO [#B] Prefix all debug output with argv[0] +** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name ** TODO [#B] Retry a server which has a non-definite reply: *** A closed connection during the TLS handshake *** A TCP timeout ** TODO [#B] Use capabilities instead of seteuid(). +** TODO [#A] Retry --connect forever * splashy ** TODO [#B] use scandir(3) instead of readdir(3) -** TODO [#B] Prefix all debug output with "Mandos plugin " + argv[0] +** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name * usplash +** TODO [#A] Make it work again ** TODO [#B] use scandir(3) instead of readdir(3) -** TODO [#B] Prefix all debug output with "Mandos plugin " + argv[0] +** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name +** TODO Use [[info:libc:Argz%20Functions][argz_extract]] * askpass-fifo -** TODO [#B] Prefix all debug output with "Mandos plugin " + argv[0] +** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name ** TODO [#B] Drop privileges after opening FIFO. * password-prompt -** TODO [#B] Prefix all debug output with "Mandos plugin " + argv[0] +** TODO [#B] Prefix all debug output with "Mandos plugin " + program_invocation_short_name +** TODO [#B] lock stdin (with flock()?) + +* TODO [#B] passdev * plugin-runner ** TODO [#B] use scandir(3) instead of readdir(3) ** 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] Log level :BUGS: -** TODO /etc/mandos/clients.d/*.conf - Watch this directory and add/remove/update clients? -** TODO config for TXT record -** TODO [#B] Run-time communication with server :BUGS: - Probably using D-Bus -*** Client class -*** Main server - + SetLogLevel - syslogger.setLevel(logging.WARNING) - + [[http://log.ometer.com/2007-05.html][Best D-Bus practices]] -** TODO Implement --foreground :BUGS: +** 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: +** TODO Date+time on console log messages :BUGS: Is this the default? -** TODO DBusServiceObjectUsingSuper -** Global enable/disable flag -** By-client countdown on secrets given -** Fix problem with fsck taking a really long time +** TODO [#C] DBusServiceObjectUsingSuper +** TODO [#B] Global enable/disable flag +** TODO [#B] By-client countdown on secrets given +** TODO [#B] Fix problem with fsck taking a really long time Whenever a client successfully gets a secret it could get a one-time timeout boost to allow for an fsck-incurred delay +** TODO [#A] Delay before client receives key + This would give an operator opportunity to cancel the request if + desired. +** TODO [#A] Client manual approval mode + A client needs manual approval on the server before it gets the + secret +** TODO [#B] Support RFC 3339 time duration syntax +** More D-Bus methods +*** NeedsApproval(50, True) -> timeout, default approve + Default approval is configurable, but True by default + + Approve(True) -> approve sending saved + + Approve(False) -> Close client connection immediately +*** 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 +** 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 * mandos.xml -** [[file:mandos.xml::XXX][Document D-Bus interface]] - -* Provide and install /etc/dbus-1/system.d/mandos.conf +** Add mandos contact info in manual pages * 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 mandos-dispatch + Listens for specified D-Bus signals and spawns shell commands with + arguments. * mandos-monitor -** D-Bus mail loop w/ signal receiver -** Snack/Newt client data displayer -*** Client Widgets +** TODO help should be toggable +** 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 * mandos-keygen ** TODO Loop until passwords match when run interactively @@ -75,7 +113,9 @@ For testing decryption before rebooting. * Makefile -** Implement DEB_BUILD_OPTIONS +** TODO Add "--Xlinker --as-needed" + http://udrepper.livejournal.com/19395.html +** TODO [#C] Implement DEB_BUILD_OPTIONS http://www.debian.org/doc/debian-policy/ch-source.html#s-debianrules-options * Package @@ -86,5 +126,9 @@ ** TODO [#C] /etc/bash_completion.d/mandos From XML sources directly? +* Side Stuff +** TODO Locate which packet move the other bin/sh when busy box is deactivated +** TODO contact owner of packet, and ask them to have that shell static in position regardless of busybox + #+STARTUP: showall === modified file 'clients.conf' --- clients.conf 2009-01-08 03:54:06 +0000 +++ clients.conf 2010-09-12 03:00:40 +0000 @@ -2,20 +2,29 @@ # values, so uncomment and change them if you want different ones. [DEFAULT] -# How long until a client is considered invalid - that is, ineligible -# to get the data this server holds. +# How long until a client is disabled and not be allowed to get the +# data this server holds. ;timeout = 1h # 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 marked -# invalid, and any running checker killed. +# above "timeout" occurs, at which time the client will be disabled, +# and any running checker killed. ;interval = 5m # What command to run as "the checker". ;checker = fping -q -- %%(host)s +# Whether to approve a client by default after the approval delay. +;approved_by_default = True + +# How long to wait for approval. +;approval_delay = 0s + +# How long one approval will last. +;approval_duration = 1s + ;#### ;# Example client @@ -62,4 +71,9 @@ ; ;# Parameters from the [DEFAULT] section can be overridden per client. ;interval = 5m +; +;# This client requires manual approval before it receives its secret. +;approved_by_default = False +;# Require approval within 30 seconds. +;approval_delay = 30s ;#### === added file 'dbus-mandos.conf' --- dbus-mandos.conf 1970-01-01 00:00:00 +0000 +++ dbus-mandos.conf 2009-11-09 07:35:16 +0000 @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + === modified file 'debian/control' --- debian/control 2009-09-17 11:47:22 +0000 +++ debian/control 2010-09-26 18:32:58 +0000 @@ -15,8 +15,8 @@ Package: mandos Architecture: all Depends: ${misc:Depends}, python (>=2.5), python-gnutls, python-dbus, - python-avahi, python-gobject, avahi-daemon, gnupg (< 2), - adduser + python-avahi, python-gobject, avahi-daemon, adduser, + python-urwid, python (>=2.6) | python-multiprocessing Recommends: fping Description: a server giving encrypted passwords to Mandos clients This is the server part of the Mandos system, which allows @@ -35,7 +35,8 @@ Package: mandos-client Architecture: any -Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, cryptsetup +Depends: ${shlibs:Depends}, ${misc:Depends}, adduser, cryptsetup, + gnupg (<< 2) Enhances: cryptsetup Description: do unattended reboots with an encrypted root file system This is the client part of the Mandos system, which allows === modified file 'debian/copyright' --- debian/copyright 2009-01-04 21:54:55 +0000 +++ debian/copyright 2010-09-26 18:32:58 +0000 @@ -5,8 +5,8 @@ Upstream-Source: Files: * -Copyright: Copyright © 2008,2009 Teddy Hogeborn -Copyright: Copyright © 2008,2009 Björn Påhlsson +Copyright: Copyright © 2008-2010 Teddy Hogeborn +Copyright: Copyright © 2008-2010 Björn Påhlsson License: GPL-3+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as === modified file 'debian/mandos-client.README.Debian' --- debian/mandos-client.README.Debian 2009-09-08 06:28:20 +0000 +++ debian/mandos-client.README.Debian 2010-09-27 17:53:53 +0000 @@ -1,9 +1,11 @@ * Choose the Client Network Interface - You MUST make sure that the correct network interface is specified - in the DEVICE setting in the "/etc/initramfs-tools/initramfs.conf" - file. *If* this is changed, it will be necessary to update the - initrd image by running the command + Please make sure that the correct network interface is specified in + the DEVICE setting in the "/etc/initramfs-tools/initramfs.conf" + file. If the setting is empty, the interface will be autodetected + at boot time, which may not be correct. *If* the DEVICE setting is + changed, it will be necessary to update the initrd image by running + the command update-initramfs -k all -u @@ -74,9 +76,6 @@ instead of using ZeroConf. The syntax for doing this is "mandos=connect::". - Warning: this will cause the client to make exactly one attempt at - connecting, and then fail if it does not succeed. - For very advanced users, it it possible to specify simply "mandos=connect" on the kernel command line to make the system only set up the network (using the data in the "ip=" option) and not pass @@ -84,4 +83,4 @@ work, "--options-for=mandos-client:--connect=
:" needs to be manually added to the file "/etc/mandos/plugin-runner.conf". - -- Teddy Hogeborn , Tue, 8 Sep 2009 08:25:58 +0200 + -- Teddy Hogeborn , Mon, 27 Sep 2010 19:53:21 +0200 === modified file 'debian/mandos.dirs' --- debian/mandos.dirs 2008-09-17 00:34:09 +0000 +++ debian/mandos.dirs 2010-09-15 17:33:14 +0000 @@ -2,4 +2,5 @@ usr/share/man/man8 etc/init.d etc/default +etc/dbus-1/system.d usr/sbin === modified file 'debian/mandos.docs' --- debian/mandos.docs 2008-10-18 11:17:22 +0000 +++ debian/mandos.docs 2010-09-12 03:00:40 +0000 @@ -1,3 +1,4 @@ NEWS README TODO +DBUS-API === modified file 'debian/rules' --- debian/rules 2009-10-25 19:06:56 +0000 +++ debian/rules 2010-09-09 18:16:14 +0000 @@ -15,13 +15,18 @@ # This has to be exported to make some magic below work. export DH_OPTIONS -ifneq (,$(findstring :$(DEB_HOST_ARCH):,:mips:mipsel:)) - BROKEN_PIE := yes - export BROKEN_PIE -endif -ifneq (,$(findstring :$(DEB_BUILD_ARCH):,:mips:mipsel:)) - BROKEN_PIE := yes - export BROKEN_PIE +# -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 === modified file 'debian/watch' --- debian/watch 2009-01-15 02:52:02 +0000 +++ debian/watch 2010-09-15 17:17:46 +0000 @@ -1,2 +1,2 @@ version=3 -ftp://ftp.fukt.bsnet.se/pub/mandos/mandos[-_]([^\s]+?)(?:\.orig)?\.tar\.(?:gz|bz2|7z) +ftp://ftp.fukt.bsnet.se/pub/mandos/mandos[-_]([^\s]+?)(?:\.orig)?\.tar\.(?:gz|bz2|7z|xz) === modified file 'mandos' --- mandos 2009-10-25 19:16:56 +0000 +++ mandos 2010-09-27 17:39:03 +0000 @@ -11,8 +11,8 @@ # "AvahiService" class, and some lines in "main". # # Everything else is -# Copyright © 2008,2009 Teddy Hogeborn -# Copyright © 2008,2009 Björn Påhlsson +# Copyright © 2008-2010 Teddy Hogeborn +# Copyright © 2008-2010 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 @@ -55,10 +55,12 @@ import logging import logging.handlers import pwd -from contextlib import closing +import contextlib import struct import fcntl import functools +import cPickle as pickle +import multiprocessing import dbus import dbus.service @@ -67,6 +69,8 @@ from dbus.mainloop.glib import DBusGMainLoop import ctypes import ctypes.util +import xml.dom.minidom +import inspect try: SO_BINDTODEVICE = socket.SO_BINDTODEVICE @@ -79,6 +83,7 @@ version = "1.0.14" +#logger = logging.getLogger(u'mandos') logger = logging.Logger(u'mandos') syslogger = (logging.handlers.SysLogHandler (facility = logging.handlers.SysLogHandler.LOG_DAEMON, @@ -152,15 +157,20 @@ u" after %i retries, exiting.", self.rename_count) raise AvahiServiceError(u"Too many renames") - self.name = self.server.GetAlternativeServiceName(self.name) + self.name = unicode(self.server.GetAlternativeServiceName(self.name)) logger.info(u"Changing Zeroconf service name to %r ...", - unicode(self.name)) + self.name) syslogger.setFormatter(logging.Formatter (u'Mandos (%s) [%%(process)d]:' u' %%(levelname)s: %%(message)s' % self.name)) self.remove() - self.add() + try: + self.add() + except dbus.exceptions.DBusException, error: + logger.critical(u"DBusException: %s", error) + self.cleanup() + os._exit(1) self.rename_count += 1 def remove(self): """Derived from the Avahi example code""" @@ -174,7 +184,8 @@ self.server.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP) self.group.connect_to_signal('StateChanged', - self.entry_group_state_changed) + self + .entry_group_state_changed) logger.debug(u"Adding Zeroconf service '%s' of type '%s' ...", self.name, self.type) self.group.AddService( @@ -188,7 +199,7 @@ self.group.Commit() def entry_group_state_changed(self, state, error): """Derived from the Avahi example code""" - logger.debug(u"Avahi state change: %i", state) + logger.debug(u"Avahi entry group state change: %i", state) if state == avahi.ENTRY_GROUP_ESTABLISHED: logger.debug(u"Zeroconf service established.") @@ -207,6 +218,7 @@ self.group = None def server_state_changed(self, state): """Derived from the Avahi example code""" + logger.debug(u"Avahi server state change: %i", state) if state == avahi.SERVER_COLLISION: logger.error(u"Zeroconf server name collision") self.remove() @@ -228,47 +240,60 @@ """A representation of a client host served by this server. Attributes: - name: string; from the config file, used in log messages and - D-Bus identifiers - fingerprint: string (40 or 32 hexadecimal digits); used to - uniquely identify the client - secret: bytestring; sent verbatim (over TLS) to client - host: string; available for use by the checker command - created: datetime.datetime(); (UTC) object creation - last_enabled: datetime.datetime(); (UTC) - enabled: bool() - last_checked_ok: datetime.datetime(); (UTC) or None - timeout: datetime.timedelta(); How long from last_checked_ok - until this client is invalid - interval: datetime.timedelta(); How often to start a new checker - disable_hook: If set, called by disable() as disable_hook(self) + _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 to see if the client lives. 'None' if no process is running. - checker_initiator_tag: a gobject event source tag, or None - disable_initiator_tag: - '' - - checker_callback_tag: - '' - - checker_command: string; External command which is run to check if - client lives. %() expansions are done at + checker_callback_tag: a gobject event source tag, or None + checker_command: string; External command which is run to check + if client lives. %() expansions are done at runtime with vars(self) as dict, so that for 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 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 + uniquely identify the client + host: string; available for use by the checker command + 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) + 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 + runtime_expansions: Allowed attributes for runtime expansion. """ + runtime_expansions = (u"approval_delay", u"approval_duration", + u"created", u"enabled", u"fingerprint", + u"host", u"interval", u"last_checked_ok", + u"last_enabled", u"name", u"timeout") + @staticmethod - def _datetime_to_milliseconds(dt): - "Convert a datetime.datetime() to milliseconds" - return ((dt.days * 24 * 60 * 60 * 1000) - + (dt.seconds * 1000) - + (dt.microseconds // 1000)) + def _timedelta_to_milliseconds(td): + "Convert a datetime.timedelta() to milliseconds" + return ((td.days * 24 * 60 * 60 * 1000) + + (td.seconds * 1000) + + (td.microseconds // 1000)) def timeout_milliseconds(self): "Return the 'timeout' attribute in milliseconds" - return self._datetime_to_milliseconds(self.timeout) + return self._timedelta_to_milliseconds(self.timeout) def interval_milliseconds(self): "Return the 'interval' attribute in milliseconds" - return self._datetime_to_milliseconds(self.interval) + return self._timedelta_to_milliseconds(self.interval) + + def approval_delay_milliseconds(self): + return self._timedelta_to_milliseconds(self.approval_delay) def __init__(self, name = None, disable_hook=None, config=None): """Note: the 'checker' key in 'config' sets the @@ -287,9 +312,9 @@ if u"secret" in config: self.secret = config[u"secret"].decode(u"base64") elif u"secfile" in config: - with closing(open(os.path.expanduser - (os.path.expandvars - (config[u"secfile"])))) as secfile: + with open(os.path.expanduser(os.path.expandvars + (config[u"secfile"])), + "rb") as secfile: self.secret = secfile.read() else: raise TypeError(u"No secret or secfile for client %s" @@ -297,6 +322,7 @@ self.host = config.get(u"host", u"") 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[u"timeout"]) @@ -309,31 +335,49 @@ self.checker_command = config[u"checker"] self.current_checker_command = None self.last_connect = None + self._approved = None + self.approved_by_default = config.get(u"approved_by_default", + True) + self.approvals_pending = 0 + self.approval_delay = string_to_delta( + config[u"approval_delay"]) + self.approval_duration = string_to_delta( + config[u"approval_duration"]) + self.changedstate = multiprocessing_manager.Condition(multiprocessing_manager.Lock()) + def send_changedstate(self): + self.changedstate.acquire() + self.changedstate.notify_all() + self.changedstate.release() + def enable(self): """Start this client's checker and timeout hooks""" if getattr(self, u"enabled", False): # Already enabled return + self.send_changedstate() self.last_enabled = datetime.datetime.utcnow() # 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)) - # Also start a new checker *right now*. - self.start_checker() # Schedule a disable() when 'timeout' has passed self.disable_initiator_tag = (gobject.timeout_add (self.timeout_milliseconds(), self.disable)) self.enabled = True + # Also start a new checker *right now*. + self.start_checker() - def disable(self): + def disable(self, quiet=True): """Disable this client.""" if not getattr(self, "enabled", False): return False - logger.info(u"Disabling client %s", self.name) + if not quiet: + self.send_changedstate() + if not quiet: + logger.info(u"Disabling client %s", self.name) if getattr(self, u"disable_initiator_tag", False): gobject.source_remove(self.disable_initiator_tag) self.disable_initiator_tag = None @@ -380,6 +424,9 @@ (self.timeout_milliseconds(), self.disable)) + def need_approval(self): + self.last_approval_request = datetime.datetime.utcnow() + def start_checker(self): """Start a new checker subprocess if one is not running. @@ -391,12 +438,17 @@ # 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 declared invalid, which - # is as it should be. + # than 'timeout' for the client to be disabled, which is as it + # should be. # If a checker exists, make sure it is not a zombie - if self.checker is not None: + try: pid, status = os.waitpid(self.checker.pid, os.WNOHANG) + except (AttributeError, OSError), error: + if (isinstance(error, OSError) + and error.errno != errno.ECHILD): + raise error + else: if pid: logger.warning(u"Checker was a zombie") gobject.source_remove(self.checker_callback_tag) @@ -409,12 +461,14 @@ command = self.checker_command % self.host except TypeError: # Escape attributes for the shell - escaped_attrs = dict((key, - re.escape(unicode(str(val), - errors= - u'replace'))) - for key, val in - vars(self).iteritems()) + escaped_attrs = dict( + (attr, + re.escape(unicode(str(getattr(self, attr, u"")), + errors= + u'replace'))) + for attr in + self.runtime_expansions) + try: command = self.checker_command % escaped_attrs except TypeError, error: @@ -458,44 +512,245 @@ logger.debug(u"Stopping checker for %(name)s", vars(self)) try: os.kill(self.checker.pid, signal.SIGTERM) - #os.sleep(0.5) + #time.sleep(0.5) #if self.checker.poll() is None: # os.kill(self.checker.pid, signal.SIGKILL) except OSError, error: if error.errno != errno.ESRCH: # No such process raise self.checker = None - - def still_valid(self): - """Has the timeout not yet passed for this client?""" - if not getattr(self, u"enabled", False): - return False - now = datetime.datetime.utcnow() - if self.last_checked_ok is None: - return now < (self.created + self.timeout) - else: - return now < (self.last_checked_ok + self.timeout) - - -class ClientDBus(Client, dbus.service.Object): + +def dbus_service_property(dbus_interface, signature=u"v", + access=u"readwrite", byte_arrays=False): + """Decorators for marking methods of a DBusObjectWithProperties to + become properties on the D-Bus. + + The decorated method will be called with no arguments by "Get" + and with one argument by "Set". + + The parameters, where they are supported, are the same as + dbus.service.method, except there is only "signature", since the + type from Get() and the type sent to Set() is the same. + """ + # Encoding deeply encoded byte arrays is not supported yet by the + # "Set" method, so we fail early here: + if byte_arrays and signature != u"ay": + raise ValueError(u"Byte arrays not supported for non-'ay'" + u" signature %r" % signature) + def decorator(func): + func._dbus_is_property = True + func._dbus_interface = dbus_interface + func._dbus_signature = signature + func._dbus_access = access + func._dbus_name = func.__name__ + if func._dbus_name.endswith(u"_dbus_property"): + func._dbus_name = func._dbus_name[:-14] + func._dbus_get_args_options = {u'byte_arrays': byte_arrays } + 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)) + + +class DBusPropertyAccessException(DBusPropertyException): + """A property's access permissions disallows an operation. + """ + pass + + +class DBusPropertyNotFound(DBusPropertyException): + """An attempt was made to access a non-existing property. + """ + pass + + +class DBusObjectWithProperties(dbus.service.Object): + """A D-Bus object with properties. + + Classes inheriting from this can use the dbus_service_property + decorator to expose methods as D-Bus properties. It exposes the + standard Get(), Set(), and GetAll() methods on the D-Bus. + """ + + @staticmethod + def _is_dbus_property(obj): + return getattr(obj, u"_dbus_is_property", False) + + def _get_all_dbus_properties(self): + """Returns a generator of (name, attribute) pairs + """ + return ((prop._dbus_name, prop) + for name, prop in + inspect.getmembers(self, self._is_dbus_property)) + + 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 name in (property_name, + property_name + u"_dbus_property"): + prop = getattr(self, name, None) + if (prop is None + or not self._is_dbus_property(prop) + or prop._dbus_name != property_name + or (interface_name and prop._dbus_interface + and interface_name != prop._dbus_interface)): + continue + return prop + # No such property + raise DBusPropertyNotFound(self.dbus_object_path + u":" + + interface_name + u"." + + property_name) + + @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ss", + out_signature=u"v") + def Get(self, interface_name, property_name): + """Standard D-Bus property Get() method, see D-Bus standard. + """ + prop = self._get_dbus_property(interface_name, property_name) + if prop._dbus_access == u"write": + raise DBusPropertyAccessException(property_name) + value = prop() + if not hasattr(value, u"variant_level"): + return value + return type(value)(value, variant_level=value.variant_level+1) + + @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"ssv") + def Set(self, interface_name, property_name, value): + """Standard D-Bus property Set() method, see D-Bus standard. + """ + prop = self._get_dbus_property(interface_name, property_name) + if prop._dbus_access == u"read": + raise DBusPropertyAccessException(property_name) + if prop._dbus_get_args_options[u"byte_arrays"]: + # The byte_arrays option is not supported yet on + # signatures other than "ay". + if prop._dbus_signature != u"ay": + raise ValueError + value = dbus.ByteArray(''.join(unichr(byte) + for byte in value)) + prop(value) + + @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature=u"s", + out_signature=u"a{sv}") + def GetAll(self, interface_name): + """Standard D-Bus property GetAll() method, see D-Bus + standard. + + Note: Will not include properties with access="write". + """ + all = {} + for name, prop in self._get_all_dbus_properties(): + if (interface_name + and interface_name != prop._dbus_interface): + # Interface non-empty but did not match + continue + # Ignore write-only properties + if prop._dbus_access == u"write": + continue + value = prop() + if not hasattr(value, u"variant_level"): + all[name] = value + continue + all[name] = type(value)(value, variant_level= + value.variant_level+1) + return dbus.Dictionary(all, signature=u"sv") + + @dbus.service.method(dbus.INTROSPECTABLE_IFACE, + out_signature=u"s", + path_keyword='object_path', + connection_keyword='connection') + def Introspect(self, object_path, connection): + """Standard D-Bus method, overloaded to insert property 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(u"property") + e.setAttribute(u"name", name) + e.setAttribute(u"type", prop._dbus_signature) + e.setAttribute(u"access", prop._dbus_access) + return e + for if_tag in document.getElementsByTagName(u"interface"): + for tag in (make_tag(document, name, prop) + for name, prop + in self._get_all_dbus_properties() + if prop._dbus_interface + == if_tag.getAttribute(u"name")): + if_tag.appendChild(tag) + # Add the names to the return values for the + # "org.freedesktop.DBus.Properties" methods + if (if_tag.getAttribute(u"name") + == u"org.freedesktop.DBus.Properties"): + for cn in if_tag.getElementsByTagName(u"method"): + if cn.getAttribute(u"name") == u"Get": + for arg in cn.getElementsByTagName(u"arg"): + if (arg.getAttribute(u"direction") + == u"out"): + arg.setAttribute(u"name", u"value") + elif cn.getAttribute(u"name") == u"GetAll": + for arg in cn.getElementsByTagName(u"arg"): + if (arg.getAttribute(u"direction") + == u"out"): + arg.setAttribute(u"name", u"props") + xmlstring = document.toxml(u"utf-8") + document.unlink() + except (AttributeError, xml.dom.DOMException, + xml.parsers.expat.ExpatError), error: + logger.error(u"Failed to override Introspection method", + error) + return xmlstring + + +class ClientDBus(Client, DBusObjectWithProperties): """A Client class using D-Bus Attributes: dbus_object_path: dbus.ObjectPath bus: dbus.SystemBus() """ + + runtime_expansions = (Client.runtime_expansions + + (u"dbus_object_path",)) + # 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( + {ord(u"."): ord(u"_"), + ord(u"-"): ord(u"_")}) self.dbus_object_path = (dbus.ObjectPath - (u"/clients/" - + self.name.replace(u".", u"_"))) - dbus.service.Object.__init__(self, self.bus, - self.dbus_object_path) + (u"/clients/" + client_object_name)) + DBusObjectWithProperties.__init__(self, self.bus, + self.dbus_object_path) + + def _get_approvals_pending(self): + return self._approvals_pending + def _set_approvals_pending(self, value): + old_value = self._approvals_pending + self._approvals_pending = value + bval = bool(value) + if (hasattr(self, "dbus_object_path") + and bval is not bool(old_value)): + dbus_bool = dbus.Boolean(bval, variant_level=1) + self.PropertyChanged(dbus.String(u"ApprovalPending"), + dbus_bool) + + approvals_pending = property(_get_approvals_pending, + _set_approvals_pending) + del _get_approvals_pending, _set_approvals_pending @staticmethod def _datetime_to_dbus(dt, variant_level=0): @@ -508,20 +763,20 @@ r = Client.enable(self) if oldstate != self.enabled: # Emit D-Bus signals - self.PropertyChanged(dbus.String(u"enabled"), + self.PropertyChanged(dbus.String(u"Enabled"), dbus.Boolean(True, variant_level=1)) self.PropertyChanged( - dbus.String(u"last_enabled"), + dbus.String(u"LastEnabled"), self._datetime_to_dbus(self.last_enabled, variant_level=1)) return r - def disable(self, signal = True): + def disable(self, quiet = False): oldstate = getattr(self, u"enabled", False) - r = Client.disable(self) - if signal and oldstate != self.enabled: + r = Client.disable(self, quiet=quiet) + if not quiet and oldstate != self.enabled: # Emit D-Bus signal - self.PropertyChanged(dbus.String(u"enabled"), + self.PropertyChanged(dbus.String(u"Enabled"), dbus.Boolean(False, variant_level=1)) return r @@ -530,8 +785,8 @@ self.remove_from_connection() except LookupError: pass - if hasattr(dbus.service.Object, u"__del__"): - dbus.service.Object.__del__(self, *args, **kwargs) + if hasattr(DBusObjectWithProperties, u"__del__"): + DBusObjectWithProperties.__del__(self, *args, **kwargs) Client.__del__(self, *args, **kwargs) def checker_callback(self, pid, condition, command, @@ -539,7 +794,7 @@ self.checker_callback_tag = None self.checker = None # Emit D-Bus signal - self.PropertyChanged(dbus.String(u"checker_running"), + self.PropertyChanged(dbus.String(u"CheckerRunning"), dbus.Boolean(False, variant_level=1)) if os.WIFEXITED(condition): exitstatus = os.WEXITSTATUS(condition) @@ -560,11 +815,20 @@ r = Client.checked_ok(self, *args, **kwargs) # Emit D-Bus signal self.PropertyChanged( - dbus.String(u"last_checked_ok"), + dbus.String(u"LastCheckedOK"), (self._datetime_to_dbus(self.last_checked_ok, variant_level=1))) return r + def need_approval(self, *args, **kwargs): + r = Client.need_approval(self, *args, **kwargs) + # Emit D-Bus signal + self.PropertyChanged( + dbus.String(u"LastApprovalRequest"), + (self._datetime_to_dbus(self.last_approval_request, + variant_level=1))) + return r + def start_checker(self, *args, **kwargs): old_checker = self.checker if self.checker is not None: @@ -578,7 +842,7 @@ # Emit D-Bus signal self.CheckerStarted(self.current_checker_command) self.PropertyChanged( - dbus.String(u"checker_running"), + dbus.String(u"CheckerRunning"), dbus.Boolean(True, variant_level=1)) return r @@ -587,17 +851,26 @@ r = Client.stop_checker(self, *args, **kwargs) if (old_checker is not None and getattr(self, u"checker", None) is None): - self.PropertyChanged(dbus.String(u"checker_running"), + self.PropertyChanged(dbus.String(u"CheckerRunning"), dbus.Boolean(False, variant_level=1)) return r - - ## D-Bus methods & signals + + def _reset_approved(self): + self._approved = None + return False + + def approve(self, value=True): + self.send_changedstate() + self._approved = value + gobject.timeout_add(self._timedelta_to_milliseconds + (self.approval_duration), + self._reset_approved) + + + ## D-Bus methods, signals & properties _interface = u"se.bsnet.fukt.Mandos.Client" - # CheckedOK - method - @dbus.service.method(_interface) - def CheckedOK(self): - return self.checked_ok() + ## Signals # CheckerCompleted - signal @dbus.service.signal(_interface, signature=u"nxs") @@ -611,115 +884,44 @@ "D-Bus signal" pass - # GetAllProperties - method - @dbus.service.method(_interface, out_signature=u"a{sv}") - def GetAllProperties(self): - "D-Bus method" - return dbus.Dictionary({ - dbus.String(u"name"): - dbus.String(self.name, variant_level=1), - dbus.String(u"fingerprint"): - dbus.String(self.fingerprint, variant_level=1), - dbus.String(u"host"): - dbus.String(self.host, variant_level=1), - dbus.String(u"created"): - self._datetime_to_dbus(self.created, - variant_level=1), - dbus.String(u"last_enabled"): - (self._datetime_to_dbus(self.last_enabled, - variant_level=1) - if self.last_enabled is not None - else dbus.Boolean(False, variant_level=1)), - dbus.String(u"enabled"): - dbus.Boolean(self.enabled, variant_level=1), - dbus.String(u"last_checked_ok"): - (self._datetime_to_dbus(self.last_checked_ok, - variant_level=1) - if self.last_checked_ok is not None - else dbus.Boolean (False, variant_level=1)), - dbus.String(u"timeout"): - dbus.UInt64(self.timeout_milliseconds(), - variant_level=1), - dbus.String(u"interval"): - dbus.UInt64(self.interval_milliseconds(), - variant_level=1), - dbus.String(u"checker"): - dbus.String(self.checker_command, - variant_level=1), - dbus.String(u"checker_running"): - dbus.Boolean(self.checker is not None, - variant_level=1), - dbus.String(u"object_path"): - dbus.ObjectPath(self.dbus_object_path, - variant_level=1) - }, signature=u"sv") - - # IsStillValid - method - @dbus.service.method(_interface, out_signature=u"b") - def IsStillValid(self): - return self.still_valid() - # PropertyChanged - signal @dbus.service.signal(_interface, signature=u"sv") def PropertyChanged(self, property, value): "D-Bus signal" pass - # ReceivedSecret - signal + # GotSecret - signal @dbus.service.signal(_interface) - def ReceivedSecret(self): - "D-Bus signal" + def GotSecret(self): + """D-Bus signal + Is sent after a successful transfer of secret from the Mandos + server to mandos-client + """ pass # Rejected - signal - @dbus.service.signal(_interface) - def Rejected(self): + @dbus.service.signal(_interface, signature=u"s") + def Rejected(self, reason): "D-Bus signal" pass - # SetChecker - method - @dbus.service.method(_interface, in_signature=u"s") - def SetChecker(self, checker): - "D-Bus setter method" - self.checker_command = checker - # Emit D-Bus signal - self.PropertyChanged(dbus.String(u"checker"), - dbus.String(self.checker_command, - variant_level=1)) - - # SetHost - method - @dbus.service.method(_interface, in_signature=u"s") - def SetHost(self, host): - "D-Bus setter method" - self.host = host - # Emit D-Bus signal - self.PropertyChanged(dbus.String(u"host"), - dbus.String(self.host, variant_level=1)) - - # SetInterval - method - @dbus.service.method(_interface, in_signature=u"t") - def SetInterval(self, milliseconds): - self.interval = datetime.timedelta(0, 0, 0, milliseconds) - # Emit D-Bus signal - self.PropertyChanged(dbus.String(u"interval"), - (dbus.UInt64(self.interval_milliseconds(), - variant_level=1))) - - # SetSecret - method - @dbus.service.method(_interface, in_signature=u"ay", - byte_arrays=True) - def SetSecret(self, secret): - "D-Bus setter method" - self.secret = str(secret) - - # SetTimeout - method - @dbus.service.method(_interface, in_signature=u"t") - def SetTimeout(self, milliseconds): - self.timeout = datetime.timedelta(0, 0, 0, milliseconds) - # Emit D-Bus signal - self.PropertyChanged(dbus.String(u"timeout"), - (dbus.UInt64(self.timeout_milliseconds(), - variant_level=1))) + # NeedApproval - signal + @dbus.service.signal(_interface, signature=u"tb") + def NeedApproval(self, timeout, default): + "D-Bus signal" + return self.need_approval() + + ## Methods + + # Approve - method + @dbus.service.method(_interface, in_signature=u"b") + def Approve(self, value): + self.approve(value) + + # CheckedOK - method + @dbus.service.method(_interface) + def CheckedOK(self): + return self.checked_ok() # Enable - method @dbus.service.method(_interface) @@ -744,9 +946,221 @@ def StopChecker(self): self.stop_checker() + ## Properties + + # ApprovalPending - property + @dbus_service_property(_interface, signature=u"b", access=u"read") + def ApprovalPending_dbus_property(self): + return dbus.Boolean(bool(self.approvals_pending)) + + # ApprovedByDefault - property + @dbus_service_property(_interface, signature=u"b", + access=u"readwrite") + def ApprovedByDefault_dbus_property(self, value=None): + if value is None: # get + return dbus.Boolean(self.approved_by_default) + self.approved_by_default = bool(value) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"ApprovedByDefault"), + dbus.Boolean(value, variant_level=1)) + + # ApprovalDelay - property + @dbus_service_property(_interface, signature=u"t", + access=u"readwrite") + def ApprovalDelay_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.approval_delay_milliseconds()) + self.approval_delay = datetime.timedelta(0, 0, 0, value) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"ApprovalDelay"), + dbus.UInt64(value, variant_level=1)) + + # ApprovalDuration - property + @dbus_service_property(_interface, signature=u"t", + access=u"readwrite") + def ApprovalDuration_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self._timedelta_to_milliseconds( + self.approval_duration)) + self.approval_duration = datetime.timedelta(0, 0, 0, value) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"ApprovalDuration"), + dbus.UInt64(value, variant_level=1)) + + # Name - property + @dbus_service_property(_interface, signature=u"s", access=u"read") + def Name_dbus_property(self): + return dbus.String(self.name) + + # Fingerprint - property + @dbus_service_property(_interface, signature=u"s", access=u"read") + def Fingerprint_dbus_property(self): + return dbus.String(self.fingerprint) + + # Host - property + @dbus_service_property(_interface, signature=u"s", + access=u"readwrite") + def Host_dbus_property(self, value=None): + if value is None: # get + return dbus.String(self.host) + self.host = value + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"Host"), + dbus.String(value, variant_level=1)) + + # Created - property + @dbus_service_property(_interface, signature=u"s", access=u"read") + def Created_dbus_property(self): + return dbus.String(self._datetime_to_dbus(self.created)) + + # LastEnabled - property + @dbus_service_property(_interface, signature=u"s", access=u"read") + def LastEnabled_dbus_property(self): + if self.last_enabled is None: + return dbus.String(u"") + return dbus.String(self._datetime_to_dbus(self.last_enabled)) + + # Enabled - property + @dbus_service_property(_interface, signature=u"b", + access=u"readwrite") + def Enabled_dbus_property(self, value=None): + if value is None: # get + return dbus.Boolean(self.enabled) + if value: + self.enable() + else: + self.disable() + + # LastCheckedOK - property + @dbus_service_property(_interface, signature=u"s", + access=u"readwrite") + def LastCheckedOK_dbus_property(self, value=None): + if value is not None: + self.checked_ok() + return + if self.last_checked_ok is None: + return dbus.String(u"") + return dbus.String(self._datetime_to_dbus(self + .last_checked_ok)) + + # LastApprovalRequest - property + @dbus_service_property(_interface, signature=u"s", access=u"read") + def LastApprovalRequest_dbus_property(self): + if self.last_approval_request is None: + return dbus.String(u"") + return dbus.String(self. + _datetime_to_dbus(self + .last_approval_request)) + + # Timeout - property + @dbus_service_property(_interface, signature=u"t", + access=u"readwrite") + def Timeout_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.timeout_milliseconds()) + self.timeout = datetime.timedelta(0, 0, 0, value) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"Timeout"), + dbus.UInt64(value, variant_level=1)) + if getattr(self, u"disable_initiator_tag", None) is None: + return + # Reschedule timeout + gobject.source_remove(self.disable_initiator_tag) + self.disable_initiator_tag = None + time_to_die = (self. + _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.disable_initiator_tag = (gobject.timeout_add + (time_to_die, self.disable)) + + # Interval - property + @dbus_service_property(_interface, signature=u"t", + access=u"readwrite") + def Interval_dbus_property(self, value=None): + if value is None: # get + return dbus.UInt64(self.interval_milliseconds()) + self.interval = datetime.timedelta(0, 0, 0, value) + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"Interval"), + dbus.UInt64(value, variant_level=1)) + if getattr(self, u"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 + + # Checker - property + @dbus_service_property(_interface, signature=u"s", + access=u"readwrite") + def Checker_dbus_property(self, value=None): + if value is None: # get + return dbus.String(self.checker_command) + self.checker_command = value + # Emit D-Bus signal + self.PropertyChanged(dbus.String(u"Checker"), + dbus.String(self.checker_command, + variant_level=1)) + + # CheckerRunning - property + @dbus_service_property(_interface, signature=u"b", + access=u"readwrite") + def CheckerRunning_dbus_property(self, value=None): + if value is None: # get + return dbus.Boolean(self.checker is not None) + if value: + self.start_checker() + else: + self.stop_checker() + + # ObjectPath - property + @dbus_service_property(_interface, signature=u"o", access=u"read") + def ObjectPath_dbus_property(self): + return self.dbus_object_path # is already a dbus.ObjectPath + + # Secret = property + @dbus_service_property(_interface, signature=u"ay", + access=u"write", byte_arrays=True) + def Secret_dbus_property(self, value): + self.secret = str(value) + del _interface +class ProxyClient(object): + def __init__(self, child_pipe, fpr, address): + self._pipe = child_pipe + self._pipe.send(('init', fpr, address)) + if not self._pipe.recv(): + raise KeyError() + + def __getattribute__(self, name): + 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'): + return super(ProxyClient, self).__setattr__(name, value) + self._pipe.send(('setattr', name, value)) + + class ClientHandler(socketserver.BaseRequestHandler, object): """A class to handle client connections. @@ -754,30 +1168,22 @@ Note: This will run in its own forked process.""" def handle(self): - logger.info(u"TCP connection from: %s", - unicode(self.client_address)) - logger.debug(u"IPC Pipe FD: %d", self.server.pipe[1]) - # Open IPC pipe to parent process - with closing(os.fdopen(self.server.pipe[1], u"w", 1)) as ipc: + with contextlib.closing(self.server.child_pipe) as child_pipe: + logger.info(u"TCP connection from: %s", + unicode(self.client_address)) + logger.debug(u"Pipe FD: %d", + self.server.child_pipe.fileno()) + session = (gnutls.connection .ClientSession(self.request, gnutls.connection .X509Credentials())) - - line = self.request.makefile().readline() - logger.debug(u"Protocol version: %r", line) - try: - if int(line.strip().split()[0]) > 1: - raise RuntimeError - except (ValueError, IndexError, RuntimeError), error: - logger.error(u"Unknown protocol version: %s", error) - return - + # Note: gnutls.connection.X509Credentials is really a # generic GnuTLS certificate credentials object so long as # no X.509 keys are added to it. Therefore, we can use it # here despite using OpenPGP certificates. - + #priority = u':'.join((u"NONE", u"+VERS-TLS1.1", # u"+AES-256-CBC", u"+SHA1", # u"+COMP-NULL", u"+CTYPE-OPENPGP", @@ -789,7 +1195,19 @@ (gnutls.library.functions .gnutls_priority_set_direct(session._c_object, priority, None)) - + + # Start communication using the Mandos protocol + # Get protocol number + line = self.request.makefile().readline() + logger.debug(u"Protocol version: %r", line) + try: + if int(line.strip().split()[0]) > 1: + raise RuntimeError + except (ValueError, IndexError, RuntimeError), error: + logger.error(u"Unknown protocol version: %s", error) + return + + # Start GnuTLS connection try: session.handshake() except gnutls.errors.GNUTLSError, error: @@ -798,39 +1216,103 @@ # established. Just abandon the request. return logger.debug(u"Handshake succeeded") + + approval_required = False try: - fpr = self.fingerprint(self.peer_certificate(session)) - except (TypeError, gnutls.errors.GNUTLSError), error: - logger.warning(u"Bad certificate: %s", error) - session.bye() - return - logger.debug(u"Fingerprint: %s", fpr) + try: + fpr = self.fingerprint(self.peer_certificate + (session)) + except (TypeError, gnutls.errors.GNUTLSError), error: + logger.warning(u"Bad certificate: %s", error) + return + logger.debug(u"Fingerprint: %s", fpr) + + try: + client = ProxyClient(child_pipe, fpr, + self.client_address) + except KeyError: + return + + if client.approval_delay: + delay = client.approval_delay + client.approvals_pending += 1 + approval_required = True + + while True: + if not client.enabled: + logger.warning(u"Client %s is disabled", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Disabled") + return + + if client._approved or not client.approval_delay: + #We are approved or approval is disabled + break + elif client._approved is None: + logger.info(u"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) + else: + logger.warning(u"Client %s was not approved", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Denied") + return + + #wait until timeout or approved + #x = float(client._timedelta_to_milliseconds(delay)) + time = datetime.datetime.now() + client.changedstate.acquire() + client.changedstate.wait(float(client._timedelta_to_milliseconds(delay) / 1000)) + client.changedstate.release() + time2 = datetime.datetime.now() + if (time2 - time) >= delay: + if not client.approved_by_default: + logger.warning("Client %s timed out while" + " waiting for approval", + client.name) + if self.server.use_dbus: + # Emit D-Bus signal + client.Rejected("Approval timed out") + return + else: + break + else: + delay -= time2 - time + + sent_size = 0 + while sent_size < len(client.secret): + try: + sent = session.send(client.secret[sent_size:]) + except (gnutls.errors.GNUTLSError), error: + logger.warning("gnutls send failed") + return + logger.debug(u"Sent: %d, remaining: %d", + sent, len(client.secret) + - (sent_size + sent)) + sent_size += sent + + logger.info(u"Sending secret to %s", client.name) + # bump the timeout as if seen + client.checked_ok() + if self.server.use_dbus: + # Emit D-Bus signal + client.GotSecret() - for c in self.server.clients: - if c.fingerprint == fpr: - client = c - break - else: - ipc.write(u"NOTFOUND %s %s\n" - % (fpr, unicode(self.client_address))) - session.bye() - return - # Have to check if client.still_valid(), since it is - # possible that the client timed out while establishing - # the GnuTLS session. - if not client.still_valid(): - ipc.write(u"INVALID %s\n" % client.name) - session.bye() - return - ipc.write(u"SENDING %s\n" % client.name) - sent_size = 0 - while sent_size < len(client.secret): - sent = session.send(client.secret[sent_size:]) - logger.debug(u"Sent: %d, remaining: %d", - sent, len(client.secret) - - (sent_size + sent)) - sent_size += sent - session.bye() + finally: + if approval_required: + client.approvals_pending -= 1 + try: + session.bye() + except (gnutls.errors.GNUTLSError), error: + logger.warning("GnuTLS bye failed") @staticmethod def peer_certificate(session): @@ -896,24 +1378,39 @@ return hex_fpr -class ForkingMixInWithPipe(socketserver.ForkingMixIn, object): - """Like socketserver.ForkingMixIn, but also pass a pipe.""" +class MultiprocessingMixIn(object): + """Like socketserver.ThreadingMixIn, but with multiprocessing""" + def sub_process_main(self, request, address): + try: + self.finish_request(request, address) + except: + self.handle_error(request, address) + self.close_request(request) + + def process_request(self, request, address): + """Start a new process to process the request.""" + multiprocessing.Process(target = self.sub_process_main, + args = (request, address)).start() + +class MultiprocessingMixInWithPipe(MultiprocessingMixIn, object): + """ adds a pipe to the MixIn """ def process_request(self, request, client_address): """Overrides and wraps the original process_request(). This function creates a new pipe in self.pipe """ - self.pipe = os.pipe() - super(ForkingMixInWithPipe, + parent_pipe, self.child_pipe = multiprocessing.Pipe() + + super(MultiprocessingMixInWithPipe, self).process_request(request, client_address) - os.close(self.pipe[1]) # close write end - self.add_pipe(self.pipe[0]) - def add_pipe(self, pipe): + self.child_pipe.close() + self.add_pipe(parent_pipe) + + def add_pipe(self, parent_pipe): """Dummy function; override as necessary""" - os.close(pipe) - - -class IPv6_TCPServer(ForkingMixInWithPipe, + pass + +class IPv6_TCPServer(MultiprocessingMixInWithPipe, socketserver.TCPServer, object): """IPv6-capable TCP server. Accepts 'None' as address and/or port @@ -983,9 +1480,6 @@ clients: set of Client objects gnutls_priority GnuTLS priority string use_dbus: Boolean; to emit D-Bus signals or not - clients: set of Client objects - gnutls_priority GnuTLS priority string - use_dbus: Boolean; to emit D-Bus signals or not Assumes a gobject.MainLoop event loop. """ @@ -1007,11 +1501,15 @@ return socketserver.TCPServer.server_activate(self) def enable(self): self.enabled = True - def add_pipe(self, pipe): + def add_pipe(self, parent_pipe): # Call "handle_ipc" for both data and EOF events - gobject.io_add_watch(pipe, gobject.IO_IN | gobject.IO_HUP, - self.handle_ipc) - def handle_ipc(self, source, condition, file_objects={}): + gobject.io_add_watch(parent_pipe.fileno(), + gobject.IO_IN | gobject.IO_HUP, + functools.partial(self.handle_ipc, + parent_pipe = parent_pipe)) + + def handle_ipc(self, source, condition, parent_pipe=None, + client_object=None): condition_names = { gobject.IO_IN: u"IN", # There is data to read. gobject.IO_OUT: u"OUT", # Data can be written (without @@ -1026,60 +1524,58 @@ for cond, name in condition_names.iteritems() if cond & condition) - logger.debug(u"Handling IPC: FD = %d, condition = %s", source, - conditions_string) - - # Turn the pipe file descriptor into a Python file object - if source not in file_objects: - file_objects[source] = os.fdopen(source, u"r", 1) - - # Read a line from the file object - cmdline = file_objects[source].readline() - if not cmdline: # Empty line means end of file - # close the IPC pipe - file_objects[source].close() - del file_objects[source] - - # Stop calling this function - return False - - logger.debug(u"IPC command: %r", cmdline) - - # Parse and act on command - cmd, args = cmdline.rstrip(u"\r\n").split(None, 1) - - if cmd == u"NOTFOUND": - logger.warning(u"Client not found for fingerprint: %s", - args) - if self.use_dbus: - # Emit D-Bus signal - mandos_dbus_service.ClientNotFound(args) - elif cmd == u"INVALID": - for client in self.clients: - if client.name == args: - logger.warning(u"Client %s is invalid", args) - if self.use_dbus: - # Emit D-Bus signal - client.Rejected() - break - else: - logger.error(u"Unknown client %s is invalid", args) - elif cmd == u"SENDING": - for client in self.clients: - if client.name == args: - logger.info(u"Sending secret to %s", client.name) - client.checked_ok() - if self.use_dbus: - # Emit D-Bus signal - client.ReceivedSecret() - break - else: - logger.error(u"Sending secret to unknown client %s", - args) - else: - logger.error(u"Unknown IPC command: %r", cmdline) - - # Keep calling this function + # error or the other end of multiprocessing.Pipe has closed + if condition & (gobject.IO_ERR | condition & gobject.IO_HUP): + return False + + # Read a request from the child + request = parent_pipe.recv() + command = request[0] + + if command == 'init': + fpr = request[1] + address = request[2] + + for c in self.clients: + if c.fingerprint == fpr: + client = c + break + else: + logger.warning(u"Client not found for fingerprint: %s, ad" + u"dress: %s", fpr, address) + if self.use_dbus: + # Emit D-Bus signal + mandos_dbus_service.ClientNotFound(fpr, address[0]) + 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, + client_object = client)) + parent_pipe.send(True) + # remove the old hook in favor of the new above hook on same fileno + return False + if command == 'funcall': + funcname = request[1] + args = request[2] + kwargs = request[3] + + parent_pipe.send(('data', getattr(client_object, funcname)(*args, **kwargs))) + + if command == 'getattr': + attrname = request[1] + if callable(client_object.__getattribute__(attrname)): + parent_pipe.send(('function',)) + else: + parent_pipe.send(('data', client_object.__getattribute__(attrname))) + + if command == 'setattr': + attrname = request[1] + value = request[2] + setattr(client_object, attrname, value) + return True @@ -1115,9 +1611,9 @@ elif suffix == u"w": delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value) else: - raise ValueError - except (ValueError, IndexError): - raise ValueError + raise ValueError(u"Unknown suffix %r" % suffix) + except (ValueError, IndexError), e: + raise ValueError(e.message) timevalue += delta return timevalue @@ -1136,7 +1632,7 @@ def if_nametoindex(interface): "Get an interface index the hard way, i.e. using fcntl()" SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h - with closing(socket.socket()) as s: + with contextlib.closing(socket.socket()) as s: ifreq = fcntl.ioctl(s, SIOCGIFINDEX, struct.pack(str(u"16s16x"), interface)) @@ -1162,7 +1658,8 @@ null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR) if not stat.S_ISCHR(os.fstat(null).st_mode): raise OSError(errno.ENODEV, - u"/dev/null not a character device") + u"%s not a character device" + % os.path.devnull) os.dup2(null, sys.stdin.fileno()) os.dup2(null, sys.stdout.fileno()) os.dup2(null, sys.stderr.fileno()) @@ -1172,7 +1669,7 @@ def main(): - ###################################################################### + ################################################################## # Parsing of options, both command line and config file parser = optparse.OptionParser(version = "%%prog %s" % version) @@ -1187,6 +1684,8 @@ parser.add_option("--debug", action=u"store_true", help=u"Debug mode; run in foreground and log to" u" terminal") + parser.add_option("--debuglevel", type=u"string", metavar="LEVEL", + help=u"Debug level for stdout output") parser.add_option("--priority", type=u"string", help=u"GnuTLS" u" priority string (see GnuTLS documentation)") parser.add_option("--servicename", type=u"string", @@ -1196,8 +1695,8 @@ help=u"Directory to search for configuration" u" files") parser.add_option("--no-dbus", action=u"store_false", - dest=u"use_dbus", - help=optparse.SUPPRESS_HELP) # XXX: Not done yet + dest=u"use_dbus", help=u"Do not provide D-Bus" + u" system bus interface") parser.add_option("--no-ipv6", action=u"store_false", dest=u"use_ipv6", help=u"Do not use IPv6") options = parser.parse_args()[0] @@ -1217,6 +1716,7 @@ u"servicename": u"Mandos", u"use_dbus": u"True", u"use_ipv6": u"True", + u"debuglevel": u"", } # Parse config file for server-global settings @@ -1239,7 +1739,7 @@ # options, if set. for option in (u"interface", u"address", u"port", u"debug", u"priority", u"servicename", u"configdir", - u"use_dbus", u"use_ipv6"): + u"use_dbus", u"use_ipv6", u"debuglevel"): value = getattr(options, option) if value is not None: server_settings[option] = value @@ -1254,14 +1754,10 @@ # For convenience debug = server_settings[u"debug"] + debuglevel = server_settings[u"debuglevel"] use_dbus = server_settings[u"use_dbus"] - use_dbus = False # XXX: Not done yet use_ipv6 = server_settings[u"use_ipv6"] - - if not debug: - syslogger.setLevel(logging.WARNING) - console.setLevel(logging.WARNING) - + if server_settings[u"servicename"] != u"Mandos": syslogger.setFormatter(logging.Formatter (u'Mandos (%s) [%%(process)d]:' @@ -1273,6 +1769,8 @@ u"interval": u"5m", u"checker": u"fping -q -- %%(host)s", u"host": u"", + u"approval_delay": u"0s", + u"approval_duration": u"1s", } client_config = configparser.SafeConfigParser(client_defaults) client_config.read(os.path.join(server_settings[u"configdir"], @@ -1284,16 +1782,18 @@ tcp_server = MandosServer((server_settings[u"address"], server_settings[u"port"]), ClientHandler, - interface=server_settings[u"interface"], + interface=(server_settings[u"interface"] + or None), use_ipv6=use_ipv6, gnutls_priority= server_settings[u"priority"], use_dbus=use_dbus) - pidfilename = u"/var/run/mandos.pid" - try: - pidfile = open(pidfilename, u"w") - except IOError: - logger.error(u"Could not open file %r", pidfilename) + if not debug: + pidfilename = u"/var/run/mandos.pid" + try: + pidfile = open(pidfilename, u"w") + except IOError: + logger.error(u"Could not open file %r", pidfilename) try: uid = pwd.getpwnam(u"_mandos").pw_uid @@ -1316,8 +1816,17 @@ if error[0] != errno.EPERM: raise error - # Enable all possible GnuTLS debugging + 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 debug: + # Enable all possible GnuTLS debugging + # "Use a log level over 10 to enable all debugging options." # - GnuTLS manual gnutls.library.functions.gnutls_global_set_log_level(11) @@ -1328,6 +1837,16 @@ (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) + os.dup2(null, sys.stdin.fileno()) + if null > 2: + os.close(null) + else: + # No console logging + logger.removeHandler(console) + global main_loop # From the Avahi example code @@ -1336,7 +1855,14 @@ bus = dbus.SystemBus() # End of Avahi example code if use_dbus: - bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", bus) + try: + bus_name = dbus.service.BusName(u"se.bsnet.fukt.Mandos", + bus, do_not_queue=True) + except dbus.exceptions.NameExistsException, e: + logger.error(unicode(e) + u", disabling D-Bus") + use_dbus = False + server_settings[u"use_dbus"] = False + tcp_server.use_dbus = False protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET service = AvahiService(name = server_settings[u"servicename"], servicetype = u"_mandos._tcp", @@ -1344,55 +1870,53 @@ if server_settings["interface"]: service.interface = (if_nametoindex (str(server_settings[u"interface"]))) + + if not debug: + # Close all input and output, do double fork, etc. + daemon() + + global multiprocessing_manager + multiprocessing_manager = multiprocessing.Manager() client_class = Client if use_dbus: client_class = functools.partial(ClientDBus, 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): + 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(section))) + config= dict(client_config_items( + client_config, section))) for section in client_config.sections())) if not tcp_server.clients: logger.warning(u"No clients defined") - - if debug: - # Redirect stdin so all checkers get /dev/null - null = os.open(os.path.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) - # Close all input and output, do double fork, etc. - daemon() - - try: - with closing(pidfile): - pid = os.getpid() - pidfile.write(str(pid) + "\n") - del pidfile - except IOError: - logger.error(u"Could not write to file %r with PID %d", - pidfilename, pid) - except NameError: - # "pidfile" was never created - pass - del pidfilename - - def cleanup(): - "Cleanup function; run on exit" - service.cleanup() - while tcp_server.clients: - client = tcp_server.clients.pop() - client.disable_hook = None - client.disable() - - atexit.register(cleanup) - if not debug: + try: + with pidfile: + pid = os.getpid() + pidfile.write(str(pid) + "\n") + del pidfile + except IOError: + logger.error(u"Could not write to file %r with PID %d", + pidfilename, pid) + except NameError: + # "pidfile" was never created + pass + 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()) @@ -1403,13 +1927,13 @@ dbus.service.Object.__init__(self, bus, u"/") _interface = u"se.bsnet.fukt.Mandos" - @dbus.service.signal(_interface, signature=u"oa{sv}") - def ClientAdded(self, objpath, properties): + @dbus.service.signal(_interface, signature=u"o") + def ClientAdded(self, objpath): "D-Bus signal" pass - @dbus.service.signal(_interface, signature=u"s") - def ClientNotFound(self, fingerprint): + @dbus.service.signal(_interface, signature=u"ss") + def ClientNotFound(self, fingerprint, address): "D-Bus signal" pass @@ -1429,7 +1953,7 @@ def GetAllClientsWithProperties(self): "D-Bus method" return dbus.Dictionary( - ((c.dbus_object_path, c.GetAllProperties()) + ((c.dbus_object_path, c.GetAll(u"")) for c in tcp_server.clients), signature=u"oa{sv}") @@ -1441,21 +1965,38 @@ tcp_server.clients.remove(c) c.remove_from_connection() # Don't signal anything except ClientRemoved - c.disable(signal=False) + c.disable(quiet=True) # Emit D-Bus signal self.ClientRemoved(object_path, c.name) return - raise KeyError + raise KeyError(object_path) del _interface mandos_dbus_service = MandosDBusService() + def cleanup(): + "Cleanup function; run on exit" + service.cleanup() + + while tcp_server.clients: + client = tcp_server.clients.pop() + 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) + + atexit.register(cleanup) + for client in tcp_server.clients: if use_dbus: # Emit D-Bus signal - mandos_dbus_service.ClientAdded(client.dbus_object_path, - client.GetAllProperties()) + mandos_dbus_service.ClientAdded(client.dbus_object_path) client.enable() tcp_server.enable() @@ -1479,6 +2020,7 @@ service.activate() except dbus.exceptions.DBusException, error: logger.critical(u"DBusException: %s", error) + cleanup() sys.exit(1) # End of Avahi example code @@ -1491,12 +2033,15 @@ main_loop.run() except AvahiError, error: logger.critical(u"AvahiError: %s", error) + cleanup() sys.exit(1) except KeyboardInterrupt: if debug: print >> sys.stderr logger.debug(u"Server received KeyboardInterrupt") logger.debug(u"Server exiting") + # Must run before the D-Bus bus name gets deregistered + cleanup() if __name__ == '__main__': main() === modified file 'mandos-clients.conf.xml' --- mandos-clients.conf.xml 2009-09-17 01:21:27 +0000 +++ mandos-clients.conf.xml 2010-09-27 18:57:12 +0000 @@ -3,7 +3,7 @@ "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd" [ /etc/mandos/clients.conf"> - + %common; ]> @@ -34,6 +34,7 @@ 2008 2009 + 2010 Teddy Hogeborn Björn Påhlsson @@ -63,9 +64,8 @@ >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 valid, even - if a client was declared invalid in a previous run of the - server. + the service. All clients listed will be regarded as enabled, + even if a client was disabled in a previous run of the server. The format starts with a [section @@ -101,54 +101,53 @@ - - - - This option is optional. - - - The timeout is how long the server will wait (for either a - successful checker run or a client receiving its secret) - until a client is considered invalid - that is, ineligible - to get the data this server holds. By default Mandos will - use 1 hour. - - - 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. - - - - - - - - - This option is optional. - - - 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 marked invalid, and any running - checker killed. The default interval is 5 minutes. - - - The format of TIME is the same - as for timeout above. + + + + This option is optional. + + + How long to wait for external approval before resorting to + use the value. The + default is 0s, i.e. not to wait. + + + The format of TIME is the same + as for timeout below. + + + + + + + + + This option is optional. + + + How long an external approval lasts. The default is 1 + second. + + + The format of TIME is the same + as for timeout below. + + + + + + + + + Whether to approve a client by default after + the . The default + is True. @@ -198,6 +197,70 @@ + + + + This option is optional, but highly + recommended unless the + option is modified to a + non-standard value without %%(host)s in it. + + + Host name for this client. This is not used by the server + directly, but can be, and is by default, used by the + checker. See the option. + + + + + + + + + This option is optional. + + + 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 below + timeout occurs, at which + time the client will be disabled, and any running checker + killed. The default interval is 5 minutes. + + + The format of TIME is the same + as for timeout below. + + + + + + + + + This option is only used if is not + specified, in which case this option is + required. + + + Similar to the , except the secret + data is in an external file. The contents of the file + should not be base64-encoded, but + will be sent to clients verbatim. + + + File names of the form ~user/foo/bar + and $ENVVAR/foo/bar + are supported. + + + + + @@ -228,42 +291,30 @@ - - - - This option is only used if is not - specified, in which case this option is - required. - - - Similar to the , except the secret - data is in an external file. The contents of the file - should not be base64-encoded, but - will be sent to clients verbatim. - - - File names of the form ~user/foo/bar - and $ENVVAR/foo/bar - are supported. - - - - - - - - - This option is optional, but highly - recommended unless the - option is modified to a - non-standard value without %%(host)s in it. - - - Host name for this client. This is not used by the server - directly, but can be, and is by default, used by the - checker. See the option. + + + + This option is optional. + + + The timeout is how long the server will wait (for either a + successful checker run or a client receiving its secret) + until a client is disabled and not allowed to get the data + this server holds. By default Mandos will use 1 hour. + + + 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. @@ -306,10 +357,28 @@ %%(foo)s will be replaced by the value of the attribute foo of the internal - Client object. See the - source code for details, and let the authors know of any - attributes that are useful so they may be preserved to any new - versions of this software. + Client object in the + Mandos server. The currently allowed values for + foo are: + approval_delay, + approval_duration, + created, + enabled, + fingerprint, + host, + interval, + last_approval_request, + last_checked_ok, + last_enabled, + name, + timeout, and, if using + D-Bus, dbus_object_path. + See the source code for details. Currently, none of these attributes + except host are guaranteed + to be valid in future versions. Therefore, please + let the authors know of any attributes that are useful so they + may be preserved to any new versions of this software. Note that this means that, in order to include an actual @@ -381,6 +450,8 @@ fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27 secfile = /etc/mandos/bar-secret timeout = 15m +approved_by_default = False +approval_delay = 30s === modified file 'mandos-ctl' --- mandos-ctl 2009-10-25 19:16:56 +0000 +++ mandos-ctl 2010-09-26 18:32:58 +0000 @@ -1,5 +1,26 @@ #!/usr/bin/python # -*- mode: python; coding: utf-8 -*- +# +# Mandos Monitor - Control and monitor the Mandos server +# +# Copyright © 2008-2010 Teddy Hogeborn +# Copyright © 2008-2010 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 . +# from __future__ import division import sys @@ -8,48 +29,50 @@ import locale import datetime import re +import os locale.setlocale(locale.LC_ALL, u'') tablewords = { - 'name': u'Name', - 'enabled': u'Enabled', - 'timeout': u'Timeout', - 'last_checked_ok': u'Last Successful Check', - 'created': u'Created', - 'interval': u'Interval', - 'host': u'Host', - 'fingerprint': u'Fingerprint', - 'checker_running': u'Check Is Running', - 'last_enabled': u'Last Enabled', - 'checker': u'Checker', + 'Name': u'Name', + 'Enabled': u'Enabled', + 'Timeout': u'Timeout', + 'LastCheckedOK': u'Last Successful Check', + 'LastApprovalRequest': u'Last Approval Request', + 'Created': u'Created', + 'Interval': u'Interval', + 'Host': u'Host', + 'Fingerprint': u'Fingerprint', + 'CheckerRunning': u'Check Is Running', + 'LastEnabled': u'Last Enabled', + 'ApprovalPending': u'Approval Is Pending', + 'ApprovedByDefault': u'Approved By Default', + 'ApprovalDelay': u"Approval Delay", + 'ApprovalDuration': u"Approval Duration", + 'Checker': u'Checker', } -defaultkeywords = ('name', 'enabled', 'timeout', 'last_checked_ok', - 'checker') +defaultkeywords = ('Name', 'Enabled', 'Timeout', 'LastCheckedOK') domain = 'se.bsnet.fukt' busname = domain + '.Mandos' server_path = '/' server_interface = domain + '.Mandos' client_interface = domain + '.Mandos.Client' version = "1.0.14" -bus = dbus.SystemBus() -mandos_dbus_objc = bus.get_object(busname, server_path) -mandos_serv = dbus.Interface(mandos_dbus_objc, - dbus_interface = server_interface) -mandos_clients = mandos_serv.GetAllClientsWithProperties() -def datetime_to_milliseconds(dt): - "Return the 'timeout' attribute in milliseconds" - return ((dt.days * 24 * 60 * 60 * 1000) - + (dt.seconds * 1000) - + (dt.microseconds // 1000)) +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 "%s%02d:%02d:%02d" % (("%dT" % td.days) if td.days else "", # days - td.seconds // 3600, # hours - (td.seconds % 3600) // 60, # minutes - (td.seconds % 60)) # seconds + return (u"%(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, + }) def string_to_delta(interval): @@ -92,102 +115,223 @@ timevalue += delta return timevalue -def print_clients(clients): +def print_clients(clients, keywords): def valuetostring(value, keyword): if type(value) is dbus.Boolean: return u"Yes" if value else u"No" - if keyword in ("timeout", "interval"): + if keyword in (u"Timeout", u"Interval", u"ApprovalDelay", + u"ApprovalDuration"): return milliseconds_to_string(value) return unicode(value) + # Create format string to print table rows format_string = u' '.join(u'%%-%ds' % max(len(tablewords[key]), - max(len(valuetostring(client[key], key)) + max(len(valuetostring(client[key], + key)) for client in clients)) for key in keywords) + # Print header line print format_string % tuple(tablewords[key] for key in keywords) for client in clients: print format_string % tuple(valuetostring(client[key], key) for key in keywords) -parser = OptionParser(version = "%%prog %s" % version) -parser.add_option("-a", "--all", action="store_true", - help="Print all fields") -parser.add_option("-e", "--enable", action="store_true", - help="Enable client") -parser.add_option("-d", "--disable", action="store_true", - help="disable client") -parser.add_option("-b", "--bump-timeout", action="store_true", - help="Bump timeout for client") -parser.add_option("--start-checker", action="store_true", - help="Start checker for client") -parser.add_option("--stop-checker", action="store_true", - help="Stop checker for client") -parser.add_option("-V", "--is-valid", action="store_true", - help="Check if client is still valid") -parser.add_option("-r", "--remove", action="store_true", - help="Remove client") -parser.add_option("-c", "--checker", type="string", - help="Set checker command for client") -parser.add_option("-t", "--timeout", type="string", - help="Set timeout for client") -parser.add_option("-i", "--interval", type="string", - help="Set checker interval for client") -parser.add_option("-H", "--host", type="string", - help="Set host for client") -parser.add_option("-s", "--secret", type="string", - help="Set password blob (file) for client") -options, client_names = parser.parse_args() - -# Compile list of clients to process -clients=[] -for name in client_names: - for path, client in mandos_clients.iteritems(): - if client['name'] == name: - client_objc = bus.get_object(busname, path) - clients.append(dbus.Interface(client_objc, - dbus_interface - = client_interface)) - break - else: - print >> sys.stderr, "Client not found on server: %r" % name - sys.exit(1) - -if not clients and mandos_clients.values(): - keywords = defaultkeywords - if options.all: - keywords = ('name', 'enabled', 'timeout', 'last_checked_ok', - 'created', 'interval', 'host', 'fingerprint', - 'checker_running', 'last_enabled', 'checker') - print_clients(mandos_clients.values()) - -# Process each client in the list by all selected options -for client in clients: - if options.remove: - mandos_serv.RemoveClient(client.__dbus_object_path__) - if options.enable: - client.Enable() - if options.disable: - client.Disable() - if options.bump_timeout: - client.BumpTimeout() - if options.start_checker: - client.StartChecker() - if options.stop_checker: - client.StopChecker() - if options.is_valid: - sys.exit(0 if client.IsStillValid() else 1) - if options.checker: - client.SetChecker(options.checker) - if options.host: - client.SetHost(options.host) - if options.interval: - client.SetInterval(datetime_to_milliseconds - (string_to_delta(options.interval))) - if options.timeout: - client.SetTimeout(datetime_to_milliseconds - (string_to_delta(options.timeout))) - if options.secret: - client.SetSecret(dbus.ByteArray(open(options.secret, 'rb').read())) +def has_actions(options): + return any((options.enable, + options.disable, + options.bump_timeout, + options.start_checker, + options.stop_checker, + options.is_enabled, + options.remove, + options.checker is not None, + options.timeout is not None, + options.interval is not None, + options.approved_by_default is not None, + options.approval_delay is not None, + options.approval_duration is not None, + options.host is not None, + options.secret is not None, + options.approve, + options.deny)) + +def main(): + parser = OptionParser(version = "%%prog %s" % version) + parser.add_option("-a", "--all", action="store_true", + help="Select all clients") + parser.add_option("-v", "--verbose", action="store_true", + help="Print all fields") + parser.add_option("-e", "--enable", action="store_true", + help="Enable client") + parser.add_option("-d", "--disable", action="store_true", + help="disable client") + parser.add_option("-b", "--bump-timeout", action="store_true", + help="Bump timeout for client") + parser.add_option("--start-checker", action="store_true", + help="Start checker for client") + parser.add_option("--stop-checker", action="store_true", + help="Stop checker for client") + parser.add_option("-V", "--is-enabled", action="store_true", + help="Check if client is enabled") + parser.add_option("-r", "--remove", action="store_true", + help="Remove client") + parser.add_option("-c", "--checker", type="string", + help="Set checker command for client") + parser.add_option("-t", "--timeout", type="string", + help="Set timeout for client") + parser.add_option("-i", "--interval", type="string", + help="Set checker interval for client") + parser.add_option("--approve-by-default", action="store_true", + dest=u"approved_by_default", + help="Set client to be approved by default") + parser.add_option("--deny-by-default", action="store_false", + dest=u"approved_by_default", + help="Set client to be denied by default") + parser.add_option("--approval-delay", type="string", + help="Set delay before client approve/deny") + parser.add_option("--approval-duration", type="string", + help="Set duration of one client approval") + parser.add_option("-H", "--host", type="string", + help="Set host for client") + parser.add_option("-s", "--secret", type="string", + help="Set password blob (file) for client") + parser.add_option("-A", "--approve", action="store_true", + help="Approve any current client request") + parser.add_option("-D", "--deny", action="store_true", + help="Deny any current client request") + options, client_names = parser.parse_args() + + if has_actions(options) and not client_names and not 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.') + + try: + bus = dbus.SystemBus() + mandos_dbus_objc = bus.get_object(busname, server_path) + except dbus.exceptions.DBusException: + print >> sys.stderr, "Could not connect to Mandos server" + sys.exit(1) + mandos_serv = dbus.Interface(mandos_dbus_objc, + dbus_interface = server_interface) + + #block stderr since dbus library prints to stderr + null = os.open(os.path.devnull, os.O_RDWR) + stderrcopy = os.dup(sys.stderr.fileno()) + os.dup2(null, sys.stderr.fileno()) + os.close(null) + try: + try: + mandos_clients = mandos_serv.GetAllClientsWithProperties() + finally: + #restore stderr + os.dup2(stderrcopy, sys.stderr.fileno()) + os.close(stderrcopy) + except dbus.exceptions.DBusException, e: + print >> sys.stderr, "Access denied: Accessing mandos server through dbus." + sys.exit(1) + + # Compile dict of (clients: properties) to process + clients={} + + if options.all or not client_names: + clients = dict((bus.get_object(busname, path), properties) + for path, properties in + mandos_clients.iteritems()) + else: + for name in client_names: + for path, client in mandos_clients.iteritems(): + if client['Name'] == name: + client_objc = bus.get_object(busname, path) + clients[client_objc] = client + break + else: + print >> sys.stderr, "Client not found on server: %r" % name + 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', + 'LastApprovalRequest', 'ApprovalDelay', + 'ApprovalDuration', 'Checker') + else: + keywords = defaultkeywords + + print_clients(clients.values(), keywords) + else: + # Process each client in the list by all selected options + for client in clients: + if options.remove: + mandos_serv.RemoveClient(client.__dbus_object_path__) + if options.enable: + client.Enable(dbus_interface=client_interface) + if options.disable: + client.Disable(dbus_interface=client_interface) + if options.bump_timeout: + client.CheckedOK(dbus_interface=client_interface) + if options.start_checker: + client.StartChecker(dbus_interface=client_interface) + if options.stop_checker: + client.StopChecker(dbus_interface=client_interface) + if options.is_enabled: + sys.exit(0 if client.Get(client_interface, + u"Enabled", + dbus_interface=dbus.PROPERTIES_IFACE) + else 1) + if options.checker: + client.Set(client_interface, u"Checker", options.checker, + dbus_interface=dbus.PROPERTIES_IFACE) + if options.host: + client.Set(client_interface, u"Host", options.host, + dbus_interface=dbus.PROPERTIES_IFACE) + if options.interval: + client.Set(client_interface, u"Interval", + timedelta_to_milliseconds + (string_to_delta(options.interval)), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.approval_delay: + client.Set(client_interface, u"ApprovalDelay", + timedelta_to_milliseconds + (string_to_delta(options. + approval_delay)), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.approval_duration: + client.Set(client_interface, u"ApprovalDuration", + timedelta_to_milliseconds + (string_to_delta(options. + approval_duration)), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.timeout: + client.Set(client_interface, u"Timeout", + timedelta_to_milliseconds + (string_to_delta(options.timeout)), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.secret: + client.Set(client_interface, u"Secret", + dbus.ByteArray(open(options.secret, + u'rb').read()), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.approved_by_default is not None: + client.Set(client_interface, u"ApprovedByDefault", + dbus.Boolean(options + .approved_by_default), + dbus_interface=dbus.PROPERTIES_IFACE) + if options.approve: + client.Approve(dbus.Boolean(True), + dbus_interface=client_interface) + elif options.deny: + client.Approve(dbus.Boolean(False), + dbus_interface=client_interface) + +if __name__ == '__main__': + main() === added file 'mandos-ctl.xml' --- mandos-ctl.xml 1970-01-01 00:00:00 +0000 +++ mandos-ctl.xml 2010-09-25 23:52:17 +0000 @@ -0,0 +1,570 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@fukt.bsnet.se +
+
+ + Teddy + Hogeborn +
+ teddy@fukt.bsnet.se +
+
+
+ + 2010 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8 + + + + &COMMANDNAME; + + Control the operation of the Mandos server + + + + + + &COMMANDNAME; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CLIENT + + + + + &COMMANDNAME; + + + + + + + CLIENT + + + + + &COMMANDNAME; + + + + + CLIENT + + + &COMMANDNAME; + + + + + + + &COMMANDNAME; + + + + + + + + + DESCRIPTION + + &COMMANDNAME; is a program to control the + operation of the Mandos server mandos8. + + + This program can be used to change client settings, approve or + deny client requests, and to remove clients from the server. + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OPTIONS + + + + + + + + Show a help message and exit + + + + + + + + + + Enable client(s). An enabled client will be eligble to + receive its secret. + + + + + + + + + + Disable client(s). A disabled client will not be eligble + to receive its secret, and no checkers will be started for + it. + + + + + + + + + Bump the timeout of the specified client(s), just as if a + checker had completed successfully for it/them. + + + + + + + + + Start a new checker now for the specified client(s). + + + + + + + + + Stop any running checker for the specified client(s). + + + + + + + + + + Remove the specified client(s) from the server. + + + + + + + + + + Set the checker option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Set the timeout option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Set the interval option of the + specified client(s); see mandos-clients.conf5. + + + + + + + + + + Set the approved_by_default option of + the specified client(s) to True or + False, respectively; see + mandos-clients.conf5. + + + + + + + + + Set the approval_delay option of the + specified client(s); see mandos-clients.conf5. + + + + + + + + + Set the approval_duration option of the + specified client(s); see mandos-clients.conf5. + + + + + + + + + + Set the host option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Set the secfile option of the specified + client(s); see mandos-clients.conf5. + + + + + + + + + + Approve client(s) if currently waiting for approval. + + + + + + + + + + Deny client(s) if currently waiting for approval. + + + + + + + + + + Make the client-modifying options modify all clients. + + + + + + + + + + Show all client settings, not just a subset. + + + + + + + + + + Check if a single client is enabled or not, and exit with + a successful exit status only if the client is enabled. + + + + + + + + + OVERVIEW + + + This program is a small utility to generate new OpenPGP keys for + new Mandos clients, and to generate sections for inclusion in + clients.conf on the server. + + + + + EXIT STATUS + + If the option is used, the exit + status will be 0 only if the specified client is enabled. + + + + + + + + + + + EXAMPLE + + + To list all clients: + + + &COMMANDNAME; + + + + + + To list all settings for the clients + named foo1.example.org and foo2.example.org: + + + + +&COMMANDNAME; --verbose foo1.example.org foo2.example.org + + + + + + + To enable all clients: + + + &COMMANDNAME; --enable --all + + + + + + To change timeout and interval value for the clients + named foo1.example.org and foo2.example.org: + + + + +&COMMANDNAME; --timeout="5m" --interval="1m" foo1.example.org foo2.example.org + + + + + + + To approve all clients currently waiting for it: + + + &COMMANDNAME; --approve --all + + + + + + SECURITY + + This program must be permitted to access the Mandos server via + the D-Bus interface. This normally requires the root user, but + could be configured otherwise by reconfiguring the D-Bus server. + + + + + SEE ALSO + + mandos + 8, + mandos-clients.conf + 5, + mandos-monitor + 8 + + + +
+ + + + + === modified file 'mandos-keygen' --- mandos-keygen 2009-10-25 19:16:56 +0000 +++ mandos-keygen 2010-09-26 18:32:58 +0000 @@ -2,8 +2,8 @@ # # Mandos key generator - create a new OpenPGP key for a Mandos client # -# Copyright © 2008,2009 Teddy Hogeborn -# Copyright © 2008,2009 Björn Påhlsson +# Copyright © 2008-2010 Teddy Hogeborn +# Copyright © 2008-2010 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 @@ -217,12 +217,27 @@ %commit EOF + if tty --quiet; then + cat <<-EOF + Note: Due to entropy requirements, key generation could take + anything from a few minutes to SEVERAL HOURS. Please be + patient and/or supply the system with more entropy if needed. + EOF + echo -n "Started: " + date + fi + # Generate a new key in the key rings gpg --quiet --batch --no-tty --no-options --enable-dsa2 \ --homedir "$RINGDIR" --trust-model always \ --gen-key "$BATCHFILE" rm --force "$BATCHFILE" + if tty --quiet; then + echo -n "Finished: " + date + fi + # Backup any old key files if cp --backup=numbered --force "$SECKEYFILE" "$SECKEYFILE" \ 2>/dev/null; then @@ -278,12 +293,13 @@ stty -echo echo -n "Enter passphrase: " >&2 first="$(head --lines=1 | tr --delete '\n')" - echo -n -e "\nRepeat passphrase: " >&2 + echo >&2 + echo -n "Repeat passphrase: " >&2 second="$(head --lines=1 | tr --delete '\n')" echo >&2 stty echo if [ "$first" != "$second" ]; then - echo -e "Passphrase mismatch" >&2 + echo "Passphrase mismatch" >&2 touch "$RINGDIR"/mismatch else echo -n "$first" === added file 'mandos-monitor' --- mandos-monitor 1970-01-01 00:00:00 +0000 +++ mandos-monitor 2010-09-27 16:27:13 +0000 @@ -0,0 +1,733 @@ +#!/usr/bin/python +# -*- mode: python; coding: utf-8 -*- +# +# Mandos Monitor - Control and monitor the Mandos server +# +# Copyright © 2009,2010 Teddy Hogeborn +# Copyright © 2009,2010 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 . +# + +from __future__ import division, absolute_import, with_statement + +import sys +import os +import signal + +import datetime + +import urwid.curses_display +import urwid + +from dbus.mainloop.glib import DBusGMainLoop +import gobject + +import dbus + +import UserList + +import locale + +locale.setlocale(locale.LC_ALL, u'') + +import logging +logging.getLogger('dbus.proxies').setLevel(logging.CRITICAL) + +# Some useful constants +domain = 'se.bsnet.fukt' +server_interface = domain + '.Mandos' +client_interface = domain + '.Mandos.Client' +version = "1.0.15" + +# 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) + +def isoformat_to_datetime(iso): + "Parse an ISO 8601 date string to a datetime.datetime()" + if not iso: + return None + d, t = iso.split(u"T", 1) + year, month, day = d.split(u"-", 2) + hour, minute, second = t.split(u":", 2) + second, fraction = divmod(float(second), 1) + return datetime.datetime(int(year), + int(month), + int(day), + int(hour), + int(minute), + int(second), # Whole seconds + int(fraction*1000000)) # Microseconds + +class MandosClientPropertyCache(object): + """This wraps a Mandos Client D-Bus proxy object, caches the + properties and calls a hook function when any of them are + changed. + """ + def __init__(self, proxy_object=None, *args, **kwargs): + self.proxy = proxy_object # Mandos Client proxy object + + self.properties = dict() + self.proxy.connect_to_signal(u"PropertyChanged", + self.property_changed, + client_interface, + byte_arrays=True) + + self.properties.update( + self.proxy.GetAll(client_interface, + dbus_interface = dbus.PROPERTIES_IFACE)) + + #XXX This break good super behaviour! +# super(MandosClientPropertyCache, self).__init__( +# *args, **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. + """ + # Update properties dict with new value + self.properties[property] = value + + +class MandosClientWidget(urwid.FlowWidget, MandosClientPropertyCache): + """A Mandos Client which is visible on the screen. + """ + + def __init__(self, server_proxy_object=None, update_hook=None, + delete_hook=None, logger=None, *args, **kwargs): + # Called on update + self.update_hook = update_hook + # Called on delete + self.delete_hook = delete_hook + # Mandos Server proxy object + self.server_proxy_object = server_proxy_object + # Logger + 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(u"") + # The widget shown when we have focus + self._focus_text_widget = urwid.Text(u"") + super(MandosClientWidget, self).__init__( + update_hook=update_hook, delete_hook=delete_hook, + *args, **kwargs) + self.update() + self.opened = False + + last_checked_ok = isoformat_to_datetime(self.properties + [u"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 + [u"Interval"])) + + if self.last_checker_failed: + self.using_timer(True) + + if self.need_approval: + self.using_timer(True) + + self.proxy.connect_to_signal(u"CheckerCompleted", + self.checker_completed, + client_interface, + byte_arrays=True) + self.proxy.connect_to_signal(u"CheckerStarted", + self.checker_started, + client_interface, + byte_arrays=True) + self.proxy.connect_to_signal(u"GotSecret", + self.got_secret, + client_interface, + byte_arrays=True) + self.proxy.connect_to_signal(u"NeedApproval", + self.need_approval, + client_interface, + byte_arrays=True) + self.proxy.connect_to_signal(u"Rejected", + self.rejected, + client_interface, + byte_arrays=True) + + def property_changed(self, property=None, value=None): + super(self, MandosClientWidget).property_changed(property, + value) + if property == u"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: + self._update_timer_callback_tag = (gobject.timeout_add + (1000, + self.update_timer)) + elif old and self._update_timer_callback_lock == 0: + 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(u'Checker for client %s (command "%s")' + # u' was successful' + # % (self.properties[u"Name"], command)) + 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(u'Checker for client %s (command "%s")' + u' failed with exit code %s' + % (self.properties[u"Name"], command, + os.WEXITSTATUS(condition))) + elif os.WIFSIGNALED(condition): + self.logger(u'Checker for client %s (command "%s")' + u' was killed by signal %s' + % (self.properties[u"Name"], command, + os.WTERMSIG(condition))) + elif os.WCOREDUMP(condition): + self.logger(u'Checker for client %s (command "%s")' + u' dumped core' + % (self.properties[u"Name"], command)) + else: + self.logger(u'Checker for client %s completed' + u' mysteriously') + self.update() + + def checker_started(self, command): + #self.logger(u'Client %s started checker "%s"' + # % (self.properties[u"Name"], unicode(command))) + pass + + def got_secret(self): + self.last_checker_failed = False + self.logger(u'Client %s received its secret' + % self.properties[u"Name"]) + + def need_approval(self, timeout, default): + if not default: + message = u'Client %s needs approval within %s seconds' + else: + message = u'Client %s will get its secret in %s seconds' + self.logger(message + % (self.properties[u"Name"], timeout/1000)) + self.using_timer(True) + + def rejected(self, reason): + self.logger(u'Client %s was rejected; reason: %s' + % (self.properties[u"Name"], reason)) + + def selectable(self): + """Make this a "selectable" widget. + This overrides the method from urwid.FlowWidget.""" + return True + + def rows(self, (maxcol,), focus=False): + """How many rows this widget will occupy might depend on + whether we have focus or not. + This overrides the method from urwid.FlowWidget""" + return self.current_widget(focus).rows((maxcol,), focus=focus) + + def current_widget(self, focus=False): + if focus or self.opened: + return self._focus_widget + return self._widget + + def update(self): + "Called when what is visible on the screen should be updated." + # How to add standout mode to a style + with_standout = { u"normal": u"standout", + u"bold": u"bold-standout", + u"underline-blink": + u"underline-blink-standout", + u"bold-underline-blink": + u"bold-underline-blink-standout", + } + + # Rebuild focus and non-focus widgets using current properties + + # Base part of a client. Name! + base = (u'%(name)s: ' + % {u"name": self.properties[u"Name"]}) + if not self.properties[u"Enabled"]: + message = u"DISABLED" + elif self.properties[u"ApprovalPending"]: + timeout = datetime.timedelta(milliseconds + = self.properties + [u"ApprovalDelay"]) + last_approval_request = isoformat_to_datetime( + self.properties[u"LastApprovalRequest"]) + if last_approval_request is not None: + timer = timeout - (datetime.datetime.utcnow() + - last_approval_request) + else: + timer = datetime.timedelta() + if self.properties[u"ApprovedByDefault"]: + message = u"Approval in %s. (d)eny?" + else: + message = u"Denial in %s. (a)pprove?" + message = message % unicode(timer).rsplit(".", 1)[0] + elif self.last_checker_failed: + timeout = datetime.timedelta(milliseconds + = self.properties + [u"Timeout"]) + last_ok = isoformat_to_datetime( + max((self.properties[u"LastCheckedOK"] + or self.properties[u"Created"]), + self.properties[u"LastEnabled"])) + timer = timeout - (datetime.datetime.utcnow() - last_ok) + message = (u'A checker has failed! Time until client' + u' gets disabled: %s' + % unicode(timer).rsplit(".", 1)[0]) + else: + message = u"enabled" + self._text = "%s%s" % (base, message) + + if not urwid.supports_unicode(): + self._text = self._text.encode("ascii", "replace") + textlist = [(u"normal", self._text)] + self._text_widget.set_text(textlist) + self._focus_text_widget.set_text([(with_standout[text[0]], + text[1]) + if isinstance(text, tuple) + else text + for text in textlist]) + self._widget = self._text_widget + self._focus_widget = urwid.AttrWrap(self._focus_text_widget, + "standout") + # Run update hook, if any + if self.update_hook is not None: + self.update_hook() + + def update_timer(self): + "called by gobject" + self.update() + return True # Keep calling this + + def delete(self): + if self._update_timer_callback_tag is not None: + gobject.source_remove(self._update_timer_callback_tag) + self._update_timer_callback_tag = None + if self.delete_hook is not None: + self.delete_hook(self) + + def render(self, (maxcol,), focus=False): + """Render differently if we have focus. + This overrides the method from urwid.FlowWidget""" + return self.current_widget(focus).render((maxcol,), + focus=focus) + + def keypress(self, (maxcol,), key): + """Handle keys. + This overrides the method from urwid.FlowWidget""" + if key == u"+": + self.proxy.Enable(dbus_interface = client_interface) + elif key == u"-": + self.proxy.Disable(dbus_interface = client_interface) + elif key == u"a": + self.proxy.Approve(dbus.Boolean(True, variant_level=1), + dbus_interface = client_interface) + elif key == u"d": + self.proxy.Approve(dbus.Boolean(False, variant_level=1), + dbus_interface = client_interface) + elif key == u"R" or key == u"_" or key == u"ctrl k": + self.server_proxy_object.RemoveClient(self.proxy + .object_path) + elif key == u"s": + self.proxy.StartChecker(dbus_interface = client_interface) + elif key == u"S": + self.proxy.StopChecker(dbus_interface = client_interface) + elif key == u"C": + self.proxy.CheckedOK(dbus_interface = client_interface) + # xxx +# elif key == u"p" or key == "=": +# self.proxy.pause() +# elif key == u"u" or key == ":": +# self.proxy.unpause() +# elif key == u"RET": +# self.open() + else: + return key + + def property_changed(self, property=None, value=None, + *args, **kwargs): + """Call self.update() if old value is not new value. + 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: + self.update() + + +class ConstrainedListBox(urwid.ListBox): + """Like a normal urwid.ListBox, but will consume all "up" or + "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, (maxcol, maxrow), key): + ret = super(ConstrainedListBox, self).keypress((maxcol, + maxrow), key) + if ret in (u"up", u"down"): + return + return ret + + +class UserInterface(object): + """This is the entire user interface - the whole screen + with boxes, lists of client widgets, etc. + """ + def __init__(self, max_log_length=1000): + DBusGMainLoop(set_as_default=True) + + self.screen = urwid.curses_display.Screen() + + self.screen.register_palette(( + (u"normal", + u"default", u"default", None), + (u"bold", + u"default", u"default", u"bold"), + (u"underline-blink", + u"default", u"default", u"underline"), + (u"standout", + u"default", u"default", u"standout"), + (u"bold-underline-blink", + u"default", u"default", (u"bold", u"underline")), + (u"bold-standout", + u"default", u"default", (u"bold", u"standout")), + (u"underline-blink-standout", + u"default", u"default", (u"underline", u"standout")), + (u"bold-underline-blink-standout", + u"default", u"default", (u"bold", u"underline", + u"standout")), + )) + + if urwid.supports_unicode(): + self.divider = u"─" # \u2500 + #self.divider = u"━" # \u2501 + else: + #self.divider = u"-" # \u002d + self.divider = u"_" # \u005f + + self.screen.start() + + self.size = self.screen.get_cols_rows() + + self.clients = urwid.SimpleListWalker([]) + self.clients_dict = {} + + # We will add Text widgets to this list + self.log = [] + self.max_log_length = max_log_length + + # 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) + + # This keeps track of whether self.uilist currently has + # self.logbox in it or not + self.log_visible = True + self.log_wrap = u"any" + + self.rebuild() + self.log_message_raw((u"bold", + u"Mandos Monitor version " + version)) + self.log_message_raw((u"bold", + u"q: Quit ?: Help")) + + self.busname = domain + '.Mandos' + self.main_loop = gobject.MainLoop() + self.bus = dbus.SystemBus() + mandos_dbus_objc = self.bus.get_object( + self.busname, u"/", 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(u"ClientRemoved", + self.find_and_remove_client, + dbus_interface=server_interface, + byte_arrays=True)) + (self.mandos_serv + .connect_to_signal(u"ClientAdded", + self.add_new_client, + dbus_interface=server_interface, + byte_arrays=True)) + (self.mandos_serv + .connect_to_signal(u"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((u"Client with address %s and fingerprint %s" + u" could not be found" % (address, + fingerprint))) + + def rebuild(self): + """This rebuilds the User Interface. + Call this when the widget layout needs to change""" + self.uilist = [] + #self.uilist.append(urwid.ListBox(self.clients)) + self.uilist.append(urwid.Frame(ConstrainedListBox(self. + clients), + #header=urwid.Divider(), + header=None, + footer= + urwid.Divider(div_char= + self.divider))) + if self.log_visible: + self.uilist.append(self.logbox) + pass + self.topwidget = urwid.Pile(self.uilist) + + def log_message(self, message): + timestamp = datetime.datetime.now().isoformat() + self.log_message_raw(timestamp + u": " + message) + + def log_message_raw(self, markup): + """Add a log message to the log buffer.""" + self.log.append(urwid.Text(markup, wrap=self.log_wrap)) + if (self.max_log_length + and len(self.log) > self.max_log_length): + del self.log[0:len(self.log)-self.max_log_length-1] + self.logbox.set_focus(len(self.logbox.body.contents), + coming_from=u"above") + self.refresh() + + def toggle_log_display(self): + """Toggle visibility of the log buffer.""" + self.log_visible = not self.log_visible + self.rebuild() + #self.log_message(u"Log visibility changed to: " + # + unicode(self.log_visible)) + + def change_log_display(self): + """Change type of log display. + Currently, this toggles wrapping of text lines.""" + if self.log_wrap == u"clip": + self.log_wrap = u"any" + else: + self.log_wrap = u"clip" + for textwidget in self.log: + textwidget.set_wrap_mode(self.log_wrap) + #self.log_message(u"Wrap mode: " + self.log_wrap) + + def find_and_remove_client(self, path, name): + """Find an client from its object path and remove it. + + This is connected to the ClientRemoved signal from the + Mandos server object.""" + try: + client = self.clients_dict[path] + except KeyError: + # not found? + return + self.remove_client(client, path) + + def add_new_client(self, path): + 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, + update_hook + =self.refresh, + delete_hook + =self.remove_client, + logger + =self.log_message), + path=path) + + def add_client(self, client, path=None): + self.clients.append(client) + if path is None: + path = client.proxy.object_path + self.clients_dict[path] = client + self.clients.sort(None, lambda c: c.properties[u"Name"]) + self.refresh() + + def remove_client(self, client, path=None): + self.clients.remove(client) + 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): + """Redraw the screen""" + canvas = self.topwidget.render(self.size, focus=True) + self.screen.draw_screen(self.size, canvas) + + def run(self): + """Start the main loop and exit when it's done.""" + self.refresh() + self._input_callback_tag = (gobject.io_add_watch + (sys.stdin.fileno(), + gobject.IO_IN, + self.process_input)) + self.main_loop.run() + # Main loop has finished, we should close everything now + gobject.source_remove(self._input_callback_tag) + self.screen.stop() + + def stop(self): + self.main_loop.quit() + + def process_input(self, source, condition): + keys = self.screen.get_input() + translations = { u"ctrl n": u"down", # Emacs + u"ctrl p": u"up", # Emacs + u"ctrl v": u"page down", # Emacs + u"meta v": u"page up", # Emacs + u" ": u"page down", # less + u"f": u"page down", # less + u"b": u"page up", # less + u"j": u"down", # vi + u"k": u"up", # vi + } + for key in keys: + try: + key = translations[key] + except KeyError: # :-) + pass + + if key == u"q" or key == u"Q": + self.stop() + break + elif key == u"window resize": + self.size = self.screen.get_cols_rows() + self.refresh() + elif key == u"\f": # Ctrl-L + self.refresh() + elif key == u"l" or key == u"D": + self.toggle_log_display() + self.refresh() + elif key == u"w" or key == u"i": + self.change_log_display() + self.refresh() + elif key == u"?" or key == u"f1" or key == u"esc": + if not self.log_visible: + self.log_visible = True + self.rebuild() + self.log_message_raw((u"bold", + u" ". + join((u"q: Quit", + u"?: Help", + u"l: Log window toggle", + u"TAB: Switch window", + u"w: Wrap (log)")))) + self.log_message_raw((u"bold", + u" " + .join((u"Clients:", + u"+: Enable", + u"-: Disable", + u"R: Remove", + u"s: Start new checker", + u"S: Stop checker", + u"C: Checker OK", + u"a: Approve", + u"d: Deny")))) + self.refresh() + elif key == u"tab": + if self.topwidget.get_focus() is self.logbox: + self.topwidget.set_focus(0) + else: + self.topwidget.set_focus(self.logbox) + self.refresh() + #elif (key == u"end" or key == u"meta >" or key == u"G" + # or key == u">"): + # pass # xxx end-of-buffer + #elif (key == u"home" or key == u"meta <" or key == u"g" + # or key == u"<"): + # pass # xxx beginning-of-buffer + #elif key == u"ctrl e" or key == u"$": + # pass # xxx move-end-of-line + #elif key == u"ctrl a" or key == u"^": + # pass # xxx move-beginning-of-line + #elif key == u"ctrl b" or key == u"meta (" or key == u"h": + # pass # xxx left + #elif key == u"ctrl f" or key == u"meta )" or key == u"l": + # pass # xxx right + #elif key == u"a": + # pass # scroll up log + #elif key == u"z": + # pass # scroll down log + elif self.topwidget.selectable(): + self.topwidget.keypress(self.size, key) + self.refresh() + return True + +ui = UserInterface() +try: + ui.run() +except KeyboardInterrupt: + ui.screen.stop() +except Exception, e: + ui.log_message(unicode(e)) + ui.screen.stop() + raise === added file 'mandos-monitor.xml' --- mandos-monitor.xml 1970-01-01 00:00:00 +0000 +++ mandos-monitor.xml 2010-09-14 18:22:03 +0000 @@ -0,0 +1,233 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@fukt.bsnet.se +
+
+ + Teddy + Hogeborn +
+ teddy@fukt.bsnet.se +
+
+
+ + 2010 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8 + + + + &COMMANDNAME; + + Text-based GUI to control the Mandos server. + + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + &COMMANDNAME; is an interactive program to + monitor and control the operations of the Mandos server (see + mandos8). + + + + + PURPOSE + + The purpose of this is to enable remote and unattended + rebooting of client host computer with an + encrypted root file system. See for details. + + + + + OVERVIEW + + + This program is used to monitor and control the Mandos server. + In particular, it can be used to approve Mandos clients which + have been configured to require approval. It also shows all + significant events reported by the Mandos server. + + + + + KEYS + + This program is used to monitor and control the Mandos server. + In particular, it can be used to approve Mandos clients which + have been configured to require approval. It also shows all + significant events reported by the Mandos server. + + + Global Keys + + Keys + Function + + + + q, Q + Quit + + + Ctrl-L + Redraw screen + + + ?, F1 + Show help + + + l, D + Toggle log window + + + TAB + Switch window + + + w, i + Toggle log window line wrap + + + Up, Ctrl-P, k + Move up a line + + + Down, Ctrl-N, j + Move down a line + + + PageUp, Meta-V, b + Move up a page + + + PageDown, Ctrl-V, SPACE, f + Move down a page + +
+ + Client List Keys + + Keys + Function + + + + + + Enable client + + + - + Disable client + + + a + Approve client + + + d + Deny client + + + r, _, Ctrl-K + Remove client + + + s + Start checker for client + + + S + Stop checker for client + + + C + Force a successful check for this client. + +
+
+ + + BUGS + + This program can currently only be used to monitor and control a + Mandos server with the default D-Bus service name of + Mandos. + + + + + EXAMPLE + + + This program takes no options: + + + &COMMANDNAME; + + + + + + SECURITY + + This program must be permitted to access the Mandos server via + the D-Bus interface. This normally requires the root user, but + could be configured otherwise by reconfiguring the D-Bus server. + + + + + SEE ALSO + + mandos + 8, + mandos-ctl + 8 + + + +
+ + + + + === modified file 'mandos.lsm' --- mandos.lsm 2009-10-25 19:16:56 +0000 +++ mandos.lsm 2010-09-14 18:22:03 +0000 @@ -1,7 +1,7 @@ Begin4 Title: Mandos Version: 1.0.14 -Entered-date: 2009-10-25 +Entered-date: 2010-09-14 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. === modified file 'mandos.xml' --- mandos.xml 2009-09-17 11:47:22 +0000 +++ mandos.xml 2010-09-27 17:39:03 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -33,6 +33,7 @@ 2008 2009 + 2010 Teddy Hogeborn Björn Påhlsson @@ -86,6 +87,11 @@ + + + + @@ -192,6 +198,24 @@ + + + + Set the debugging log level. + LEVEL is a string, one of + CRITICAL, + ERROR, + WARNING, + INFO, or + DEBUG, in order of + increasing verbosity. The default level is + WARNING. + + + + + @@ -233,6 +257,16 @@ + + + + + See also . + + + + + @@ -326,6 +360,26 @@
+ + APPROVAL + + The server can be configured to require manual approval for a + client before it is sent its secret. The delay to wait for such + approval and the default action (approve or deny) can be + configured both globally and per client; see + mandos-clients.conf + 5. By default all clients + will be approved immediately without delay. + + + This can be used to deny a client its secret if not manually + approved within a specified time. It can also be used to make + the server delay before giving a client its secret, allowing + optional manual denying of this specific client. + + + + LOGGING @@ -336,6 +390,16 @@ + + D-BUS INTERFACE + + The server will by default provide a D-Bus system bus interface. + This interface will only be accessible by the root user or a + Mandos-specific user, if such a user exists. For documentation + of the D-Bus API, see the file DBUS-API. + + + EXIT STATUS @@ -396,8 +460,8 @@ /var/run/mandos.pid - The file containing the process id of - &COMMANDNAME;. + The file containing the process id of the + &COMMANDNAME; process started last. @@ -431,15 +495,9 @@ backtrace. This could be considered a feature. - Currently, if a client is declared invalid due to - having timed out, the server does not record this fact onto - permanent storage. This has some security implications, see - . - - - There is currently no way of querying the server of the current - status of clients, other than analyzing its syslog output. + 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. @@ -527,19 +585,18 @@ If a client is compromised, its downtime should be duly noted - by the server which would therefore declare the client - invalid. 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 valid, 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. + 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 === modified file 'plugin-runner.c' --- plugin-runner.c 2009-09-10 06:28:14 +0000 +++ plugin-runner.c 2010-09-26 21:27:28 +0000 @@ -2,8 +2,8 @@ /* * Mandos plugin runner - Run Mandos plugins * - * Copyright © 2008,2009 Teddy Hogeborn - * Copyright © 2008,2009 Björn Påhlsson + * Copyright © 2008-2010 Teddy Hogeborn + * Copyright © 2008-2010 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,14 +23,14 @@ */ #define _GNU_SOURCE /* TEMP_FAILURE_RETRY(), getline(), - asprintf() */ + asprintf(), O_CLOEXEC */ #include /* size_t, NULL */ -#include /* malloc(), exit(), EXIT_FAILURE, - EXIT_SUCCESS, realloc() */ +#include /* malloc(), exit(), EXIT_SUCCESS, + realloc() */ #include /* bool, true, false */ -#include /* perror, fileno(), fprintf(), +#include /* fileno(), fprintf(), stderr, STDOUT_FILENO */ -#include /* DIR, opendir(), stat(), struct +#include /* DIR, fdopendir(), stat(), struct stat, waitpid(), WIFEXITED(), WEXITSTATUS(), wait(), pid_t, uid_t, gid_t, getuid(), getgid(), @@ -42,7 +42,7 @@ WCOREDUMP() */ #include /* struct stat, stat(), S_ISREG() */ #include /* and, or, not */ -#include /* DIR, struct dirent, opendir(), +#include /* DIR, struct dirent, fdopendir(), readdir(), closedir(), dirfd() */ #include /* struct stat, stat(), S_ISREG(), fcntl(), setuid(), setgid(), @@ -54,7 +54,7 @@ #include /* fcntl(), F_GETFD, F_SETFD, FD_CLOEXEC */ #include /* strsep, strlen(), asprintf(), - strsignal() */ + strsignal(), strcmp(), strncmp() */ #include /* errno */ #include /* struct argp_option, struct argp_state, struct argp, @@ -68,6 +68,10 @@ */ #include /* errno, EBADF */ #include /* intmax_t, PRIdMAX, strtoimax() */ +#include /* EX_OSERR, EX_USAGE, EX_IOERR, + EX_CONFIG, EX_UNAVAILABLE, EX_OK */ +#include /* errno */ +#include /* error() */ #define BUFFER_SIZE 256 @@ -102,7 +106,7 @@ /* Gets an existing plugin based on name, or if none is found, creates a new one */ static plugin *getplugin(char *name){ - /* Check for exiting plugin with that name */ + /* Check for existing plugin with that name */ for(plugin *p = plugin_list; p != NULL; p = p->next){ if((p->name == name) or (p->name and name and (strcmp(p->name, name) == 0))){ @@ -123,7 +127,9 @@ copy_name = strdup(name); } while(copy_name == NULL and errno == EINTR); if(copy_name == NULL){ + int e = errno; free(new_plugin); + errno = e; return NULL; } } @@ -137,8 +143,10 @@ new_plugin->argv = malloc(sizeof(char *) * 2); } while(new_plugin->argv == NULL and errno == EINTR); if(new_plugin->argv == NULL){ + int e = errno; free(copy_name); free(new_plugin); + errno = e; return NULL; } new_plugin->argv[0] = copy_name; @@ -148,9 +156,11 @@ new_plugin->environ = malloc(sizeof(char *)); } while(new_plugin->environ == NULL and errno == EINTR); if(new_plugin->environ == NULL){ + int e = errno; free(copy_name); free(new_plugin->argv); free(new_plugin); + errno = e; return NULL; } new_plugin->environ[0] = NULL; @@ -258,7 +268,7 @@ /* No child processes */ break; } - perror("waitpid"); + error(0, errno, "waitpid"); } /* A child exited, find it in process_list */ @@ -349,14 +359,14 @@ sigemptyset(&sigchld_action.sa_mask); ret = sigaddset(&sigchld_action.sa_mask, SIGCHLD); if(ret == -1){ - perror("sigaddset"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigaddset"); + exitstatus = EX_OSERR; goto fallback; } ret = sigaction(SIGCHLD, &sigchld_action, &old_sigchld_action); if(ret == -1){ - perror("sigaction"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; goto fallback; } @@ -394,119 +404,137 @@ .doc = "Group ID the plugins will run as", .group = 3 }, { .name = "debug", .key = 132, .doc = "Debug mode", .group = 4 }, + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + { .name = "help", .key = '?', + .doc = "Give this help list", .group = -1 }, + { .name = "usage", .key = -3, + .doc = "Give a short usage message", .group = -1 }, + { .name = "version", .key = 'V', + .doc = "Print program version", .group = -1 }, { .name = NULL } }; - error_t parse_opt(int key, char *arg, __attribute__((unused)) - struct argp_state *state){ + error_t parse_opt(int key, char *arg, struct argp_state *state){ + errno = 0; switch(key){ char *tmp; intmax_t tmpmax; case 'g': /* --global-options */ - if(arg != NULL){ + { char *plugin_option; while((plugin_option = strsep(&arg, ",")) != NULL){ - if(plugin_option[0] == '\0'){ - continue; - } if(not add_argument(getplugin(NULL), plugin_option)){ - perror("add_argument"); - return ARGP_ERR_UNKNOWN; + break; } } } break; case 'G': /* --global-env */ - if(arg == NULL){ - break; - } - if(not add_environment(getplugin(NULL), arg, true)){ - perror("add_environment"); - } + add_environment(getplugin(NULL), arg, true); break; case 'o': /* --options-for */ - if(arg != NULL){ - char *plugin_name = strsep(&arg, ":"); - if(plugin_name[0] == '\0'){ - break; - } - char *plugin_option; - while((plugin_option = strsep(&arg, ",")) != NULL){ - if(not add_argument(getplugin(plugin_name), plugin_option)){ - perror("add_argument"); - return ARGP_ERR_UNKNOWN; + { + char *option_list = strchr(arg, ':'); + if(option_list == NULL){ + argp_error(state, "No colon in \"%s\"", arg); + errno = EINVAL; + break; + } + *option_list = '\0'; + option_list++; + if(arg[0] == '\0'){ + argp_error(state, "Empty plugin name"); + errno = EINVAL; + break; + } + char *option; + while((option = strsep(&option_list, ",")) != NULL){ + if(not add_argument(getplugin(arg), option)){ + break; } } } break; case 'E': /* --env-for */ - if(arg == NULL){ - break; - } { char *envdef = strchr(arg, ':'); if(envdef == NULL){ + argp_error(state, "No colon in \"%s\"", arg); + errno = EINVAL; break; } *envdef = '\0'; - if(not add_environment(getplugin(arg), envdef+1, true)){ - perror("add_environment"); + envdef++; + if(arg[0] == '\0'){ + argp_error(state, "Empty plugin name"); + errno = EINVAL; + break; } + add_environment(getplugin(arg), envdef, true); } break; case 'd': /* --disable */ - if(arg != NULL){ + { plugin *p = getplugin(arg); - if(p == NULL){ - return ARGP_ERR_UNKNOWN; + if(p != NULL){ + p->disabled = true; } - p->disabled = true; } break; case 'e': /* --enable */ - if(arg != NULL){ + { plugin *p = getplugin(arg); - if(p == NULL){ - return ARGP_ERR_UNKNOWN; + if(p != NULL){ + p->disabled = false; } - p->disabled = false; } break; case 128: /* --plugin-dir */ free(plugindir); plugindir = strdup(arg); - if(plugindir == NULL){ - perror("strdup"); - } break; case 129: /* --config-file */ /* This is already done by parse_opt_config_file() */ break; case 130: /* --userid */ - errno = 0; tmpmax = strtoimax(arg, &tmp, 10); if(errno != 0 or tmp == arg or *tmp != '\0' or tmpmax != (uid_t)tmpmax){ - fprintf(stderr, "Bad user ID number: \"%s\", using %" - PRIdMAX "\n", arg, (intmax_t)uid); - } else { - uid = (uid_t)tmpmax; + argp_error(state, "Bad user ID number: \"%s\", using %" + PRIdMAX, arg, (intmax_t)uid); + break; } + uid = (uid_t)tmpmax; break; case 131: /* --groupid */ - errno = 0; tmpmax = strtoimax(arg, &tmp, 10); if(errno != 0 or tmp == arg or *tmp != '\0' or tmpmax != (gid_t)tmpmax){ - fprintf(stderr, "Bad group ID number: \"%s\", using %" - PRIdMAX "\n", arg, (intmax_t)gid); - } else { - gid = (gid_t)tmpmax; + argp_error(state, "Bad group ID number: \"%s\", using %" + PRIdMAX, arg, (intmax_t)gid); + break; } + gid = (gid_t)tmpmax; break; case 132: /* --debug */ debug = true; break; + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + case '?': /* --help */ + state->flags &= ~(unsigned int)ARGP_NO_EXIT; /* force exit */ + argp_state_help(state, state->out_stream, ARGP_HELP_STD_HELP); + case -3: /* --usage */ + state->flags &= ~(unsigned int)ARGP_NO_EXIT; /* force exit */ + argp_state_help(state, state->out_stream, + ARGP_HELP_USAGE | ARGP_HELP_EXIT_OK); + case 'V': /* --version */ + fprintf(state->out_stream, "%s\n", argp_program_version); + exit(EXIT_SUCCESS); + break; /* * When adding more options before this line, remember to also add a * "case" to the "parse_opt_config_file" function below. @@ -515,16 +543,13 @@ /* Cryptsetup always passes an argument, which is an empty string if "none" was specified in /etc/crypttab. So if argument was empty, we ignore it silently. */ - if(arg[0] != '\0'){ - fprintf(stderr, "Ignoring unknown argument \"%s\"\n", arg); + if(arg[0] == '\0'){ + break; } - break; - case ARGP_KEY_END: - break; default: return ARGP_ERR_UNKNOWN; } - return 0; + return errno; /* Set to 0 at start */ } /* This option parser is the same as parse_opt() above, except it @@ -532,6 +557,7 @@ error_t parse_opt_config_file(int key, char *arg, __attribute__((unused)) struct argp_state *state){ + errno = 0; switch(key){ case 'g': /* --global-options */ case 'G': /* --global-env */ @@ -544,20 +570,19 @@ case 129: /* --config-file */ free(argfile); argfile = strdup(arg); - if(argfile == NULL){ - perror("strdup"); - } break; case 130: /* --userid */ case 131: /* --groupid */ case 132: /* --debug */ + case '?': /* --help */ + case -3: /* --usage */ + case 'V': /* --version */ case ARGP_KEY_ARG: - case ARGP_KEY_END: break; default: return ARGP_ERR_UNKNOWN; } - return 0; + return errno; } struct argp argp = { .options = options, @@ -567,10 +592,20 @@ /* Parse using parse_opt_config_file() in order to get the custom config file location, if any. */ - ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, 0, NULL); - if(ret == ARGP_ERR_UNKNOWN){ - fprintf(stderr, "Unknown error while parsing arguments\n"); - exitstatus = EXIT_FAILURE; + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, + NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + exitstatus = EX_OSERR; + goto fallback; + case EINVAL: + exitstatus = EX_USAGE; goto fallback; } @@ -593,8 +628,8 @@ custom_argc = 1; custom_argv = malloc(sizeof(char*) * 2); if(custom_argv == NULL){ - perror("malloc"); - exitstatus = EXIT_FAILURE; + error(0, errno, "malloc"); + exitstatus = EX_OSERR; goto fallback; } custom_argv[0] = argv[0]; @@ -616,8 +651,8 @@ } new_arg = strdup(p); if(new_arg == NULL){ - perror("strdup"); - exitstatus = EXIT_FAILURE; + error(0, errno, "strdup"); + exitstatus = EX_OSERR; free(org_line); goto fallback; } @@ -626,8 +661,8 @@ custom_argv = realloc(custom_argv, sizeof(char *) * ((unsigned int) custom_argc + 1)); if(custom_argv == NULL){ - perror("realloc"); - exitstatus = EXIT_FAILURE; + error(0, errno, "realloc"); + exitstatus = EX_OSERR; free(org_line); goto fallback; } @@ -639,8 +674,8 @@ ret = fclose(conffp); } while(ret == EOF and errno == EINTR); if(ret == EOF){ - perror("fclose"); - exitstatus = EXIT_FAILURE; + error(0, errno, "fclose"); + exitstatus = EX_IOERR; goto fallback; } free(org_line); @@ -648,29 +683,48 @@ /* Check for harmful errors and go to fallback. Other errors might not affect opening plugins */ if(errno == EMFILE or errno == ENFILE or errno == ENOMEM){ - perror("fopen"); - exitstatus = EXIT_FAILURE; + error(0, errno, "fopen"); + exitstatus = EX_OSERR; goto fallback; } } - /* If there was any arguments from configuration file, - pass them to parser as command arguments */ + /* If there were any arguments from the configuration file, pass + them to parser as command line arguments */ if(custom_argv != NULL){ - ret = argp_parse(&argp, custom_argc, custom_argv, ARGP_IN_ORDER, - 0, NULL); - if(ret == ARGP_ERR_UNKNOWN){ - fprintf(stderr, "Unknown error while parsing arguments\n"); - exitstatus = EXIT_FAILURE; + ret = argp_parse(&argp, custom_argc, custom_argv, + ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, + NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + exitstatus = EX_OSERR; + goto fallback; + case EINVAL: + exitstatus = EX_CONFIG; goto fallback; } } /* Parse actual command line arguments, to let them override the config file */ - ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, 0, NULL); - if(ret == ARGP_ERR_UNKNOWN){ - fprintf(stderr, "Unknown error while parsing arguments\n"); - exitstatus = EXIT_FAILURE; + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP, + NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + exitstatus = EX_OSERR; + goto fallback; + case EINVAL: + exitstatus = EX_USAGE; goto fallback; } @@ -691,35 +745,56 @@ /* Strip permissions down to nobody */ setgid(gid); if(ret == -1){ - perror("setgid"); + error(0, errno, "setgid"); } ret = setuid(uid); if(ret == -1){ - perror("setuid"); - } - - if(plugindir == NULL){ - dir = opendir(PDIR); - } else { - dir = opendir(plugindir); - } - - if(dir == NULL){ - perror("Could not open plugin dir"); - exitstatus = EXIT_FAILURE; - goto fallback; - } - - /* Set the FD_CLOEXEC flag on the directory, if possible */ + error(0, errno, "setuid"); + } + + /* Open plugin directory with close_on_exec flag */ { - int dir_fd = dirfd(dir); - if(dir_fd >= 0){ - ret = set_cloexec_flag(dir_fd); - if(ret < 0){ - perror("set_cloexec_flag"); - exitstatus = EXIT_FAILURE; - goto fallback; - } + 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 */ + ); + } + if(dir_fd == -1){ + error(0, errno, "Could not open plugin dir"); + exitstatus = EX_UNAVAILABLE; + goto fallback; + } + +#ifndef O_CLOEXEC + /* Set the FD_CLOEXEC flag on the directory */ + 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; } } @@ -734,8 +809,8 @@ /* All directory entries have been processed */ if(dirst == NULL){ if(errno == EBADF){ - perror("readdir"); - exitstatus = EXIT_FAILURE; + error(0, errno, "readdir"); + exitstatus = EX_IOERR; goto fallback; } break; @@ -771,7 +846,7 @@ 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) + and (strcmp((dirst->d_name) + d_name_len-suf_len, *suf) == 0)){ if(debug){ fprintf(stderr, "Ignoring plugin dir entry \"%s\"" @@ -797,13 +872,13 @@ dirst->d_name)); } if(ret < 0){ - perror("asprintf"); + error(0, errno, "asprintf"); continue; } ret = (int)TEMP_FAILURE_RETRY(stat(filename, &st)); if(ret == -1){ - perror("stat"); + error(0, errno, "stat"); free(filename); continue; } @@ -821,7 +896,7 @@ plugin *p = getplugin(dirst->d_name); if(p == NULL){ - perror("getplugin"); + error(0, errno, "getplugin"); free(filename); continue; } @@ -839,13 +914,13 @@ if(g != NULL){ for(char **a = g->argv + 1; *a != NULL; a++){ if(not add_argument(p, *a)){ - perror("add_argument"); + error(0, errno, "add_argument"); } } /* Add global environment variables */ for(char **e = g->environ; *e != NULL; e++){ if(not add_environment(p, *e, false)){ - perror("add_environment"); + error(0, errno, "add_environment"); } } } @@ -856,7 +931,7 @@ if(p->environ[0] != NULL){ for(char **e = environ; *e != NULL; e++){ if(not add_environment(p, *e, false)){ - perror("add_environment"); + error(0, errno, "add_environment"); } } } @@ -864,21 +939,21 @@ int pipefd[2]; ret = (int)TEMP_FAILURE_RETRY(pipe(pipefd)); if(ret == -1){ - perror("pipe"); - exitstatus = EXIT_FAILURE; + error(0, errno, "pipe"); + exitstatus = EX_OSERR; goto fallback; } /* Ask OS to automatic close the pipe on exec */ ret = set_cloexec_flag(pipefd[0]); if(ret < 0){ - perror("set_cloexec_flag"); - exitstatus = EXIT_FAILURE; + error(0, errno, "set_cloexec_flag"); + exitstatus = EX_OSERR; goto fallback; } ret = set_cloexec_flag(pipefd[1]); if(ret < 0){ - perror("set_cloexec_flag"); - exitstatus = EXIT_FAILURE; + error(0, errno, "set_cloexec_flag"); + exitstatus = EX_OSERR; goto fallback; } /* Block SIGCHLD until process is safely in process list */ @@ -886,8 +961,8 @@ &sigchld_action.sa_mask, NULL)); if(ret < 0){ - perror("sigprocmask"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; goto fallback; } /* Starting a new process to be watched */ @@ -896,27 +971,27 @@ pid = fork(); } while(pid == -1 and errno == EINTR); if(pid == -1){ - perror("fork"); - exitstatus = EXIT_FAILURE; + error(0, errno, "fork"); + exitstatus = EX_OSERR; goto fallback; } if(pid == 0){ /* this is the child process */ ret = sigaction(SIGCHLD, &old_sigchld_action, NULL); if(ret < 0){ - perror("sigaction"); - _exit(EXIT_FAILURE); + error(0, errno, "sigaction"); + _exit(EX_OSERR); } ret = sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL); if(ret < 0){ - perror("sigprocmask"); - _exit(EXIT_FAILURE); + error(0, errno, "sigprocmask"); + _exit(EX_OSERR); } ret = dup2(pipefd[1], STDOUT_FILENO); /* replace our stdout */ if(ret == -1){ - perror("dup2"); - _exit(EXIT_FAILURE); + error(0, errno, "dup2"); + _exit(EX_OSERR); } if(dirfd(dir) < 0){ @@ -926,13 +1001,13 @@ } if(p->environ[0] == NULL){ if(execv(filename, p->argv) < 0){ - perror("execv"); - _exit(EXIT_FAILURE); + error(0, errno, "execv for %s", filename); + _exit(EX_OSERR); } } else { if(execve(filename, p->argv, p->environ) < 0){ - perror("execve"); - _exit(EXIT_FAILURE); + error(0, errno, "execve for %s", filename); + _exit(EX_OSERR); } } /* no return */ @@ -943,14 +1018,14 @@ free(filename); plugin *new_plugin = getplugin(dirst->d_name); if(new_plugin == NULL){ - perror("getplugin"); + error(0, errno, "getplugin"); ret = (int)(TEMP_FAILURE_RETRY (sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL))); if(ret < 0){ - perror("sigprocmask"); + error(0, errno, "sigprocmask"); } - exitstatus = EXIT_FAILURE; + exitstatus = EX_OSERR; goto fallback; } @@ -963,12 +1038,13 @@ &sigchld_action.sa_mask, NULL)); if(ret < 0){ - perror("sigprocmask"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; goto fallback; } - FD_SET(new_plugin->fd, &rfds_all); + FD_SET(new_plugin->fd, &rfds_all); /* Spurious warning from + -Wconversion */ if(maxfd < new_plugin->fd){ maxfd = new_plugin->fd; @@ -995,8 +1071,8 @@ fd_set rfds = rfds_all; int select_ret = select(maxfd+1, &rfds, NULL, NULL, NULL); if(select_ret == -1 and errno != EINTR){ - perror("select"); - exitstatus = EXIT_FAILURE; + error(0, errno, "select"); + exitstatus = EX_OSERR; goto fallback; } /* OK, now either a process completed, or something can be read @@ -1028,7 +1104,8 @@ } /* Remove the plugin */ - FD_CLR(proc->fd, &rfds_all); + FD_CLR(proc->fd, &rfds_all); /* Spurious warning from + -Wconversion */ /* Block signal while modifying process_list */ ret = (int)TEMP_FAILURE_RETRY(sigprocmask @@ -1036,8 +1113,8 @@ &sigchld_action.sa_mask, NULL)); if(ret < 0){ - perror("sigprocmask"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; goto fallback; } @@ -1050,8 +1127,8 @@ (sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL))); if(ret < 0){ - perror("sigprocmask"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigprocmask"); + exitstatus = EX_OSERR; goto fallback; } @@ -1067,14 +1144,16 @@ bool bret = print_out_password(proc->buffer, proc->buffer_length); if(not bret){ - perror("print_out_password"); - exitstatus = EXIT_FAILURE; + error(0, errno, "print_out_password"); + exitstatus = EX_IOERR; } goto fallback; } /* This process has not completed. Does it have any output? */ - if(proc->eof or not FD_ISSET(proc->fd, &rfds)){ + if(proc->eof or not FD_ISSET(proc->fd, &rfds)){ /* Spurious + warning from + -Wconversion */ /* This process had nothing to say at this time */ proc = proc->next; continue; @@ -1084,8 +1163,8 @@ proc->buffer = realloc(proc->buffer, proc->buffer_size + (size_t) BUFFER_SIZE); if(proc->buffer == NULL){ - perror("malloc"); - exitstatus = EXIT_FAILURE; + error(0, errno, "malloc"); + exitstatus = EX_OSERR; goto fallback; } proc->buffer_size += BUFFER_SIZE; @@ -1112,7 +1191,8 @@ fallback: - if(plugin_list == NULL or exitstatus != EXIT_SUCCESS){ + if(plugin_list == NULL or (exitstatus != EXIT_SUCCESS + and exitstatus != EX_OK)){ /* Fallback if all plugins failed, none are found or an error occured */ bool bret; @@ -1126,16 +1206,16 @@ } bret = print_out_password(passwordbuffer, len); if(not bret){ - perror("print_out_password"); - exitstatus = EXIT_FAILURE; + error(0, errno, "print_out_password"); + exitstatus = EX_IOERR; } } /* Restore old signal handler */ ret = sigaction(SIGCHLD, &old_sigchld_action, NULL); if(ret == -1){ - perror("sigaction"); - exitstatus = EXIT_FAILURE; + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; } if(custom_argv != NULL){ @@ -1156,7 +1236,7 @@ ret = kill(p->pid, SIGTERM); if(ret == -1 and errno != ESRCH){ /* Set-uid proccesses might not get closed */ - perror("kill"); + error(0, errno, "kill"); } } } @@ -1166,7 +1246,7 @@ ret = wait(NULL); } while(ret >= 0); if(errno != ECHILD){ - perror("wait"); + error(0, errno, "wait"); } free_plugin_list(); === modified file 'plugins.d/askpass-fifo.c' --- plugins.d/askpass-fifo.c 2009-09-16 23:28:39 +0000 +++ plugins.d/askpass-fifo.c 2010-09-26 18:32:58 +0000 @@ -2,8 +2,8 @@ /* * Askpass-FIFO - Read a password from a FIFO and output it * - * Copyright © 2008,2009 Teddy Hogeborn - * Copyright © 2008,2009 Björn Påhlsson + * Copyright © 2008-2010 Teddy Hogeborn + * Copyright © 2008-2010 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 @@ -26,13 +26,19 @@ #include /* ssize_t */ #include /* mkfifo(), S_IRUSR, S_IWUSR */ #include /* and */ -#include /* errno, EEXIST */ -#include /* perror() */ +#include /* errno, EACCES, ENOTDIR, ELOOP, + ENAMETOOLONG, ENOSPC, EROFS, + ENOENT, EEXIST, EFAULT, EMFILE, + ENFILE, ENOMEM, EBADF, EINVAL, EIO, + EISDIR, EFBIG */ +#include /* error() */ #include /* EXIT_FAILURE, NULL, size_t, free(), realloc(), EXIT_SUCCESS */ #include /* open(), O_RDONLY */ #include /* read(), close(), write(), STDOUT_FILENO */ +#include /* EX_OSERR, EX_OSFILE, + EX_UNAVAILABLE, EX_IOERR */ int main(__attribute__((unused))int argc, @@ -43,16 +49,46 @@ /* Create FIFO */ const char passfifo[] = "/lib/cryptsetup/passfifo"; ret = mkfifo(passfifo, S_IRUSR | S_IWUSR); - if(ret == -1 and errno != EEXIST){ - perror("mkfifo"); - return EXIT_FAILURE; + if(ret == -1){ + int e = errno; + error(0, errno, "mkfifo"); + switch(e){ + case EACCES: + case ENOTDIR: + case ELOOP: + return EX_OSFILE; + case ENAMETOOLONG: + case ENOSPC: + case EROFS: + default: + return EX_OSERR; + case ENOENT: + return EX_UNAVAILABLE; /* no "/lib/cryptsetup"? */ + case EEXIST: + break; /* not an error */ + } } /* Open FIFO */ int fifo_fd = open(passfifo, O_RDONLY); if(fifo_fd == -1){ - perror("open"); - return EXIT_FAILURE; + int e = errno; + error(0, errno, "open"); + switch(e){ + case EACCES: + case ENOENT: + case EFAULT: + return EX_UNAVAILABLE; + case ENAMETOOLONG: + case EMFILE: + case ENFILE: + case ENOMEM: + default: + return EX_OSERR; + case ENOTDIR: + case ELOOP: + return EX_OSFILE; + } } /* Read from FIFO */ @@ -65,18 +101,30 @@ if(buf_len + blocksize > buf_allocated){ char *tmp = realloc(buf, buf_allocated + blocksize); if(tmp == NULL){ - perror("realloc"); + error(0, errno, "realloc"); free(buf); - return EXIT_FAILURE; + return EX_OSERR; } buf = tmp; buf_allocated += blocksize; } sret = read(fifo_fd, buf + buf_len, buf_allocated - buf_len); if(sret == -1){ - perror("read"); + int e = errno; free(buf); - return EXIT_FAILURE; + errno = e; + error(0, errno, "read"); + switch(e){ + case EBADF: + case EFAULT: + case EINVAL: + default: + return EX_OSERR; + case EIO: + return EX_IOERR; + case EISDIR: + return EX_UNAVAILABLE; + } } buf_len += (size_t)sret; } while(sret != 0); @@ -90,13 +138,37 @@ while(written < buf_len){ sret = write(STDOUT_FILENO, buf + written, buf_len - written); if(sret == -1){ - perror("write"); + int e = errno; free(buf); - return EXIT_FAILURE; + errno = e; + error(0, errno, "write"); + switch(e){ + case EBADF: + case EFAULT: + case EINVAL: + return EX_OSFILE; + case EFBIG: + case EIO: + case ENOSPC: + default: + return EX_IOERR; + } } written += (size_t)sret; } free(buf); + ret = close(STDOUT_FILENO); + if(ret == -1){ + int e = errno; + error(0, errno, "close"); + switch(e){ + case EBADF: + return EX_OSFILE; + case EIO: + default: + return EX_IOERR; + } + } return EXIT_SUCCESS; } === modified file 'plugins.d/mandos-client.c' --- plugins.d/mandos-client.c 2009-09-17 11:22:28 +0000 +++ plugins.d/mandos-client.c 2010-09-26 21:27:28 +0000 @@ -9,8 +9,8 @@ * "browse_callback", and parts of "main". * * Everything else is - * Copyright © 2008,2009 Teddy Hogeborn - * Copyright © 2008,2009 Björn Påhlsson + * Copyright © 2008-2010 Teddy Hogeborn + * Copyright © 2008-2010 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 @@ -43,8 +43,8 @@ stdout, ferror(), remove() */ #include /* uint16_t, uint32_t */ #include /* NULL, size_t, ssize_t */ -#include /* free(), EXIT_SUCCESS, EXIT_FAILURE, - srand(), strtof(), abort() */ +#include /* free(), EXIT_SUCCESS, srand(), + strtof(), abort() */ #include /* bool, false, true */ #include /* memset(), strcmp(), strlen(), strerror(), asprintf(), strcpy() */ @@ -63,7 +63,7 @@ strtoimax() */ #include /* assert() */ #include /* perror(), errno */ -#include /* nanosleep(), time() */ +#include /* nanosleep(), time(), sleep() */ #include /* ioctl, ifreq, SIOCGIFFLAGS, IFF_UP, SIOCSIFFLAGS, if_indextoname(), if_nametoindex(), IF_NAMESIZE */ @@ -82,6 +82,8 @@ #include /* sigemptyset(), sigaddset(), sigaction(), SIGTERM, sig_atomic_t, raise() */ +#include /* EX_OSERR, EX_USAGE, EX_UNAVAILABLE, + EX_NOHOST, EX_IOERR, EX_PROTOCOL */ #ifdef __linux__ #include /* klogctl() */ @@ -125,6 +127,8 @@ static const char mandos_protocol_version[] = "1"; const char *argp_program_version = "mandos-client " VERSION; const char *argp_program_bug_address = ""; +static const char sys_class_net[] = "/sys/class/net"; +char *connect_to = NULL; /* Used for passing in values through the Avahi callback functions */ typedef struct { @@ -552,7 +556,10 @@ gnutls_session_t session; int pf; /* Protocol family */ + errno = 0; + if(quit_now){ + errno = EINTR; return -1; } @@ -565,6 +572,7 @@ break; default: fprintf(stderr, "Bad address family: %d\n", af); + errno = EINVAL; return -1; } @@ -580,11 +588,14 @@ tcp_sd = socket(pf, SOCK_STREAM, 0); if(tcp_sd < 0){ + int e = errno; perror("socket"); + errno = e; goto mandos_end; } if(quit_now){ + errno = EINTR; goto mandos_end; } @@ -597,11 +608,15 @@ ret = inet_pton(af, ip, &to.in.sin_addr); } if(ret < 0 ){ + int e = errno; perror("inet_pton"); + errno = e; goto mandos_end; } if(ret == 0){ + int e = errno; fprintf(stderr, "Bad address: %s\n", ip); + errno = e; goto mandos_end; } if(af == AF_INET6){ @@ -615,6 +630,7 @@ if(if_index == AVAHI_IF_UNSPEC){ fprintf(stderr, "An IPv6 link-local address is incomplete" " without a network interface\n"); + errno = EINVAL; goto mandos_end; } /* Set the network interface number as scope */ @@ -627,6 +643,7 @@ } if(quit_now){ + errno = EINTR; goto mandos_end; } @@ -663,6 +680,7 @@ } if(quit_now){ + errno = EINTR; goto mandos_end; } @@ -672,11 +690,16 @@ ret = connect(tcp_sd, &to.in, sizeof(to)); /* IPv4 */ } if(ret < 0){ - perror("connect"); + if ((errno != ECONNREFUSED and errno != ENETUNREACH) or debug){ + int e = errno; + perror("connect"); + errno = e; + } goto mandos_end; } if(quit_now){ + errno = EINTR; goto mandos_end; } @@ -687,7 +710,9 @@ ret = (int)TEMP_FAILURE_RETRY(write(tcp_sd, out + written, out_size - written)); if(ret == -1){ + int e = errno; perror("write"); + errno = e; goto mandos_end; } written += (size_t)ret; @@ -703,6 +728,7 @@ } if(quit_now){ + errno = EINTR; goto mandos_end; } } @@ -712,18 +738,21 @@ } if(quit_now){ + errno = EINTR; goto mandos_end; } gnutls_transport_set_ptr(session, (gnutls_transport_ptr_t) tcp_sd); if(quit_now){ + errno = EINTR; goto mandos_end; } do { ret = gnutls_handshake(session); if(quit_now){ + errno = EINTR; goto mandos_end; } } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); @@ -733,6 +762,7 @@ fprintf(stderr, "*** GnuTLS Handshake failed ***\n"); gnutls_perror(ret); } + errno = EPROTO; goto mandos_end; } @@ -746,17 +776,21 @@ while(true){ if(quit_now){ + errno = EINTR; goto mandos_end; } buffer_capacity = incbuffer(&buffer, buffer_length, buffer_capacity); if(buffer_capacity == 0){ + int e = errno; perror("incbuffer"); + errno = e; goto mandos_end; } if(quit_now){ + errno = EINTR; goto mandos_end; } @@ -775,12 +809,14 @@ ret = gnutls_handshake(session); if(quit_now){ + errno = EINTR; goto mandos_end; } } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); if(ret < 0){ fprintf(stderr, "*** GnuTLS Re-handshake failed ***\n"); gnutls_perror(ret); + errno = EPROTO; goto mandos_end; } break; @@ -788,6 +824,7 @@ fprintf(stderr, "Unknown error while reading data from" " encrypted session with Mandos server\n"); gnutls_bye(session, GNUTLS_SHUT_RDWR); + errno = EIO; goto mandos_end; } } else { @@ -800,12 +837,14 @@ } if(quit_now){ + errno = EINTR; goto mandos_end; } do { ret = gnutls_bye(session, GNUTLS_SHUT_RDWR); if(quit_now){ + errno = EINTR; goto mandos_end; } } while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED); @@ -820,6 +859,7 @@ written = 0; while(written < (size_t) decrypted_buffer_size){ if(quit_now){ + errno = EINTR; goto mandos_end; } @@ -827,10 +867,12 @@ (size_t)decrypted_buffer_size - written, stdout); if(ret == 0 and ferror(stdout)){ + int e = errno; if(debug){ fprintf(stderr, "Error writing encrypted data: %s\n", strerror(errno)); } + errno = e; goto mandos_end; } written += (size_t)ret; @@ -842,17 +884,25 @@ /* Shutdown procedure */ mandos_end: - free(decrypted_buffer); - free(buffer); - if(tcp_sd >= 0){ - ret = (int)TEMP_FAILURE_RETRY(close(tcp_sd)); - } - if(ret == -1){ - perror("close"); - } - gnutls_deinit(session); - if(quit_now){ - retval = -1; + { + int e = errno; + free(decrypted_buffer); + free(buffer); + if(tcp_sd >= 0){ + ret = (int)TEMP_FAILURE_RETRY(close(tcp_sd)); + } + if(ret == -1){ + if(e == 0){ + e = errno; + } + perror("close"); + } + gnutls_deinit(session); + if(quit_now){ + e = EINTR; + retval = -1; + } + errno = e; } return retval; } @@ -974,6 +1024,101 @@ errno = old_errno; } +/* + * This function determines if a directory entry in /sys/class/net + * corresponds to an acceptable network device. + * (This function is passed to scandir(3) as a filter function.) + */ +int good_interface(const struct dirent *if_entry){ + ssize_t ssret; + char *flagname = NULL; + int ret = asprintf(&flagname, "%s/%s/flags", sys_class_net, + if_entry->d_name); + if(ret < 0){ + perror("asprintf"); + return 0; + } + if(if_entry->d_name[0] == '.'){ + return 0; + } + int flags_fd = (int)TEMP_FAILURE_RETRY(open(flagname, O_RDONLY)); + if(flags_fd == -1){ + perror("open"); + return 0; + } + typedef short ifreq_flags; /* ifreq.ifr_flags in netdevice(7) */ + /* read line from flags_fd */ + ssize_t to_read = (sizeof(ifreq_flags)*2)+3; /* "0x1003\n" */ + char *flagstring = malloc((size_t)to_read+1); /* +1 for final \0 */ + flagstring[(size_t)to_read] = '\0'; + if(flagstring == NULL){ + perror("malloc"); + close(flags_fd); + return 0; + } + while(to_read > 0){ + ssret = (ssize_t)TEMP_FAILURE_RETRY(read(flags_fd, flagstring, + (size_t)to_read)); + if(ssret == -1){ + perror("read"); + free(flagstring); + close(flags_fd); + return 0; + } + to_read -= ssret; + if(ssret == 0){ + break; + } + } + close(flags_fd); + intmax_t tmpmax; + char *tmp; + errno = 0; + tmpmax = strtoimax(flagstring, &tmp, 0); + if(errno != 0 or tmp == flagstring or (*tmp != '\0' + and not (isspace(*tmp))) + or tmpmax != (ifreq_flags)tmpmax){ + if(debug){ + fprintf(stderr, "Invalid flags \"%s\" for interface \"%s\"\n", + flagstring, if_entry->d_name); + } + free(flagstring); + return 0; + } + free(flagstring); + ifreq_flags flags = (ifreq_flags)tmpmax; + /* Reject the loopback device */ + if(flags & IFF_LOOPBACK){ + if(debug){ + fprintf(stderr, "Rejecting loopback interface \"%s\"\n", + if_entry->d_name); + } + return 0; + } + /* Accept point-to-point devices only if connect_to is specified */ + if(connect_to != NULL and (flags & IFF_POINTOPOINT)){ + if(debug){ + fprintf(stderr, "Accepting point-to-point interface \"%s\"\n", + if_entry->d_name); + } + return 1; + } + /* Otherwise, reject non-broadcast-capable devices */ + if(not (flags & IFF_BROADCAST)){ + if(debug){ + fprintf(stderr, "Rejecting non-broadcast interface \"%s\"\n", + if_entry->d_name); + } + return 0; + } + /* Accept this device */ + if(debug){ + fprintf(stderr, "Interface \"%s\" is acceptable\n", + if_entry->d_name); + } + return 1; +} + int main(int argc, char *argv[]){ AvahiSServiceBrowser *sb = NULL; int error; @@ -981,13 +1126,12 @@ intmax_t tmpmax; char *tmp; int exitcode = EXIT_SUCCESS; - const char *interface = "eth0"; + const char *interface = ""; struct ifreq network; int sd = -1; bool take_down_interface = false; uid_t uid; gid_t gid; - char *connect_to = NULL; char tempdir[] = "/tmp/mandosXXXXXX"; bool tempdir_created = false; AvahiIfIndex if_index = AVAHI_IF_UNSPEC; @@ -1056,11 +1200,21 @@ .arg = "SECONDS", .doc = "Maximum delay to wait for interface startup", .group = 2 }, + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + { .name = "help", .key = '?', + .doc = "Give this help list", .group = -1 }, + { .name = "usage", .key = -3, + .doc = "Give a short usage message", .group = -1 }, + { .name = "version", .key = 'V', + .doc = "Print program version", .group = -1 }, { .name = NULL } }; error_t parse_opt(int key, char *arg, struct argp_state *state){ + errno = 0; switch(key){ case 128: /* --debug */ debug = true; @@ -1082,8 +1236,7 @@ tmpmax = strtoimax(arg, &tmp, 10); if(errno != 0 or tmp == arg or *tmp != '\0' or tmpmax != (typeof(mc.dh_bits))tmpmax){ - fprintf(stderr, "Bad number of DH bits\n"); - exit(EXIT_FAILURE); + argp_error(state, "Bad number of DH bits"); } mc.dh_bits = (typeof(mc.dh_bits))tmpmax; break; @@ -1094,28 +1247,46 @@ errno = 0; delay = strtof(arg, &tmp); if(errno != 0 or tmp == arg or *tmp != '\0'){ - fprintf(stderr, "Bad delay\n"); - exit(EXIT_FAILURE); + argp_error(state, "Bad delay"); } break; - case ARGP_KEY_ARG: - argp_usage(state); - case ARGP_KEY_END: + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + case '?': /* --help */ + argp_state_help(state, state->out_stream, + (ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR) + & ~(unsigned int)ARGP_HELP_EXIT_OK); + case -3: /* --usage */ + argp_state_help(state, state->out_stream, + ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR); + case 'V': /* --version */ + fprintf(state->out_stream, "%s\n", argp_program_version); + exit(argp_err_exit_status); break; default: return ARGP_ERR_UNKNOWN; } - return 0; + return errno; } struct argp argp = { .options = options, .parser = parse_opt, .args_doc = "", .doc = "Mandos client -- Get and decrypt" " passwords from a Mandos server" }; - ret = argp_parse(&argp, argc, argv, 0, 0, NULL); - if(ret == ARGP_ERR_UNKNOWN){ - fprintf(stderr, "Unknown error while parsing arguments\n"); - exitcode = EXIT_FAILURE; + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_HELP, 0, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + perror("argp_parse"); + exitcode = EX_OSERR; + goto end; + case EINVAL: + exitcode = EX_USAGE; goto end; } } @@ -1123,6 +1294,31 @@ if(not debug){ avahi_set_log_function(empty_log); } + + if(interface[0] == '\0'){ + struct dirent **direntries; + ret = scandir(sys_class_net, &direntries, good_interface, + alphasort); + if(ret >= 1){ + /* Pick the first good interface */ + interface = strdup(direntries[0]->d_name); + if(debug){ + fprintf(stderr, "Using interface \"%s\"\n", interface); + } + if(interface == NULL){ + perror("malloc"); + free(direntries); + exitcode = EXIT_FAILURE; + goto end; + } + free(direntries); + } else { + free(direntries); + fprintf(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 */ @@ -1131,7 +1327,7 @@ mc.simple_poll = avahi_simple_poll_new(); if(mc.simple_poll == NULL){ fprintf(stderr, "Avahi: Failed to create simple poll object.\n"); - exitcode = EXIT_FAILURE; + exitcode = EX_UNAVAILABLE; goto end; } @@ -1139,19 +1335,19 @@ ret = sigaddset(&sigterm_action.sa_mask, SIGINT); if(ret == -1){ perror("sigaddset"); - exitcode = EXIT_FAILURE; + exitcode = EX_OSERR; goto end; } ret = sigaddset(&sigterm_action.sa_mask, SIGHUP); if(ret == -1){ perror("sigaddset"); - exitcode = EXIT_FAILURE; + exitcode = EX_OSERR; goto end; } ret = sigaddset(&sigterm_action.sa_mask, SIGTERM); if(ret == -1){ perror("sigaddset"); - exitcode = EXIT_FAILURE; + exitcode = EX_OSERR; goto end; } /* Need to check if the handler is SIG_IGN before handling: @@ -1161,49 +1357,49 @@ ret = sigaction(SIGINT, NULL, &old_sigterm_action); if(ret == -1){ perror("sigaction"); - return EXIT_FAILURE; + return EX_OSERR; } if(old_sigterm_action.sa_handler != SIG_IGN){ ret = sigaction(SIGINT, &sigterm_action, NULL); if(ret == -1){ perror("sigaction"); - exitcode = EXIT_FAILURE; + exitcode = EX_OSERR; goto end; } } ret = sigaction(SIGHUP, NULL, &old_sigterm_action); if(ret == -1){ perror("sigaction"); - return EXIT_FAILURE; + return EX_OSERR; } if(old_sigterm_action.sa_handler != SIG_IGN){ ret = sigaction(SIGHUP, &sigterm_action, NULL); if(ret == -1){ perror("sigaction"); - exitcode = EXIT_FAILURE; + exitcode = EX_OSERR; goto end; } } ret = sigaction(SIGTERM, NULL, &old_sigterm_action); if(ret == -1){ perror("sigaction"); - return EXIT_FAILURE; + return EX_OSERR; } if(old_sigterm_action.sa_handler != SIG_IGN){ ret = sigaction(SIGTERM, &sigterm_action, NULL); if(ret == -1){ perror("sigaction"); - exitcode = EXIT_FAILURE; + exitcode = EX_OSERR; goto end; } } /* If the interface is down, bring it up */ - if(interface[0] != '\0'){ + if(strcmp(interface, "none") != 0){ if_index = (AvahiIfIndex) if_nametoindex(interface); if(if_index == 0){ fprintf(stderr, "No such interface: \"%s\"\n", interface); - exitcode = EXIT_FAILURE; + exitcode = EX_UNAVAILABLE; goto end; } @@ -1220,7 +1416,7 @@ #ifdef __linux__ /* Lower kernel loglevel to KERN_NOTICE to avoid KERN_INFO - messages to mess up the prompt */ + messages about the network interface to mess up the prompt */ ret = klogctl(8, NULL, 5); bool restore_loglevel = true; if(ret == -1){ @@ -1232,7 +1428,7 @@ sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP); if(sd < 0){ perror("socket"); - exitcode = EXIT_FAILURE; + exitcode = EX_OSERR; #ifdef __linux__ if(restore_loglevel){ ret = klogctl(7, NULL, 0); @@ -1261,7 +1457,7 @@ } } #endif /* __linux__ */ - exitcode = EXIT_FAILURE; + exitcode = EX_OSERR; /* Lower privileges */ errno = 0; ret = seteuid(uid); @@ -1276,8 +1472,8 @@ ret = ioctl(sd, SIOCSIFFLAGS, &network); if(ret == -1){ take_down_interface = false; - perror("ioctl SIOCSIFFLAGS"); - exitcode = EXIT_FAILURE; + perror("ioctl SIOCSIFFLAGS +IFF_UP"); + exitcode = EX_OSERR; #ifdef __linux__ if(restore_loglevel){ ret = klogctl(7, NULL, 0); @@ -1349,7 +1545,7 @@ ret = init_gnutls_global(pubkey, seckey); if(ret == -1){ fprintf(stderr, "init_gnutls_global failed\n"); - exitcode = EXIT_FAILURE; + exitcode = EX_UNAVAILABLE; goto end; } else { gnutls_initialized = true; @@ -1372,7 +1568,7 @@ if(not init_gpgme(pubkey, seckey, tempdir)){ fprintf(stderr, "init_gpgme failed\n"); - exitcode = EXIT_FAILURE; + exitcode = EX_UNAVAILABLE; goto end; } else { gpgme_initialized = true; @@ -1388,7 +1584,7 @@ char *address = strrchr(connect_to, ':'); if(address == NULL){ fprintf(stderr, "No colon in address\n"); - exitcode = EXIT_FAILURE; + exitcode = EX_USAGE; goto end; } @@ -1402,7 +1598,7 @@ if(errno != 0 or tmp == address+1 or *tmp != '\0' or tmpmax != (uint16_t)tmpmax){ fprintf(stderr, "Bad port number\n"); - exitcode = EXIT_FAILURE; + exitcode = EX_USAGE; goto end; } @@ -1424,13 +1620,19 @@ if(quit_now){ goto end; } - - ret = start_mandos_communication(address, port, if_index, af); - if(ret < 0){ - exitcode = EXIT_FAILURE; - } else { + + while(not quit_now){ + ret = start_mandos_communication(address, port, if_index, af); + if(quit_now or ret == 0){ + break; + } + sleep(15); + }; + + if (not quit_now){ exitcode = EXIT_SUCCESS; } + goto end; } @@ -1460,7 +1662,7 @@ if(mc.server == NULL){ fprintf(stderr, "Failed to create Avahi server: %s\n", avahi_strerror(error)); - exitcode = EXIT_FAILURE; + exitcode = EX_UNAVAILABLE; goto end; } @@ -1475,7 +1677,7 @@ if(sb == NULL){ fprintf(stderr, "Failed to create service browser: %s\n", avahi_strerror(avahi_server_errno(mc.server))); - exitcode = EXIT_FAILURE; + exitcode = EX_UNAVAILABLE; goto end; } @@ -1530,10 +1732,10 @@ if(ret == -1){ perror("ioctl SIOCGIFFLAGS"); } else if(network.ifr_flags & IFF_UP) { - network.ifr_flags &= ~IFF_UP; /* clear flag */ + network.ifr_flags &= ~(short)IFF_UP; /* clear flag */ ret = ioctl(sd, SIOCSIFFLAGS, &network); if(ret == -1){ - perror("ioctl SIOCSIFFLAGS"); + perror("ioctl SIOCSIFFLAGS -IFF_UP"); } } ret = (int)TEMP_FAILURE_RETRY(close(sd)); === modified file 'plugins.d/mandos-client.xml' --- plugins.d/mandos-client.xml 2009-02-09 02:01:13 +0000 +++ plugins.d/mandos-client.xml 2010-09-26 18:32:58 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -202,8 +202,9 @@ Network interface that will be brought up and scanned for - Mandos servers to connect to. The default is - eth0. + Mandos servers to connect to. The default is the empty + string, which will automatically choose an appropriate + interface. If the option is used, this @@ -220,10 +221,11 @@ by this program. - NAME can be the empty string; - 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. + 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. === modified file 'plugins.d/password-prompt.c' --- plugins.d/password-prompt.c 2009-09-07 07:48:59 +0000 +++ plugins.d/password-prompt.c 2010-09-26 18:32:58 +0000 @@ -2,8 +2,8 @@ /* * Password-prompt - Read a password from the terminal and print it * - * Copyright © 2008,2009 Teddy Hogeborn - * Copyright © 2008,2009 Björn Påhlsson + * Copyright © 2008-2010 Teddy Hogeborn + * Copyright © 2008-2010 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 @@ -37,19 +37,24 @@ #include /* NULL, size_t, ssize_t */ #include /* ssize_t */ #include /* EXIT_SUCCESS, EXIT_FAILURE, - getopt_long, getenv() */ + getenv() */ #include /* fprintf(), stderr, getline(), - stdin, feof(), perror(), fputc(), - stdout, getopt_long */ -#include /* errno, EINVAL */ + stdin, feof(), fputc() + */ +#include /* errno, EBADF, ENOTTY, EINVAL, + EFAULT, EFBIG, EIO, ENOSPC, EINTR + */ +#include /* error() */ #include /* or, not */ #include /* bool, false, true */ -#include /* strlen, rindex, strncmp, strcmp */ +#include /* strlen, rindex */ #include /* struct argp_option, struct argp_state, struct argp, argp_parse(), error_t, ARGP_KEY_ARG, ARGP_KEY_END, ARGP_ERR_UNKNOWN */ +#include /* EX_SOFTWARE, EX_OSERR, + EX_UNAVAILABLE, EX_IOERR, EX_OK */ volatile sig_atomic_t quit_now = 0; int signal_received; @@ -66,7 +71,8 @@ } int main(int argc, char **argv){ - ssize_t ret; + ssize_t sret; + int ret; size_t n; struct termios t_new, t_old; char *buffer = NULL; @@ -82,10 +88,20 @@ .doc = "Prefix shown before the prompt", .group = 2 }, { .name = "debug", .key = 128, .doc = "Debug mode", .group = 3 }, + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + { .name = "help", .key = '?', + .doc = "Give this help list", .group = -1 }, + { .name = "usage", .key = -3, + .doc = "Give a short usage message", .group = -1 }, + { .name = "version", .key = 'V', + .doc = "Print program version", .group = -1 }, { .name = NULL } }; error_t parse_opt (int key, char *arg, struct argp_state *state){ + errno = 0; switch (key){ case 'p': prefix = arg; @@ -93,25 +109,42 @@ case 128: debug = true; break; - case ARGP_KEY_ARG: - argp_usage(state); - break; - case ARGP_KEY_END: + /* + * These reproduce what we would get without ARGP_NO_HELP + */ + case '?': /* --help */ + argp_state_help(state, state->out_stream, + (ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR) + & ~(unsigned int)ARGP_HELP_EXIT_OK); + case -3: /* --usage */ + argp_state_help(state, state->out_stream, + ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR); + case 'V': /* --version */ + fprintf(state->out_stream, "%s\n", argp_program_version); + exit(argp_err_exit_status); break; default: return ARGP_ERR_UNKNOWN; } - return 0; + return errno; } struct argp argp = { .options = options, .parser = parse_opt, .args_doc = "", .doc = "Mandos password-prompt -- Read and" " output a password" }; - ret = argp_parse(&argp, argc, argv, 0, 0, NULL); - if(ret == ARGP_ERR_UNKNOWN){ - fprintf(stderr, "Unknown error while parsing arguments\n"); - return EXIT_FAILURE; + ret = argp_parse(&argp, argc, argv, + ARGP_IN_ORDER | ARGP_NO_HELP, NULL, NULL); + switch(ret){ + case 0: + break; + case ENOMEM: + default: + errno = ret; + error(0, errno, "argp_parse"); + return EX_OSERR; + case EINVAL: + return EX_USAGE; } } @@ -123,25 +156,32 @@ } if(tcgetattr(STDIN_FILENO, &t_old) != 0){ - perror("tcgetattr"); - return EXIT_FAILURE; + int e = errno; + error(0, errno, "tcgetattr"); + switch(e){ + case EBADF: + case ENOTTY: + return EX_UNAVAILABLE; + default: + return EX_OSERR; + } } sigemptyset(&new_action.sa_mask); ret = sigaddset(&new_action.sa_mask, SIGINT); if(ret == -1){ - perror("sigaddset"); - return EXIT_FAILURE; + error(0, errno, "sigaddset"); + return EX_OSERR; } ret = sigaddset(&new_action.sa_mask, SIGHUP); if(ret == -1){ - perror("sigaddset"); - return EXIT_FAILURE; + error(0, errno, "sigaddset"); + return EX_OSERR; } ret = sigaddset(&new_action.sa_mask, SIGTERM); if(ret == -1){ - perror("sigaddset"); - return EXIT_FAILURE; + error(0, errno, "sigaddset"); + return EX_OSERR; } /* Need to check if the handler is SIG_IGN before handling: | [[info:libc:Initial Signal Actions]] | @@ -149,38 +189,38 @@ */ ret = sigaction(SIGINT, NULL, &old_action); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGINT, &new_action, NULL); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } } ret = sigaction(SIGHUP, NULL, &old_action); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGHUP, &new_action, NULL); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } } ret = sigaction(SIGTERM, NULL, &old_action); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGTERM, &new_action, NULL); if(ret == -1){ - perror("sigaction"); - return EXIT_FAILURE; + error(0, errno, "sigaction"); + return EX_OSERR; } } @@ -190,12 +230,20 @@ } t_new = t_old; - t_new.c_lflag &= ~ECHO; + t_new.c_lflag &= ~(tcflag_t)ECHO; if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_new) != 0){ - perror("tcsetattr-echo"); - return EXIT_FAILURE; + int e = errno; + error(0, errno, "tcsetattr-echo"); + switch(e){ + case EBADF: + case ENOTTY: + return EX_UNAVAILABLE; + case EINVAL: + default: + return EX_OSERR; + } } - + if(debug){ fprintf(stderr, "Waiting for input from stdin \n"); } @@ -212,55 +260,101 @@ fprintf(stderr, "%s ", prefix); } { - const char *cryptsource = getenv("cryptsource"); - const char *crypttarget = getenv("crypttarget"); - const char *const prompt - = "Enter passphrase to unlock the disk"; + const char *cryptsource = getenv("CRYPTTAB_SOURCE"); + const char *crypttarget = getenv("CRYPTTAB_NAME"); + /* Before cryptsetup 1.1.0~rc2 */ + if(cryptsource == NULL){ + cryptsource = getenv("cryptsource"); + } + if(crypttarget == NULL){ + crypttarget = getenv("crypttarget"); + } + const char *const prompt1 = "Unlocking the disk"; + const char *const prompt2 = "Enter passphrase"; if(cryptsource == NULL){ if(crypttarget == NULL){ - fprintf(stderr, "%s: ", prompt); + fprintf(stderr, "%s to unlock the disk: ", prompt2); } else { - fprintf(stderr, "%s (%s): ", prompt, crypttarget); + fprintf(stderr, "%s (%s)\n%s: ", prompt1, crypttarget, + prompt2); } } else { if(crypttarget == NULL){ - fprintf(stderr, "%s %s: ", prompt, cryptsource); + fprintf(stderr, "%s %s\n%s: ", prompt1, cryptsource, + prompt2); } else { - fprintf(stderr, "%s %s (%s): ", prompt, cryptsource, - crypttarget); + fprintf(stderr, "%s %s (%s)\n%s: ", prompt1, cryptsource, + crypttarget, prompt2); } } } - ret = getline(&buffer, &n, stdin); - if(ret > 0){ + sret = getline(&buffer, &n, stdin); + if(sret > 0){ status = EXIT_SUCCESS; /* Make n = data size instead of allocated buffer size */ - n = (size_t)ret; + n = (size_t)sret; /* Strip final newline */ - if(n>0 and buffer[n-1] == '\n'){ + if(n > 0 and buffer[n-1] == '\n'){ buffer[n-1] = '\0'; /* not strictly necessary */ n--; } size_t written = 0; while(written < n){ - ret = write(STDOUT_FILENO, buffer + written, n - written); - if(ret < 0){ - perror("write"); - status = EXIT_FAILURE; - break; - } - written += (size_t)ret; + sret = write(STDOUT_FILENO, buffer + written, n - written); + if(sret < 0){ + int e = errno; + error(0, errno, "write"); + switch(e){ + case EBADF: + case EFAULT: + case EINVAL: + case EFBIG: + case EIO: + case ENOSPC: + default: + status = EX_IOERR; + break; + case EINTR: + status = EXIT_FAILURE; + break; + } + break; + } + written += (size_t)sret; + } + sret = close(STDOUT_FILENO); + if(sret == -1){ + int e = errno; + error(0, errno, "close"); + switch(e){ + case EBADF: + status = EX_OSFILE; + break; + case EIO: + default: + status = EX_IOERR; + break; + } } break; } - if(ret < 0){ + if(sret < 0){ + int e = errno; if(errno != EINTR and not feof(stdin)){ - perror("getline"); - status = EXIT_FAILURE; + error(0, errno, "getline"); + switch(e){ + case EBADF: + status = EX_UNAVAILABLE; + case EIO: + case EINVAL: + default: + status = EX_IOERR; + break; + } break; } } - /* if(ret == 0), then the only sensible thing to do is to retry to + /* if(sret == 0), then the only sensible thing to do is to retry to read from stdin */ fputc('\n', stderr); if(debug and not quit_now){ @@ -276,7 +370,7 @@ fprintf(stderr, "Restoring terminal attributes\n"); } if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_old) != 0){ - perror("tcsetattr+echo"); + error(0, errno, "tcsetattr+echo"); } if(quit_now){ @@ -284,7 +378,7 @@ old_action.sa_handler = SIG_DFL; ret = sigaction(signal_received, &old_action, NULL); if(ret == -1){ - perror("sigaction"); + error(0, errno, "sigaction"); } raise(signal_received); } @@ -293,7 +387,7 @@ fprintf(stderr, "%s is exiting with status %d\n", argv[0], status); } - if(status == EXIT_SUCCESS){ + if(status == EXIT_SUCCESS or status == EX_OK){ fputc('\n', stderr); } === modified file 'plugins.d/password-prompt.xml' --- plugins.d/password-prompt.xml 2009-01-04 21:54:55 +0000 +++ plugins.d/password-prompt.xml 2009-10-30 16:23:43 +0000 @@ -2,7 +2,7 @@ - + %common; ]> @@ -183,8 +183,8 @@ ENVIRONMENT - cryptsource - crypttarget + CRYPTTAB_SOURCE + CRYPTTAB_NAME If set, these environment variables will be assumed to === added file 'plugins.d/plymouth.c' --- plugins.d/plymouth.c 1970-01-01 00:00:00 +0000 +++ plugins.d/plymouth.c 2010-09-26 18:32:58 +0000 @@ -0,0 +1,431 @@ +/* -*- coding: utf-8 -*- */ +/* + * Usplash - Read a password from usplash and output it + * + * Copyright © 2010 Teddy Hogeborn + * Copyright © 2010 Björn Påhlsson + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * . + * + * Contact the authors at . + */ + +#define _GNU_SOURCE /* asprintf(), TEMP_FAILURE_RETRY() */ +#include /* sig_atomic_t, struct sigaction, + sigemptyset(), sigaddset(), SIGINT, + SIGHUP, SIGTERM, sigaction(), + kill(), SIG_IGN */ +#include /* bool, false, true */ +#include /* open(), O_RDONLY */ +#include /* and, or, not*/ +#include /* size_t, ssize_t, pid_t, struct + dirent, waitpid() */ +#include /* waitpid() */ +#include /* NULL */ +#include /* strchr(), memcmp() */ +#include /* asprintf(), perror(), fopen(), + fscanf() */ +#include /* close(), readlink(), read(), + fork(), setsid(), chdir(), dup2(), + STDERR_FILENO, execv(), access() */ +#include /* free(), EXIT_FAILURE, realloc(), + EXIT_SUCCESS, malloc(), _exit(), + getenv() */ +#include /* scandir(), alphasort() */ +#include /* intmax_t, strtoumax(), SCNuMAX */ +#include /* struct stat, lstat() */ +#include /* EX_OSERR, EX_UNAVAILABLE */ +#include /* error() */ +#include /* TEMP_FAILURE_RETRY */ +#include /* argz_count(), argz_extract() */ + +sig_atomic_t interrupted_by_signal = 0; +const char plymouth_pid[] = "/dev/.initramfs/plymouth.pid"; +const char plymouth_path[] = "/bin/plymouth"; +const char plymouthd_path[] = "/sbin/plymouthd"; +const char *plymouthd_default_argv[] = {"/sbin/plymouthd", + "--mode=boot", + "--attach-to-session", + "--pid-file=" + "/dev/.initramfs/" + "plymouth.pid", + NULL }; + +static void termination_handler(__attribute__((unused))int signum){ + if(interrupted_by_signal){ + return; + } + interrupted_by_signal = 1; +} + +/* Create prompt string */ +char *makeprompt(void){ + int ret = 0; + char *prompt; + const char *const cryptsource = getenv("cryptsource"); + const char *const crypttarget = getenv("crypttarget"); + const char prompt_start[] = "Enter passphrase to unlock the disk"; + + if(cryptsource == NULL){ + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s: ", prompt_start); + } else { + ret = asprintf(&prompt, "%s (%s): ", prompt_start, + crypttarget); + } + } else { + if(crypttarget == NULL){ + ret = asprintf(&prompt, "%s %s: ", prompt_start, cryptsource); + } else { + ret = asprintf(&prompt, "%s %s (%s): ", prompt_start, + cryptsource, crypttarget); + } + } + if(ret == -1){ + return NULL; + } + return prompt; +} + +void kill_and_wait(pid_t pid){ + TEMP_FAILURE_RETRY(kill(pid, SIGTERM)); + TEMP_FAILURE_RETRY(waitpid(pid, NULL, 0)); +} + +bool become_a_daemon(void){ + int ret = setuid(geteuid()); + if(ret == -1){ + error(0, errno, "setuid"); + } + + setsid(); + ret = chdir("/"); + if(ret == -1){ + error(0, errno, "chdir"); + return false; + } + ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */ + if(ret == -1){ + error(0, errno, "dup2"); + return false; + } + return true; +} + +bool exec_and_wait(pid_t *pid_return, const char *path, + const char **argv, bool interruptable, + bool daemonize){ + int status; + int ret; + pid_t pid; + pid = fork(); + if(pid == -1){ + error(0, errno, "fork"); + return false; + } + if(pid == 0){ + /* Child */ + if(daemonize){ + if(not become_a_daemon()){ + _exit(EX_OSERR); + } + } + + char **new_argv = NULL; + char *tmp; + int i = 0; + for (; argv[i]!=(char *)NULL; i++){ + tmp = realloc(new_argv, sizeof(const char *) * ((size_t)i + 1)); + if (tmp == NULL){ + error(0, errno, "realloc"); + free(new_argv); + _exit(EX_OSERR); + } + new_argv = (char **)tmp; + new_argv[i] = strdup(argv[i]); + } + new_argv[i] = (char *) NULL; + + execv(path, (char *const *)new_argv); + error(0, errno, "execv"); + _exit(EXIT_FAILURE); + } + if(pid_return != NULL){ + *pid_return = pid; + } + do { + ret = waitpid(pid, &status, 0); + } while(ret == -1 and errno == EINTR + and ((not interrupted_by_signal) + or (not interruptable))); + if(interrupted_by_signal and interruptable){ + return false; + } + if(ret == -1){ + error(0, errno, "waitpid"); + return false; + } + if(WIFEXITED(status) and (WEXITSTATUS(status) == 0)){ + return true; + } + return false; +} + +int is_plymouth(const struct dirent *proc_entry){ + int ret; + { + uintmax_t maxvalue; + char *tmp; + errno = 0; + maxvalue = strtoumax(proc_entry->d_name, &tmp, 10); + + if(errno != 0 or *tmp != '\0' + or maxvalue != (uintmax_t)((pid_t)maxvalue)){ + return 0; + } + } + char exe_target[sizeof(plymouth_path)]; + char *exe_link; + ret = asprintf(&exe_link, "/proc/%s/exe", proc_entry->d_name); + if(ret == -1){ + error(0, errno, "asprintf"); + return 0; + } + + struct stat exe_stat; + ret = lstat(exe_link, &exe_stat); + if(ret == -1){ + free(exe_link); + if(errno != ENOENT){ + error(0, errno, "lstat"); + } + return 0; + } + + if(not S_ISLNK(exe_stat.st_mode) + or exe_stat.st_uid != 0 + or exe_stat.st_gid != 0){ + free(exe_link); + return 0; + } + + ssize_t sret = readlink(exe_link, exe_target, sizeof(exe_target)); + free(exe_link); + if((sret != (ssize_t)sizeof(plymouth_path)-1) or + (memcmp(plymouth_path, exe_target, + sizeof(plymouth_path)-1) != 0)){ + return 0; + } + return 1; +} + +pid_t get_pid(void){ + int ret; + FILE *pidfile = fopen(plymouth_pid, "r"); + uintmax_t maxvalue = 0; + if(pidfile != NULL){ + ret = fscanf(pidfile, "%" SCNuMAX, &maxvalue); + if(ret != 1){ + maxvalue = 0; + } + fclose(pidfile); + } + if(maxvalue == 0){ + struct dirent **direntries; + ret = scandir("/proc", &direntries, is_plymouth, alphasort); + sscanf(direntries[0]->d_name, "%" SCNuMAX, &maxvalue); + } + pid_t pid; + pid = (pid_t)maxvalue; + if((uintmax_t)pid == maxvalue){ + return pid; + } + + return 0; +} + +const char **getargv(pid_t pid){ + int cl_fd; + char *cmdline_filename; + ssize_t sret; + int ret; + + ret = asprintf(&cmdline_filename, "/proc/%" PRIuMAX "/cmdline", + (uintmax_t)pid); + if(ret == -1){ + error(0, errno, "asprintf"); + return NULL; + } + + /* Open /proc//cmdline */ + cl_fd = open(cmdline_filename, O_RDONLY); + free(cmdline_filename); + if(cl_fd == -1){ + error(0, errno, "open"); + return NULL; + } + + size_t cmdline_allocated = 0; + size_t cmdline_len = 0; + char *cmdline = NULL; + char *tmp; + const size_t blocksize = 1024; + do { + /* Allocate more space? */ + if(cmdline_len + blocksize > cmdline_allocated){ + tmp = realloc(cmdline, cmdline_allocated + blocksize); + if(tmp == NULL){ + error(0, errno, "realloc"); + free(cmdline); + close(cl_fd); + return NULL; + } + cmdline = tmp; + cmdline_allocated += blocksize; + } + + /* Read data */ + sret = read(cl_fd, cmdline + cmdline_len, + cmdline_allocated - cmdline_len); + if(sret == -1){ + error(0, errno, "read"); + free(cmdline); + close(cl_fd); + return NULL; + } + cmdline_len += (size_t)sret; + } while(sret != 0); + ret = close(cl_fd); + if(ret == -1){ + error(0, errno, "close"); + free(cmdline); + return NULL; + } + + /* we got cmdline and cmdline_len, ignore rest... */ + char **argv = malloc((argz_count(cmdline, cmdline_len) + 1) + * sizeof(char *)); /* Get number of args */ + if(argv == NULL){ + error(0, errno, "argv = malloc()"); + free(cmdline); + return NULL; + } + argz_extract(cmdline, cmdline_len, argv); /* Create argv */ + return (const char **)argv; +} + +int main(__attribute__((unused))int argc, + __attribute__((unused))char **argv){ + char *prompt; + char *prompt_arg; + pid_t plymouth_command_pid; + int ret; + bool bret; + + /* test -x /bin/plymouth */ + ret = access(plymouth_path, X_OK); + if(ret == -1){ + /* Plymouth is probably not installed. Don't print an error + message, just exit. */ + exit(EX_UNAVAILABLE); + } + + { /* Add signal handlers */ + struct sigaction old_action, + new_action = { .sa_handler = termination_handler, + .sa_flags = 0 }; + sigemptyset(&new_action.sa_mask); + for(int *sig = (int[]){ SIGINT, SIGHUP, SIGTERM, 0 }; + *sig != 0; sig++){ + ret = sigaddset(&new_action.sa_mask, *sig); + if(ret == -1){ + error(EX_OSERR, errno, "sigaddset"); + } + ret = sigaction(*sig, NULL, &old_action); + if(ret == -1){ + error(EX_OSERR, errno, "sigaction"); + } + if(old_action.sa_handler != SIG_IGN){ + ret = sigaction(*sig, &new_action, NULL); + if(ret == -1){ + error(EX_OSERR, errno, "sigaction"); + } + } + } + } + + /* plymouth --ping */ + bret = exec_and_wait(&plymouth_command_pid, plymouth_path, + (const char *[]) + { plymouth_path, "--ping", NULL }, + true, false); + if(not bret){ + if(interrupted_by_signal){ + kill_and_wait(plymouth_command_pid); + exit(EXIT_FAILURE); + } + /* Plymouth is probably not running. Don't print an error + message, just exit. */ + exit(EX_UNAVAILABLE); + } + + prompt = makeprompt(); + ret = asprintf(&prompt_arg, "--prompt=%s", prompt); + free(prompt); + if(ret == -1){ + error(EX_OSERR, errno, "asprintf"); + } + + /* plymouth ask-for-password --prompt="$prompt" */ + bret = exec_and_wait(&plymouth_command_pid, + plymouth_path, (const char *[]) + { plymouth_path, "ask-for-password", + prompt_arg, NULL }, + true, false); + free(prompt_arg); + if(bret){ + exit(EXIT_SUCCESS); + } + if(not interrupted_by_signal){ + /* exec_and_wait failed for some other reason */ + exit(EXIT_FAILURE); + } + kill_and_wait(plymouth_command_pid); + + const char **plymouthd_argv; + pid_t pid = get_pid(); + if(pid == 0){ + error(0, 0, "plymouthd pid not found"); + plymouthd_argv = plymouthd_default_argv; + } else { + plymouthd_argv = getargv(pid); + } + + bret = exec_and_wait(NULL, plymouth_path, (const char *[]) + { plymouth_path, "quit", NULL }, + false, false); + if(not bret){ + exit(EXIT_FAILURE); + } + bret = exec_and_wait(NULL, plymouthd_path, plymouthd_argv, + false, true); + if(not bret){ + exit(EXIT_FAILURE); + } + exec_and_wait(NULL, plymouth_path, (const char *[]) + { plymouth_path, "show-splash", NULL }, + false, false); + exit(EXIT_FAILURE); +} === added file 'plugins.d/plymouth.xml' --- plugins.d/plymouth.xml 1970-01-01 00:00:00 +0000 +++ plugins.d/plymouth.xml 2010-09-26 18:32:58 +0000 @@ -0,0 +1,278 @@ + + + + +%common; +]> + + + + Mandos Manual + + Mandos + &version; + &TIMESTAMP; + + + Björn + Påhlsson +
+ belorn@fukt.bsnet.se +
+
+ + Teddy + Hogeborn +
+ teddy@fukt.bsnet.se +
+
+
+ + 2010 + Teddy Hogeborn + Björn Påhlsson + + +
+ + + &COMMANDNAME; + 8mandos + + + + &COMMANDNAME; + Mandos plugin to use plymouth to get a + password. + + + + + &COMMANDNAME; + + + + + DESCRIPTION + + This program prompts for a password using + plymouth8 + and outputs any given password to standard + output. If no plymouth8 + process can be found, this program will immediately exit with an + exit code indicating failure. + + + This program is not very useful on its own. This program is + really meant to run as a plugin in the Mandos client-side system, where it is used as a + fallback and alternative to retrieving passwords from a + Mandos server. + + + If this program is killed (presumably by + plugin-runner + 8mandos because some other + plugin provided the password), it cannot tell + plymouth8 + to abort requesting a password, because + plymouth + 8 does not support this. + Therefore, this program will then kill the + running plymouth + 8 process and start a + new one using the same command line + arguments as the old one was using. + + + + + OPTIONS + + This program takes no options. + + + + + EXIT STATUS + + If exit status is 0, the output from the program is the password + as it was read. Otherwise, if exit status is other than 0, the + program was interrupted or encountered an error, and any output + so far could be corrupt and/or truncated, and should therefore + be ignored. + + + + + ENVIRONMENT + + + cryptsource + crypttarget + + + If set, these environment variables will be assumed to + contain the source device name and the target device + mapper name, respectively, and will be shown as part of + the prompt. + + + These variables will normally be inherited from + plugin-runner + 8mandos, which will + normally have inherited them from + /scripts/local-top/cryptroot in the + initial RAM disk environment, which will + have set them from parsing kernel arguments and + /conf/conf.d/cryptroot (also in the + initial RAM disk environment), which in turn will have been + created when the initial RAM disk image was created by + /usr/share/initramfs-tools/hooks/cryptroot, by + extracting the information of the root file system from + /etc/crypttab. + + + This behavior is meant to exactly mirror the behavior of + askpass, the default password prompter. + + + + + + + + FILES + + + /bin/plymouth + + + This is the command run to retrieve a password from + plymouth + 8. + + + + + /proc + + + To find the running plymouth8 + , this directory will be searched for + numeric entries which will be assumed to be directories. + In all those directories, the exe and + cmdline entries will be used to + determine the name of the running binary, effective user + and group ID, and the command line + arguments. See proc5 + . + + + + + /sbin/plymouthd + + + This is the name of the binary which will be searched for + in the process list. See plymouth8 + . + + + + + + + + BUGS + + Killing the plymouth8 + daemon and starting a new one is ugly, but necessary as long as + it does not support aborting a password request. + + + + + EXAMPLE + + Note that normally, this program will not be invoked directly, + but instead started by the Mandos plugin-runner8mandos + . + + + + This program takes no options. + + + &COMMANDNAME; + + + + + + SECURITY + + If this program is killed by a signal, it will kill the process + ID which at the start of this program was + determined to run plymouth8 + as root (see also ). There is a very + slight risk that, in the time between those events, that process + ID was freed and then taken up by another + process; the wrong process would then be killed. Now, this + program can only be killed by the user who started it; see + plugin-runner + 8mandos. This program + should therefore be started by a completely separate + non-privileged user, and no other programs should be allowed to + run as that special user. This means that it is not recommended + to use the user "nobody" to start this program, as other + possibly less trusted programs could be running as "nobody", and + they would then be able to kill this program, triggering the + killing of the process ID which may or may not + be plymouth + 8. + + + The only other thing that could be considered worthy of note is + this: This program is meant to be run by + plugin-runner8mandos, and will, when run + standalone, outside, in a normal environment, immediately output + on its standard output any presumably secret password it just + received. Therefore, when running this program standalone + (which should never normally be done), take care not to type in + any real secret password by force of habit, since it would then + immediately be shown as output. + + + + + SEE ALSO + + crypttab + 5, + plugin-runner + 8mandos, + proc + 5, + plymouth + 8 + + +
+ + + + + === modified file 'plugins.d/splashy.c' --- plugins.d/splashy.c 2009-09-16 23:28:39 +0000 +++ plugins.d/splashy.c 2010-09-26 18:32:58 +0000 @@ -2,8 +2,8 @@ /* * Splashy - Read a password from splashy and output it * - * Copyright © 2008,2009 Teddy Hogeborn - * Copyright © 2008,2009 Björn Påhlsson + * Copyright © 2008-2010 Teddy Hogeborn + * Copyright © 2008-2010 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 @@ -29,7 +29,7 @@ SIG_IGN, kill(), SIGKILL */ #include /* NULL */ #include /* getenv() */ -#include /* asprintf(), perror() */ +#include /* asprintf() */ #include /* EXIT_FAILURE, free(), EXIT_SUCCESS */ #include /* pid_t, DIR, struct dirent, @@ -43,9 +43,17 @@ STDOUT_FILENO, _exit(), pause() */ #include /* memcmp() */ -#include /* errno */ +#include /* errno, EACCES, ENOTDIR, ELOOP, + ENOENT, ENAMETOOLONG, EMFILE, + ENFILE, ENOMEM, ENOEXEC, EINVAL, + E2BIG, EFAULT, EIO, ETXTBSY, + EISDIR, ELIBBAD, EPERM, EINTR, + ECHILD */ +#include /* error() */ #include /* waitpid(), WIFEXITED(), WEXITSTATUS() */ +#include /* EX_OSERR, EX_OSFILE, + EX_UNAVAILABLE */ sig_atomic_t interrupted_by_signal = 0; int signal_received; @@ -65,6 +73,7 @@ DIR *proc_dir = NULL; pid_t splashy_pid = 0; pid_t splashy_command_pid = 0; + int exitstatus = EXIT_FAILURE; /* Create prompt string */ { @@ -90,6 +99,7 @@ } if(ret == -1){ prompt = NULL; + exitstatus = EX_OSERR; goto failure; } } @@ -99,7 +109,23 @@ const char splashy_name[] = "/sbin/splashy"; proc_dir = opendir("/proc"); if(proc_dir == NULL){ - perror("opendir"); + int e = errno; + error(0, errno, "opendir"); + switch(e){ + case EACCES: + case ENOTDIR: + case ELOOP: + case ENOENT: + default: + exitstatus = EX_OSFILE; + break; + case ENAMETOOLONG: + case EMFILE: + case ENFILE: + case ENOMEM: + exitstatus = EX_OSERR; + break; + } goto failure; } for(struct dirent *proc_ent = readdir(proc_dir); @@ -126,7 +152,8 @@ char *exe_link; ret = asprintf(&exe_link, "/proc/%s/exe", proc_ent->d_name); if(ret == -1){ - perror("asprintf"); + error(0, errno, "asprintf"); + exitstatus = EX_OSERR; goto failure; } @@ -138,8 +165,20 @@ free(exe_link); continue; } - perror("lstat"); + int e = errno; + error(0, errno, "lstat"); free(exe_link); + switch(e){ + case EACCES: + case ENOTDIR: + case ELOOP: + default: + exitstatus = EX_OSFILE; + break; + case ENAMETOOLONG: + exitstatus = EX_OSERR; + break; + } goto failure; } if(not S_ISLNK(exe_stat.st_mode) @@ -163,6 +202,7 @@ proc_dir = NULL; } if(splashy_pid == 0){ + exitstatus = EX_UNAVAILABLE; goto failure; } @@ -174,52 +214,61 @@ sigemptyset(&new_action.sa_mask); ret = sigaddset(&new_action.sa_mask, SIGINT); if(ret == -1){ - perror("sigaddset"); + error(0, errno, "sigaddset"); + exitstatus = EX_OSERR; goto failure; } ret = sigaddset(&new_action.sa_mask, SIGHUP); if(ret == -1){ - perror("sigaddset"); + error(0, errno, "sigaddset"); + exitstatus = EX_OSERR; goto failure; } ret = sigaddset(&new_action.sa_mask, SIGTERM); if(ret == -1){ - perror("sigaddset"); + error(0, errno, "sigaddset"); + exitstatus = EX_OSERR; goto failure; } ret = sigaction(SIGINT, NULL, &old_action); if(ret == -1){ - perror("sigaction"); + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; goto failure; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGINT, &new_action, NULL); if(ret == -1){ - perror("sigaction"); + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; goto failure; } } ret = sigaction(SIGHUP, NULL, &old_action); if(ret == -1){ - perror("sigaction"); + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; goto failure; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGHUP, &new_action, NULL); if(ret == -1){ - perror("sigaction"); + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; goto failure; } } ret = sigaction(SIGTERM, NULL, &old_action); if(ret == -1){ - perror("sigaction"); + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; goto failure; } if(old_action.sa_handler != SIG_IGN){ ret = sigaction(SIGTERM, &new_action, NULL); if(ret == -1){ - perror("sigaction"); + error(0, errno, "sigaction"); + exitstatus = EX_OSERR; goto failure; } } @@ -235,7 +284,8 @@ goto failure; } if(splashy_command_pid == -1){ - perror("fork"); + error(0, errno, "fork"); + exitstatus = EX_OSERR; goto failure; } /* Child */ @@ -243,7 +293,31 @@ if(not interrupted_by_signal){ const char splashy_command[] = "/sbin/splashy_update"; execl(splashy_command, splashy_command, prompt, (char *)NULL); - perror("execl"); + int e = errno; + error(0, errno, "execl"); + switch(e){ + case EACCES: + case ENOENT: + case ENOEXEC: + case EINVAL: + _exit(EX_UNAVAILABLE); + case ENAMETOOLONG: + case E2BIG: + case ENOMEM: + case EFAULT: + case EIO: + case EMFILE: + case ENFILE: + case ETXTBSY: + default: + _exit(EX_OSERR); + case ENOTDIR: + case ELOOP: + case EISDIR: + case ELIBBAD: + case EPERM: + _exit(EX_OSFILE); + } } free(prompt); _exit(EXIT_FAILURE); @@ -268,7 +342,7 @@ goto failure; } if(ret == -1){ - perror("waitpid"); + error(0, errno, "waitpid"); if(errno == ECHILD){ splashy_command_pid = 0; } @@ -306,26 +380,42 @@ the real user ID (_mandos) */ ret = setuid(geteuid()); if(ret == -1){ - perror("setuid"); + error(0, errno, "setuid"); } setsid(); ret = chdir("/"); if(ret == -1){ - perror("chdir"); + error(0, errno, "chdir"); } /* if(fork() != 0){ */ /* _exit(EXIT_SUCCESS); */ /* } */ ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace stdout */ if(ret == -1){ - perror("dup2"); - _exit(EXIT_FAILURE); + error(0, errno, "dup2"); + _exit(EX_OSERR); } - + execl("/sbin/splashy", "/sbin/splashy", "boot", (char *)NULL); - perror("execl"); - _exit(EXIT_FAILURE); + { + int e = errno; + error(0, errno, "execl"); + switch(e){ + case EACCES: + case ENOENT: + case ENOEXEC: + default: + _exit(EX_UNAVAILABLE); + case ENAMETOOLONG: + case E2BIG: + case ENOMEM: + _exit(EX_OSERR); + case ENOTDIR: + case ELOOP: + _exit(EX_OSFILE); + } + } } } @@ -336,17 +426,17 @@ ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, &signal_action, NULL)); if(ret == -1){ - perror("sigaction"); + error(0, errno, "sigaction"); } do { ret = raise(signal_received); } while(ret != 0 and errno == EINTR); if(ret != 0){ - perror("raise"); + error(0, errno, "raise"); abort(); } TEMP_FAILURE_RETRY(pause()); } - return EXIT_FAILURE; + return exitstatus; } === modified file 'plugins.d/usplash.c' --- plugins.d/usplash.c 2009-09-17 04:16:32 +0000 +++ plugins.d/usplash.c 2010-09-26 18:32:58 +0000 @@ -2,8 +2,8 @@ /* * Usplash - Read a password from usplash and output it * - * Copyright © 2008,2009 Teddy Hogeborn - * Copyright © 2008,2009 Björn Påhlsson + * Copyright © 2008-2010 Teddy Hogeborn + * Copyright © 2008-2010 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 @@ -31,22 +31,25 @@ #include /* open(), O_WRONLY, O_RDONLY */ #include /* and, or, not*/ #include /* errno, EINTR */ +#include #include /* size_t, ssize_t, pid_t, DIR, struct dirent */ #include /* NULL */ #include /* strlen(), memcmp() */ -#include /* asprintf(), perror() */ +#include /* asprintf()*/ #include /* close(), write(), readlink(), read(), STDOUT_FILENO, sleep(), fork(), setuid(), geteuid(), setsid(), chdir(), dup2(), STDERR_FILENO, execv() */ #include /* free(), EXIT_FAILURE, realloc(), - EXIT_SUCCESS, malloc(), _exit() */ -#include /* getenv() */ + EXIT_SUCCESS, malloc(), _exit(), + getenv() */ #include /* opendir(), readdir(), closedir() */ #include /* intmax_t, strtoimax() */ #include /* struct stat, lstat(), S_ISLNK */ +#include /* EX_OSERR, EX_UNAVAILABLE */ +#include /* argz_count(), argz_extract() */ sig_atomic_t interrupted_by_signal = 0; int signal_received; @@ -152,7 +155,7 @@ size_t cmdline_len = 0; DIR *proc_dir = opendir("/proc"); if(proc_dir == NULL){ - perror("opendir"); + error(0, errno, "opendir"); return -1; } errno = 0; @@ -180,7 +183,7 @@ char *exe_link; ret = asprintf(&exe_link, "/proc/%s/exe", proc_ent->d_name); if(ret == -1){ - perror("asprintf"); + error(0, errno, "asprintf"); goto fail_find_usplash; } @@ -192,7 +195,7 @@ free(exe_link); continue; } - perror("lstat"); + error(0, errno, "lstat"); free(exe_link); goto fail_find_usplash; } @@ -223,13 +226,13 @@ ret = asprintf(&cmdline_filename, "/proc/%s/cmdline", proc_ent->d_name); if(ret == -1){ - perror("asprintf"); + error(0, errno, "asprintf"); goto fail_find_usplash; } cl_fd = open(cmdline_filename, O_RDONLY); free(cmdline_filename); if(cl_fd == -1){ - perror("open"); + error(0, errno, "open"); goto fail_find_usplash; } } @@ -241,7 +244,7 @@ if(cmdline_len + blocksize > cmdline_allocated){ tmp = realloc(cmdline, cmdline_allocated + blocksize); if(tmp == NULL){ - perror("realloc"); + error(0, errno, "realloc"); close(cl_fd); goto fail_find_usplash; } @@ -252,7 +255,7 @@ sret = read(cl_fd, cmdline + cmdline_len, cmdline_allocated - cmdline_len); if(sret == -1){ - perror("read"); + error(0, errno, "read"); close(cl_fd); goto fail_find_usplash; } @@ -260,14 +263,14 @@ } while(sret != 0); ret = close(cl_fd); if(ret == -1){ - perror("close"); + error(0, errno, "close"); goto fail_find_usplash; } } /* Close directory */ ret = closedir(proc_dir); if(ret == -1){ - perror("closedir"); + error(0, errno, "closedir"); goto fail_find_usplash; } /* Success */ @@ -297,9 +300,11 @@ size_t buf_len = 0; pid_t usplash_pid = -1; bool usplash_accessed = false; + int status = EXIT_FAILURE; /* Default failure exit status */ char *prompt = makeprompt(); if(prompt == NULL){ + status = EX_OSERR; goto failure; } @@ -308,6 +313,7 @@ size_t cmdline_len = 0; usplash_pid = find_usplash(&cmdline, &cmdline_len); if(usplash_pid == 0){ + status = EX_UNAVAILABLE; goto failure; } @@ -319,23 +325,27 @@ sigemptyset(&new_action.sa_mask); ret = sigaddset(&new_action.sa_mask, SIGINT); if(ret == -1){ - perror("sigaddset"); + error(0, errno, "sigaddset"); + status = EX_OSERR; goto failure; } ret = sigaddset(&new_action.sa_mask, SIGHUP); if(ret == -1){ - perror("sigaddset"); + error(0, errno, "sigaddset"); + status = EX_OSERR; goto failure; } ret = sigaddset(&new_action.sa_mask, SIGTERM); if(ret == -1){ - perror("sigaddset"); + error(0, errno, "sigaddset"); + status = EX_OSERR; goto failure; } ret = sigaction(SIGINT, NULL, &old_action); if(ret == -1){ if(errno != EINTR){ - perror("sigaction"); + error(0, errno, "sigaction"); + status = EX_OSERR; } goto failure; } @@ -343,7 +353,8 @@ ret = sigaction(SIGINT, &new_action, NULL); if(ret == -1){ if(errno != EINTR){ - perror("sigaction"); + error(0, errno, "sigaction"); + status = EX_OSERR; } goto failure; } @@ -351,7 +362,8 @@ ret = sigaction(SIGHUP, NULL, &old_action); if(ret == -1){ if(errno != EINTR){ - perror("sigaction"); + error(0, errno, "sigaction"); + status = EX_OSERR; } goto failure; } @@ -359,7 +371,8 @@ ret = sigaction(SIGHUP, &new_action, NULL); if(ret == -1){ if(errno != EINTR){ - perror("sigaction"); + error(0, errno, "sigaction"); + status = EX_OSERR; } goto failure; } @@ -367,7 +380,8 @@ ret = sigaction(SIGTERM, NULL, &old_action); if(ret == -1){ if(errno != EINTR){ - perror("sigaction"); + error(0, errno, "sigaction"); + status = EX_OSERR; } goto failure; } @@ -375,7 +389,8 @@ ret = sigaction(SIGTERM, &new_action, NULL); if(ret == -1){ if(errno != EINTR){ - perror("sigaction"); + error(0, errno, "sigaction"); + status = EX_OSERR; } goto failure; } @@ -386,7 +401,8 @@ /* Write command to FIFO */ if(not usplash_write(&fifo_fd, "TIMEOUT", "0")){ if(errno != EINTR){ - perror("usplash_write"); + error(0, errno, "usplash_write"); + status = EX_OSERR; } goto failure; } @@ -397,7 +413,8 @@ if(not usplash_write(&fifo_fd, "INPUTQUIET", prompt)){ if(errno != EINTR){ - perror("usplash_write"); + error(0, errno, "usplash_write"); + status = EX_OSERR; } goto failure; } @@ -414,7 +431,8 @@ outfifo_fd = open("/dev/.initramfs/usplash_outfifo", O_RDONLY); if(outfifo_fd == -1){ if(errno != EINTR){ - perror("open"); + error(0, errno, "open"); + status = EX_OSERR; } goto failure; } @@ -432,7 +450,8 @@ char *tmp = realloc(buf, buf_allocated + blocksize); if(tmp == NULL){ if(errno != EINTR){ - perror("realloc"); + error(0, errno, "realloc"); + status = EX_OSERR; } goto failure; } @@ -443,7 +462,8 @@ buf_allocated - buf_len); if(sret == -1){ if(errno != EINTR){ - perror("read"); + error(0, errno, "read"); + status = EX_OSERR; } TEMP_FAILURE_RETRY(close(outfifo_fd)); goto failure; @@ -457,7 +477,8 @@ ret = close(outfifo_fd); if(ret == -1){ if(errno != EINTR){ - perror("close"); + error(0, errno, "close"); + status = EX_OSERR; } goto failure; } @@ -469,7 +490,8 @@ if(not usplash_write(&fifo_fd, "TIMEOUT", "15")){ if(errno != EINTR){ - perror("usplash_write"); + error(0, errno, "usplash_write"); + status = EX_OSERR; } goto failure; } @@ -481,7 +503,8 @@ ret = close(fifo_fd); if(ret == -1){ if(errno != EINTR){ - perror("close"); + error(0, errno, "close"); + status = EX_OSERR; } goto failure; } @@ -494,7 +517,8 @@ sret = write(STDOUT_FILENO, buf + written, buf_len - written); if(sret == -1){ if(errno != EINTR){ - perror("write"); + error(0, errno, "write"); + status = EX_OSERR; } goto failure; } @@ -523,14 +547,14 @@ /* If usplash was never accessed, we can stop now */ if(not usplash_accessed){ - return EXIT_FAILURE; + return status; } /* Close FIFO */ if(fifo_fd != -1){ ret = (int)TEMP_FAILURE_RETRY(close(fifo_fd)); if(ret == -1 and errno != EINTR){ - perror("close"); + error(0, errno, "close"); } fifo_fd = -1; } @@ -539,31 +563,19 @@ if(outfifo_fd != -1){ ret = (int)TEMP_FAILURE_RETRY(close(outfifo_fd)); if(ret == -1){ - perror("close"); - } - } - - /* Create argc and argv for new usplash*/ - int cmdline_argc = 0; - char **cmdline_argv = malloc(sizeof(char *)); - { - size_t position = 0; - while(position < cmdline_len){ - char **tmp = realloc(cmdline_argv, - (sizeof(char *) - * (size_t)(cmdline_argc + 2))); - if(tmp == NULL){ - perror("realloc"); - free(cmdline_argv); - return EXIT_FAILURE; - } - cmdline_argv = tmp; - cmdline_argv[cmdline_argc] = cmdline + position; - cmdline_argc++; - position += strlen(cmdline + position) + 1; - } - cmdline_argv[cmdline_argc] = NULL; - } + error(0, errno, "close"); + } + } + + /* Create argv for new usplash*/ + char **cmdline_argv = malloc((argz_count(cmdline, cmdline_len) + 1) + * sizeof(char *)); /* Count args */ + if(cmdline_argv == NULL){ + error(0, errno, "malloc"); + return status; + } + argz_extract(cmdline, cmdline_len, cmdline_argv); /* Create argv */ + /* Kill old usplash */ kill(usplash_pid, SIGTERM); sleep(2); @@ -580,34 +592,38 @@ the real user ID (_mandos) */ ret = setuid(geteuid()); if(ret == -1){ - perror("setuid"); + error(0, errno, "setuid"); } setsid(); ret = chdir("/"); + if(ret == -1){ + error(0, errno, "chdir"); + _exit(EX_OSERR); + } /* if(fork() != 0){ */ /* _exit(EXIT_SUCCESS); */ /* } */ ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */ if(ret == -1){ - perror("dup2"); - _exit(EXIT_FAILURE); + error(0, errno, "dup2"); + _exit(EX_OSERR); } execv(usplash_name, cmdline_argv); if(not interrupted_by_signal){ - perror("execv"); + error(0, errno, "execv"); } free(cmdline); free(cmdline_argv); - _exit(EXIT_FAILURE); + _exit(EX_OSERR); } free(cmdline); free(cmdline_argv); sleep(2); if(not usplash_write(&fifo_fd, "PULSATE", NULL)){ if(errno != EINTR){ - perror("usplash_write"); + error(0, errno, "usplash_write"); } } @@ -615,7 +631,7 @@ if(fifo_fd != -1){ ret = (int)TEMP_FAILURE_RETRY(close(fifo_fd)); if(ret == -1 and errno != EINTR){ - perror("close"); + error(0, errno, "close"); } fifo_fd = -1; } @@ -626,17 +642,17 @@ ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received, &signal_action, NULL)); if(ret == -1){ - perror("sigaction"); + error(0, errno, "sigaction"); } do { ret = raise(signal_received); } while(ret != 0 and errno == EINTR); if(ret != 0){ - perror("raise"); + error(0, errno, "raise"); abort(); } TEMP_FAILURE_RETRY(pause()); } - return EXIT_FAILURE; + return status; }