=== added directory '.bzr-builddeb'
=== added file '.bzr-builddeb/default.conf'
--- .bzr-builddeb/default.conf 1970-01-01 00:00:00 +0000
+++ .bzr-builddeb/default.conf 2008-09-17 00:34:09 +0000
@@ -0,0 +1,2 @@
+[BUILDDEB]
+split = True
=== added file '.bzrignore'
--- .bzrignore 1970-01-01 00:00:00 +0000
+++ .bzrignore 2019-07-27 10:11:45 +0000
@@ -0,0 +1,17 @@
+*.5
+*.8
+*.8mandos
+confdir
+keydir
+statedir
+man
+plugin-runner
+plugins.d/askpass-fifo
+plugins.d/mandos-client
+plugins.d/password-prompt
+plugins.d/splashy
+plugins.d/usplash
+plugins.d/plymouth
+plugin-helpers/mandos-client-iprouteadddel
+dracut-module/password-agent
+.tramp_history
=== added file 'COPYING'
--- COPYING 1970-01-01 00:00:00 +0000
+++ COPYING 2008-08-15 20:17:32 +0000
@@ -0,0 +1,676 @@
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ 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 .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
+
=== added file 'DBUS-API'
--- DBUS-API 1970-01-01 00:00:00 +0000
+++ DBUS-API 2019-02-10 04:20:26 +0000
@@ -0,0 +1,155 @@
+ -*- 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.recompile.Mandos"
+
+
+* Object Paths:
+
+ | Path | Object |
+ |-----------------------+-------------------|
+ | "/" | The Mandos Server |
+
+ (To get a list of paths to client objects, use the standard D-Bus
+ org.freedesktop.DBus.ObjectManager interface, which the server
+ object supports.)
+
+
+* Mandos Server Interface:
+ Interface name: "se.recompile.Mandos"
+
+** Methods:
+*** RemoveClient(o: ObjectPath) → nothing
+ Removes a client
+
+** Signals:
+*** ClientNotFound(s: KeyID, s: Address)
+ A client connected from Address using KeyID, but was
+ rejected because it was not found in the server. The key ID
+ is represented as a string of hexadecimal digits. The address is
+ an IPv4 or IPv6 address in its normal string format.
+
+
+* Mandos Client Interface:
+ Interface name: "se.recompile.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.
+
+** 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 |
+ | Expires (f) | s | Read | N/A |
+ | ExtendedTimeout (a) | t | Read/Write | extended_timeout |
+ | Fingerprint | s | Read | fingerprint |
+ | KeyID | s | Read | key_id |
+ | Host | s | Read/Write | host |
+ | Interval (a) | t | Read/Write | interval |
+ | LastApprovalRequest (g) | s | Read | N/A |
+ | LastCheckedOK (h) | s | Read/Write | N/A |
+ | LastCheckerStatus (i) | n | Read | N/A |
+ | LastEnabled (j) | s | Read | N/A |
+ | Name | s | Read | (Section name) |
+ | Secret (k) | ay | Write | secret (or secfile) |
+ | Timeout (a) | t | Read/Write | timeout |
+
+ a) Represented as milliseconds.
+
+ b) An approval is currently pending.
+
+ c) Changing this property can either start a new checker or abort a
+ running one.
+
+ d) The creation time of this client object, as an RFC 3339 string.
+
+ e) Changing this property enables or disables a client.
+
+ f) The date and time this client will be disabled, as an RFC 3339
+ string, or an empty string if this is not scheduled.
+
+ g) The date and time of the last approval request, as an RFC 3339
+ string, or an empty string if this has not happened.
+
+ h) The date and time a checker was last successful, as an 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.
+
+ i) The exit status of the last checker, -1 if it did not exit
+ cleanly, -2 if a checker has not yet returned.
+
+ j) The date and time this client was last enabled, as an RFC 3339
+ string, or an empty string if this has not happened.
+
+ k) 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.
+
+*** Rejected(s: Reason)
+ This client was not given its secret for a specified Reason.
+
+* Copyright
+
+ Copyright © 2010-2019 Teddy Hogeborn
+ Copyright © 2010-2019 Björn Påhlsson
+
+** License:
+
+ This file is part of Mandos.
+
+ Mandos 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.
+
+ Mandos 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 Mandos. If not, see .
+
+
+#+STARTUP: showall
=== added file 'INSTALL'
--- INSTALL 1970-01-01 00:00:00 +0000
+++ INSTALL 2019-11-03 19:17:57 +0000
@@ -0,0 +1,157 @@
+-*- org -*-
+
+* Prerequisites
+
+** Operating System
+
+ Debian 8.0 "jessie" or Ubuntu 15.10 "Wily Werewolf" (or later).
+
+ This is mostly for the support scripts which make sure that the
+ client is installed and started in the initial RAM disk environment
+ and that the initial RAM file system image file is automatically
+ made unreadable. The server and client programs themselves *could*
+ be run in other distributions, but they *are* specific to GNU/Linux
+ systems, and are not written with portabillity to other Unixes in
+ mind.
+
+** Libraries
+
+ The following libraries and packages are needed. (It is possible
+ that it might work with older versions of some of these, but these
+ versions are confirmed to work. Newer versions are almost
+ certainly OK.)
+
+*** Documentation
+ These are required to build the manual pages for both the server
+ and client:
+
+ + DocBook 4.5 http://www.docbook.org/
+ Note: DocBook 5.0 is not compatible.
+ + DocBook XSL stylesheets 1.71.0
+ http://wiki.docbook.org/DocBookXslStylesheets
+
+ Package names:
+ docbook docbook-xsl
+
+ To build just the documentation, run the command "make doc". Then
+ the manual page "mandos.8", for example, can be read by running
+ "man -l mandos.8".
+
+*** Mandos Server
+ + GnuTLS 3.3 https://www.gnutls.org/
+ (but not 3.6.0 or later, until 3.6.6, which works)
+ + Avahi 0.6.16 https://www.avahi.org/
+ + Python 3 https://www.python.org/
+ Note: Python 2.7 is still supported, if the "mandos",
+ "mandos-ctl", and "mandos-monitor" files are edited to contain
+ "#!/usr/bin/python" instead of python3.
+ + dbus-python 0.82.4 https://dbus.freedesktop.org/doc/dbus-python/
+ + PyGObject 3.8 https://wiki.gnome.org/Projects/PyGObject
+ + pkg-config https://www.freedesktop.org/wiki/Software/pkg-config/
+ + Urwid 1.0.1 http://urwid.org/
+ (Only needed by the "mandos-monitor" tool.)
+
+ Strongly recommended:
+ + fping 2.4b2-to-ipv6 http://www.fping.org/
+ + ssh-keyscan from OpenSSH http://www.openssh.com/
+
+ Package names:
+ avahi-daemon python3 python3-dbus python3-gi python3-urwid
+ pkg-config fping ssh-client
+
+*** Mandos Client
+ + GNU C Library 2.17 https://gnu.org/software/libc/
+ + GnuTLS 3.3 https://www.gnutls.org/
+ (but not 3.6.0 or later, until 3.6.6 which works)
+ + Avahi 0.6.16 https://www.avahi.org/
+ + GnuPG 1.4.9 https://www.gnupg.org/
+ + GPGME 1.1.6 https://www.gnupg.org/related_software/gpgme/
+ + pkg-config https://www.freedesktop.org/wiki/Software/pkg-config/
+ + libnl-route 3 https://www.infradead.org/~tgr/libnl/
+ + GLib 2.40 http://www.gtk.org/
+
+ One of:
+ + initramfs-tools 0.85i
+ https://tracker.debian.org/pkg/initramfs-tools
+ + dracut 044+241
+ http://www.kernel.org/pub/linux/utils/boot/dracut/dracut.html
+
+ Strongly recommended:
+ + OpenSSH http://www.openssh.com/
+
+ Package names:
+ initramfs-tools dracut libgnutls-dev gnutls-bin libavahi-core-dev
+ gnupg libgpgme11-dev pkg-config ssh libnl-route-3-dev
+ libglib2.0-dev
+
+* Installing the Mandos server
+
+ 1. Do "make doc".
+
+ 2. On the computer to run as a Mandos server, run the following
+ command:
+ For Debian: su - -c 'make install-server'
+ For Ubuntu: sudo make install-server
+
+ (This creates a configuration without any clients configured; you
+ need an actually configured client to do that; see below.)
+
+* Installing the Mandos client.
+
+ 1. Do "make all doc".
+
+ 2. On the computer to run as a Mandos client, run the following
+ command:
+ For Debian: su - -c 'make install-client'
+ For Ubuntu: sudo make install-client
+
+ This will also create an OpenPGP key, which will take some time
+ and entropy, so be patient.
+
+ 3. Run the following command:
+ For Debian: su - -c 'mandos-keygen --password'
+ For Ubuntu: sudo mandos-keygen --password
+
+ When prompted, enter the password/passphrase for the encrypted
+ root file system on this client computer. The command will
+ output a section of text, starting with a [section header]. Copy
+ and append this to the file "/etc/mandos/clients.conf" *on the
+ server computer*.
+
+ 4. Configure the client to use any special configuration needed for
+ your local system. Note: This is not necessary if the server is
+ present on the same wired local network as the client. If you do
+ make changes to /etc/mandos/plugin-runner.conf, the initrd.img
+ file must be updated, possibly using the following command:
+
+ # update-initramfs -k all -u
+
+ 5. On the server computer, start the server by running the command
+ For Debian: su - -c 'invoke-rc.d mandos start'
+ For Ubuntu: sudo service mandos start
+
+ At this point, it is possible to verify that the correct password
+ will be received by the client by running the command:
+
+ # /usr/lib/mandos/plugins.d/mandos-client \
+ --pubkey=/etc/keys/mandos/pubkey.txt \
+ --seckey=/etc/keys/mandos/seckey.txt \
+ --tls-privkey=/etc/keys/mandos/tls-privkey.pem \
+ --tls-pubkey=/etc/keys/mandos/tls-pubkey.pem; echo
+
+ This command should retrieve the password from the server,
+ decrypt it, and output it to standard output.
+
+ After this, the client computer should be able to reboot without
+ needing a password entered on the console, as long as it does not
+ take more than five minutes to reboot.
+
+* Further customizations
+
+ You may want to tighten or loosen the timeouts in the server
+ configuration files; see mandos.conf(5) and mandos-clients.conf(5).
+ If IPsec is not used and SSH is not installed, it is suggested that
+ a more cryptographically secure checker program is used and
+ configured, since, without IPsec, ping packets can be faked.
+
+#+STARTUP: showall
=== modified file 'Makefile'
--- Makefile 2008-08-10 17:52:54 +0000
+++ Makefile 2019-09-03 19:06:41 +0000
@@ -1,62 +1,605 @@
-WARN=-O -Wall -Wformat=2 -Winit-self -Wmissing-include-dirs -Wswitch-default -Wswitch-enum -Wunused-parameter -Wstrict-aliasing=2 -Wextra -Wfloat-equal -Wundef -Wshadow -Wunsafe-loop-optimizations -Wpointer-arith -Wbad-function-cast -Wcast-qual -Wcast-align -Wwrite-strings -Wconversion -Wstrict-prototypes -Wold-style-definition -Wpacked -Wnested-externs -Wunreachable-code -Winline -Wvolatile-register-var
-DEBUG=-ggdb3
-# For info about _FORTIFY_SOURCE, see
-#
-FORTIFY=-D_FORTIFY_SOURCE=2 # -fstack-protector-all
+WARN:=-O -Wall -Wextra -Wdouble-promotion -Wformat=2 -Winit-self \
+ -Wmissing-include-dirs -Wswitch-default -Wswitch-enum \
+ -Wunused -Wuninitialized -Wstrict-overflow=5 \
+ -Wsuggest-attribute=pure -Wsuggest-attribute=const \
+ -Wsuggest-attribute=noreturn -Wfloat-equal -Wundef -Wshadow \
+ -Wunsafe-loop-optimizations -Wpointer-arith \
+ -Wbad-function-cast -Wcast-qual -Wcast-align -Wwrite-strings \
+ -Wconversion -Wlogical-op -Waggregate-return \
+ -Wstrict-prototypes -Wold-style-definition \
+ -Wmissing-format-attribute -Wnormalized=nfc -Wpacked \
+ -Wredundant-decls -Wnested-externs -Winline -Wvla \
+ -Wvolatile-register-var -Woverlength-strings
+
+#DEBUG:=-ggdb3 -fsanitize=address $(SANITIZE)
+## Check which sanitizing options can be used
+#SANITIZE:=$(foreach option,$(ALL_SANITIZE_OPTIONS),$(shell \
+# echo 'int main(){}' | $(CC) --language=c $(option) \
+# /dev/stdin -o /dev/null >/dev/null 2>&1 && echo $(option)))
+#
+ALL_SANITIZE_OPTIONS:=-fsanitize=leak -fsanitize=undefined \
+ -fsanitize=shift -fsanitize=integer-divide-by-zero \
+ -fsanitize=unreachable -fsanitize=vla-bound -fsanitize=null \
+ -fsanitize=return -fsanitize=signed-integer-overflow \
+ -fsanitize=bounds -fsanitize=alignment \
+ -fsanitize=object-size -fsanitize=float-divide-by-zero \
+ -fsanitize=float-cast-overflow -fsanitize=nonnull-attribute \
+ -fsanitize=returns-nonnull-attribute -fsanitize=bool \
+ -fsanitize=enum -fsanitize-address-use-after-scope
+
+# For info about _FORTIFY_SOURCE, see feature_test_macros(7)
+# and .
+FORTIFY:=-D_FORTIFY_SOURCE=2 -fstack-protector-all -fPIC
+LINK_FORTIFY_LD:=-z relro -z now
+LINK_FORTIFY:=
+
+# If BROKEN_PIE is set, do not build with -pie
+ifndef BROKEN_PIE
+FORTIFY += -fPIE
+LINK_FORTIFY += -pie
+endif
#COVERAGE=--coverage
-OPTIMIZE=-Os
-LANGUAGE=-std=gnu99
+OPTIMIZE:=-Os -fno-strict-aliasing
+LANGUAGE:=-std=gnu11
+FEATURES:=-D_FILE_OFFSET_BITS=64
+htmldir:=man
+version:=1.8.9
+SED:=sed
+PKG_CONFIG?=pkg-config
+
+USER:=$(firstword $(subst :, ,$(shell getent passwd _mandos \
+ || getent passwd nobody || echo 65534)))
+GROUP:=$(firstword $(subst :, ,$(shell getent group _mandos \
+ || getent group nogroup || echo 65534)))
+
+LINUXVERSION:=$(shell uname --kernel-release)
+
+## Use these settings for a traditional /usr/local install
+# PREFIX:=$(DESTDIR)/usr/local
+# CONFDIR:=$(DESTDIR)/etc/mandos
+# KEYDIR:=$(DESTDIR)/etc/mandos/keys
+# MANDIR:=$(PREFIX)/man
+# INITRAMFSTOOLS:=$(DESTDIR)/etc/initramfs-tools
+# DRACUTMODULE:=$(DESTDIR)/usr/lib/dracut/modules.d/90mandos
+# STATEDIR:=$(DESTDIR)/var/lib/mandos
+# LIBDIR:=$(PREFIX)/lib
+##
+
+## These settings are for a package-type install
+PREFIX:=$(DESTDIR)/usr
+CONFDIR:=$(DESTDIR)/etc/mandos
+KEYDIR:=$(DESTDIR)/etc/keys/mandos
+MANDIR:=$(PREFIX)/share/man
+INITRAMFSTOOLS:=$(DESTDIR)/usr/share/initramfs-tools
+DRACUTMODULE:=$(DESTDIR)/usr/lib/dracut/modules.d/90mandos
+STATEDIR:=$(DESTDIR)/var/lib/mandos
+LIBDIR:=$(shell \
+ for d in \
+ "/usr/lib/`dpkg-architecture \
+ -qDEB_HOST_MULTIARCH 2>/dev/null`" \
+ "`rpm --eval='%{_libdir}' 2>/dev/null`" /usr/lib; do \
+ if [ -d "$$d" -a "$$d" = "$${d%/}" ]; then \
+ echo "$(DESTDIR)$$d"; \
+ break; \
+ fi; \
+ done)
+##
+
+SYSTEMD:=$(DESTDIR)$(shell $(PKG_CONFIG) systemd \
+ --variable=systemdsystemunitdir)
+TMPFILES:=$(DESTDIR)$(shell $(PKG_CONFIG) systemd \
+ --variable=tmpfilesdir)
+SYSUSERS:=$(DESTDIR)$(shell $(PKG_CONFIG) systemd \
+ --variable=sysusersdir)
+
+GNUTLS_CFLAGS:=$(shell $(PKG_CONFIG) --cflags-only-I gnutls)
+GNUTLS_LIBS:=$(shell $(PKG_CONFIG) --libs gnutls)
+AVAHI_CFLAGS:=$(shell $(PKG_CONFIG) --cflags-only-I avahi-core)
+AVAHI_LIBS:=$(shell $(PKG_CONFIG) --libs avahi-core)
+GPGME_CFLAGS:=$(shell gpgme-config --cflags; getconf LFS_CFLAGS)
+GPGME_LIBS:=$(shell gpgme-config --libs; getconf LFS_LIBS; \
+ getconf LFS_LDFLAGS)
+LIBNL3_CFLAGS:=$(shell $(PKG_CONFIG) --cflags-only-I libnl-route-3.0)
+LIBNL3_LIBS:=$(shell $(PKG_CONFIG) --libs libnl-route-3.0)
+GLIB_CFLAGS:=$(shell $(PKG_CONFIG) --cflags glib-2.0)
+GLIB_LIBS:=$(shell $(PKG_CONFIG) --libs glib-2.0)
# Do not change these two
-CFLAGS=$(WARN) $(DEBUG) $(FORTIFY) $(COVERAGE) $(OPTIMIZE) $(LANGUAGE)
-LDFLAGS=$(COVERAGE)
+CFLAGS+=$(WARN) $(DEBUG) $(FORTIFY) $(COVERAGE) $(OPTIMIZE) \
+ $(LANGUAGE) $(FEATURES) -DVERSION='"$(version)"'
+LDFLAGS+=-Xlinker --as-needed $(COVERAGE) $(LINK_FORTIFY) $(strip \
+ ) $(foreach flag,$(LINK_FORTIFY_LD),-Xlinker $(flag))
-DOCBOOKTOMAN=xsltproc --nonet \
+# Commands to format a DocBook document into a manual page
+DOCBOOKTOMAN=$(strip cd $(dir $<); xsltproc --nonet --xinclude \
--param man.charmap.use.subset 0 \
--param make.year.ranges 1 \
--param make.single.year.ranges 1 \
--param man.output.quietly 1 \
- --param man.authors.section.enabled 0
-
-PROGS=mandos-client plugins.d/password-request plugins.d/password-prompt
-DOCS=mandos.8 mandos-client.8mandos plugins.d/password-request.8mandos plugins.d/password-prompt.8mandos mandos.conf.5 mandos-clients.conf.5
-
-objects=$(shell for p in $(PROGS); do echo $${p}.o; done)
-
-all: $(PROGS) $(DOCS)
-
-%.5: %.xml
- cd $(shell dirname $^); $(DOCBOOKTOMAN) $(shell basename $^)
-
-%.8: %.xml
- cd $(shell dirname $^); $(DOCBOOKTOMAN) $(shell basename $^)
-
-%.8mandos: %.xml
- cd $(shell dirname $^); $(DOCBOOKTOMAN) $(shell basename $^)
-
-mandos-client: mandos-client.o
- $(LINK.o) -lgnutls $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@
-
-plugins.d/password-request: plugins.d/password-request.o
- $(LINK.o) -lgnutls -lavahi-core -lgpgme $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@
-
-plugins.d/password-prompt: plugins.d/password-prompt.o
- $(LINK.o) $(COMMON) $^ $(LOADLIBES) $(LDLIBS) -o $@
-
-.PHONY : all clean distclean run-client run-server
+ --param man.authors.section.enabled 0 \
+ /usr/share/xml/docbook/stylesheet/nwalsh/manpages/docbook.xsl \
+ $(notdir $<); \
+ if locale --all 2>/dev/null | grep --regexp='^en_US\.utf8$$' \
+ && command -v man >/dev/null; then LANG=en_US.UTF-8 \
+ MANWIDTH=80 man --warnings --encoding=UTF-8 --local-file \
+ $(notdir $@); fi >/dev/null)
+
+DOCBOOKTOHTML=$(strip xsltproc --nonet --xinclude \
+ --param make.year.ranges 1 \
+ --param make.single.year.ranges 1 \
+ --param man.output.quietly 1 \
+ --param man.authors.section.enabled 0 \
+ --param citerefentry.link 1 \
+ --output $@ \
+ /usr/share/xml/docbook/stylesheet/nwalsh/xhtml/docbook.xsl \
+ $<; $(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/plymouth
+PLUGIN_HELPERS:=plugin-helpers/mandos-client-iprouteadddel
+CPROGS:=plugin-runner dracut-module/password-agent $(PLUGINS) \
+ $(PLUGIN_HELPERS)
+PROGS:=mandos mandos-keygen mandos-ctl mandos-monitor $(CPROGS)
+DOCS:=mandos.8 mandos-keygen.8 mandos-monitor.8 mandos-ctl.8 \
+ mandos.conf.5 mandos-clients.conf.5 plugin-runner.8mandos \
+ dracut-module/password-agent.8mandos \
+ plugins.d/mandos-client.8mandos \
+ plugins.d/password-prompt.8mandos plugins.d/usplash.8mandos \
+ plugins.d/splashy.8mandos plugins.d/askpass-fifo.8mandos \
+ plugins.d/plymouth.8mandos intro.8mandos
+
+htmldocs:=$(addsuffix .xhtml,$(DOCS))
+
+objects:=$(addsuffix .o,$(CPROGS))
+
+all: $(PROGS) mandos.lsm
+
+doc: $(DOCS)
+
+html: $(htmldocs)
+
+%.5: %.xml common.ent legalnotice.xml
+ $(DOCBOOKTOMAN)
+%.5.xhtml: %.xml common.ent legalnotice.xml
+ $(DOCBOOKTOHTML)
+
+%.8: %.xml common.ent legalnotice.xml
+ $(DOCBOOKTOMAN)
+%.8.xhtml: %.xml common.ent legalnotice.xml
+ $(DOCBOOKTOHTML)
+
+%.8mandos: %.xml common.ent legalnotice.xml
+ $(DOCBOOKTOMAN)
+%.8mandos.xhtml: %.xml common.ent legalnotice.xml
+ $(DOCBOOKTOHTML)
+
+intro.8mandos: intro.xml common.ent legalnotice.xml
+ $(DOCBOOKTOMAN)
+intro.8mandos.xhtml: intro.xml common.ent legalnotice.xml
+ $(DOCBOOKTOHTML)
+
+mandos.8: mandos.xml common.ent mandos-options.xml overview.xml \
+ legalnotice.xml
+ $(DOCBOOKTOMAN)
+mandos.8.xhtml: mandos.xml common.ent mandos-options.xml \
+ overview.xml legalnotice.xml
+ $(DOCBOOKTOHTML)
+
+mandos-keygen.8: mandos-keygen.xml common.ent overview.xml \
+ legalnotice.xml
+ $(DOCBOOKTOMAN)
+mandos-keygen.8.xhtml: mandos-keygen.xml common.ent overview.xml \
+ 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)
+mandos.conf.5.xhtml: mandos.conf.xml common.ent mandos-options.xml \
+ legalnotice.xml
+ $(DOCBOOKTOHTML)
+
+plugin-runner.8mandos: plugin-runner.xml common.ent overview.xml \
+ legalnotice.xml
+ $(DOCBOOKTOMAN)
+plugin-runner.8mandos.xhtml: plugin-runner.xml common.ent \
+ overview.xml legalnotice.xml
+ $(DOCBOOKTOHTML)
+
+dracut-module/password-agent.8mandos: \
+ dracut-module/password-agent.xml common.ent \
+ overview.xml legalnotice.xml
+ $(DOCBOOKTOMAN)
+dracut-module/password-agent.8mandos.xhtml: \
+ dracut-module/password-agent.xml common.ent \
+ overview.xml legalnotice.xml
+ $(DOCBOOKTOHTML)
+
+plugins.d/mandos-client.8mandos: plugins.d/mandos-client.xml \
+ common.ent \
+ mandos-options.xml \
+ overview.xml legalnotice.xml
+ $(DOCBOOKTOMAN)
+plugins.d/mandos-client.8mandos.xhtml: plugins.d/mandos-client.xml \
+ common.ent \
+ mandos-options.xml \
+ overview.xml legalnotice.xml
+ $(DOCBOOKTOHTML)
+
+# Update all these files with version number $(version)
+common.ent: Makefile
+ $(strip $(SED) --in-place \
+ --expression='s/^\($$/\1$(version)">/' \
+ $@)
+
+mandos: Makefile
+ $(strip $(SED) --in-place \
+ --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \
+ $@)
+
+mandos-keygen: Makefile
+ $(strip $(SED) --in-place \
+ --expression='s/^\(VERSION="\)[^"]*"$$/\1$(version)"/' \
+ $@)
+
+mandos-ctl: Makefile
+ $(strip $(SED) --in-place \
+ --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \
+ $@)
+
+mandos-monitor: Makefile
+ $(strip $(SED) --in-place \
+ --expression='s/^\(version = "\)[^"]*"$$/\1$(version)"/' \
+ $@)
+
+mandos.lsm: Makefile
+ $(strip $(SED) --in-place \
+ --expression='s/^\(Version:\).*/\1\t$(version)/' \
+ $@)
+ $(strip $(SED) --in-place \
+ --expression='s/^\(Entered-date:\).*/\1\t$(shell date --rfc-3339=date --reference=Makefile)/' \
+ $@)
+ $(strip $(SED) --in-place \
+ --expression='s/\(mandos_\)[0-9.]\+\(\.orig\.tar\.gz\)/\1$(version)\2/' \
+ $@)
+
+# Need to add the GnuTLS, Avahi and GPGME libraries
+plugins.d/mandos-client: plugins.d/mandos-client.c
+ $(LINK.c) $^ $(GNUTLS_CFLAGS) $(AVAHI_CFLAGS) $(strip\
+ ) $(GPGME_CFLAGS) $(GNUTLS_LIBS) $(strip\
+ ) $(AVAHI_LIBS) $(GPGME_LIBS) $(LOADLIBES) $(strip\
+ ) $(LDLIBS) -o $@
+
+# Need to add the libnl-route library
+plugin-helpers/mandos-client-iprouteadddel: plugin-helpers/mandos-client-iprouteadddel.c
+ $(LINK.c) $(LIBNL3_CFLAGS) $^ $(LIBNL3_LIBS) $(strip\
+ ) $(LOADLIBES) $(LDLIBS) -o $@
+
+# Need to add the GLib and pthread libraries
+dracut-module/password-agent: dracut-module/password-agent.c
+ $(LINK.c) $(GLIB_CFLAGS) $^ $(GLIB_LIBS) -lpthread $(strip\
+ ) $(LOADLIBES) $(LDLIBS) -o $@
+
+.PHONY : all doc html clean distclean mostlyclean maintainer-clean \
+ check run-client run-server install install-html \
+ install-server install-client-nokey install-client uninstall \
+ uninstall-server uninstall-client purge purge-server \
+ purge-client
clean:
- -rm --force $(PROGS) $(objects) $(DOCS) core
+ -rm --force $(CPROGS) $(objects) $(htmldocs) $(DOCS) core
distclean: clean
mostlyclean: clean
maintainer-clean: clean
+ -rm --force --recursive keydir confdir statedir
check: all
./mandos --check
-
-run-client: all
- ./mandos-client --plugin-dir=plugins.d --options-for=password-request:--keydir=keydir
-
-run-server: all
- ./mandos --debug --configdir=.
+ ./mandos-ctl --check
+ ./mandos-keygen --version
+ ./plugin-runner --version
+ ./plugin-helpers/mandos-client-iprouteadddel --version
+ ./dracut-module/password-agent --test
+
+# Run the client with a local config and key
+run-client: all keydir/seckey.txt keydir/pubkey.txt \
+ keydir/tls-privkey.pem keydir/tls-pubkey.pem
+ @echo '######################################################'
+ @echo '# The following error messages are harmless and can #'
+ @echo '# be safely ignored: #'
+ @echo '## From plugin-runner: #'
+ @echo '# setgid: Operation not permitted #'
+ @echo '# setuid: Operation not permitted #'
+ @echo '## From askpass-fifo: #'
+ @echo '# mkfifo: Permission denied #'
+ @echo '## From mandos-client: #'
+ @echo '# Failed to raise privileges: Operation not permi... #'
+ @echo '# Warning: network hook "*" exited with status * #'
+ @echo '# ioctl SIOCSIFFLAGS +IFF_UP: Operation not permi... #'
+ @echo '# Failed to bring up interface "*": Operation not... #'
+ @echo '# #'
+ @echo '# (The messages are caused by not running as root, #'
+ @echo '# but you should NOT run "make run-client" as root #'
+ @echo '# unless you also unpacked and compiled Mandos as #'
+ @echo '# root, which is also NOT recommended.) #'
+ @echo '######################################################'
+# We set GNOME_KEYRING_CONTROL to block pam_gnome_keyring
+ ./plugin-runner --plugin-dir=plugins.d \
+ --plugin-helper-dir=plugin-helpers \
+ --config-file=plugin-runner.conf \
+ --options-for=mandos-client:--seckey=keydir/seckey.txt,--pubkey=keydir/pubkey.txt,--tls-privkey=keydir/tls-privkey.pem,--tls-pubkey=keydir/tls-pubkey.pem,--network-hook-dir=network-hooks.d \
+ --env-for=mandos-client:GNOME_KEYRING_CONTROL= \
+ $(CLIENTARGS)
+
+# Used by run-client
+keydir/seckey.txt keydir/pubkey.txt keydir/tls-privkey.pem keydir/tls-pubkey.pem: mandos-keygen
+ install --directory keydir
+ ./mandos-keygen --dir keydir --force
+
+# Run the server with a local config
+run-server: confdir/mandos.conf confdir/clients.conf statedir
+ ./mandos --debug --no-dbus --configdir=confdir \
+ --statedir=statedir $(SERVERARGS)
+
+# Used by run-server
+confdir/mandos.conf: mandos.conf
+ install --directory confdir
+ install --mode=u=rw,go=r $^ $@
+confdir/clients.conf: clients.conf keydir/seckey.txt keydir/tls-pubkey.pem
+ install --directory confdir
+ install --mode=u=rw $< $@
+# Add a client password
+ ./mandos-keygen --dir keydir --password --no-ssh >> $@
+statedir:
+ install --directory statedir
+
+install: install-server install-client-nokey
+
+install-html: html
+ install --directory $(htmldir)
+ install --mode=u=rw,go=r --target-directory=$(htmldir) \
+ $(htmldocs)
+
+install-server: doc
+ install --directory $(CONFDIR)
+ if install --directory --mode=u=rwx --owner=$(USER) \
+ --group=$(GROUP) $(STATEDIR); then \
+ :; \
+ elif install --directory --mode=u=rwx $(STATEDIR); then \
+ chown -- $(USER):$(GROUP) $(STATEDIR) || :; \
+ fi
+ if [ "$(TMPFILES)" != "$(DESTDIR)" \
+ -a -d "$(TMPFILES)" ]; then \
+ install --mode=u=rw,go=r tmpfiles.d-mandos.conf \
+ $(TMPFILES)/mandos.conf; \
+ fi
+ if [ "$(SYSUSERS)" != "$(DESTDIR)" \
+ -a -d "$(SYSUSERS)" ]; then \
+ install --mode=u=rw,go=r sysusers.d-mandos.conf \
+ $(SYSUSERS)/mandos.conf; \
+ fi
+ 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
+ if [ "$(SYSTEMD)" != "$(DESTDIR)" -a -d "$(SYSTEMD)" ]; then \
+ install --mode=u=rw,go=r mandos.service $(SYSTEMD); \
+ fi
+ install --mode=u=rw,go=r default-mandos \
+ $(DESTDIR)/etc/default/mandos
+ if [ -z $(DESTDIR) ]; then \
+ update-rc.d mandos defaults 25 15;\
+ 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 \
+ > $(MANDIR)/man5/mandos-clients.conf.5.gz
+ gzip --best --to-stdout intro.8mandos \
+ > $(MANDIR)/man8/intro.8mandos.gz
+
+install-client-nokey: all doc
+ install --directory $(LIBDIR)/mandos $(CONFDIR)
+ install --directory --mode=u=rwx $(KEYDIR) \
+ $(LIBDIR)/mandos/plugins.d \
+ $(LIBDIR)/mandos/plugin-helpers
+ if [ "$(SYSUSERS)" != "$(DESTDIR)" \
+ -a -d "$(SYSUSERS)" ]; then \
+ install --mode=u=rw,go=r sysusers.d-mandos.conf \
+ $(SYSUSERS)/mandos-client.conf; \
+ fi
+ if [ "$(CONFDIR)" != "$(LIBDIR)/mandos" ]; then \
+ install --mode=u=rwx \
+ --directory "$(CONFDIR)/plugins.d" \
+ "$(CONFDIR)/plugin-helpers"; \
+ fi
+ install --mode=u=rwx,go=rx --directory \
+ "$(CONFDIR)/network-hooks.d"
+ install --mode=u=rwx,go=rx \
+ --target-directory=$(LIBDIR)/mandos plugin-runner
+ install --mode=u=rwx,go=rx \
+ --target-directory=$(LIBDIR)/mandos \
+ mandos-to-cryptroot-unlock
+ install --mode=u=rwx,go=rx --target-directory=$(PREFIX)/sbin \
+ mandos-keygen
+ install --mode=u=rwx,go=rx \
+ --target-directory=$(LIBDIR)/mandos/plugins.d \
+ plugins.d/password-prompt
+ install --mode=u=rwxs,go=rx \
+ --target-directory=$(LIBDIR)/mandos/plugins.d \
+ plugins.d/mandos-client
+ install --mode=u=rwxs,go=rx \
+ --target-directory=$(LIBDIR)/mandos/plugins.d \
+ plugins.d/usplash
+ install --mode=u=rwxs,go=rx \
+ --target-directory=$(LIBDIR)/mandos/plugins.d \
+ plugins.d/splashy
+ install --mode=u=rwxs,go=rx \
+ --target-directory=$(LIBDIR)/mandos/plugins.d \
+ plugins.d/askpass-fifo
+ install --mode=u=rwxs,go=rx \
+ --target-directory=$(LIBDIR)/mandos/plugins.d \
+ plugins.d/plymouth
+ install --mode=u=rwx,go=rx \
+ --target-directory=$(LIBDIR)/mandos/plugin-helpers \
+ plugin-helpers/mandos-client-iprouteadddel
+ install initramfs-tools-hook \
+ $(INITRAMFSTOOLS)/hooks/mandos
+ install --mode=u=rw,go=r initramfs-tools-conf \
+ $(INITRAMFSTOOLS)/conf.d/mandos-conf
+ install --mode=u=rw,go=r initramfs-tools-conf-hook \
+ $(INITRAMFSTOOLS)/conf-hooks.d/zz-mandos
+ install initramfs-tools-script \
+ $(INITRAMFSTOOLS)/scripts/init-premount/mandos
+ install initramfs-tools-script-stop \
+ $(INITRAMFSTOOLS)/scripts/local-premount/mandos
+ install --directory $(DRACUTMODULE)
+ install --mode=u=rw,go=r --target-directory=$(DRACUTMODULE) \
+ dracut-module/ask-password-mandos.path \
+ dracut-module/ask-password-mandos.service
+ install --mode=u=rwxs,go=rx \
+ --target-directory=$(DRACUTMODULE) \
+ dracut-module/module-setup.sh \
+ dracut-module/cmdline-mandos.sh \
+ dracut-module/password-agent
+ install --mode=u=rw,go=r plugin-runner.conf $(CONFDIR)
+ gzip --best --to-stdout mandos-keygen.8 \
+ > $(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/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
+ gzip --best --to-stdout dracut-module/password-agent.8mandos \
+ > $(MANDIR)/man8/password-agent.8mandos.gz
+
+install-client: install-client-nokey
+# Post-installation stuff
+ -$(PREFIX)/sbin/mandos-keygen --dir "$(KEYDIR)"
+ if command -v update-initramfs >/dev/null; then \
+ update-initramfs -k all -u; \
+ elif command -v dracut >/dev/null; then \
+ for initrd in $(DESTDIR)/boot/initr*-$(LINUXVERSION); do \
+ if [ -w "$$initrd" ]; then \
+ chmod go-r "$$initrd"; \
+ dracut --force "$$initrd"; \
+ fi; \
+ done; \
+ fi
+ echo "Now run mandos-keygen --password --dir $(KEYDIR)"
+
+uninstall: uninstall-server uninstall-client
+
+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
+ -rmdir $(CONFDIR)
+
+uninstall-client:
+# Refuse to uninstall client if /etc/crypttab is explicitly configured
+# to use it.
+ ! grep --regexp='^ *[^ #].*keyscript=[^,=]*/mandos/' \
+ $(DESTDIR)/etc/crypttab
+ -rm --force $(PREFIX)/sbin/mandos-keygen \
+ $(LIBDIR)/mandos/plugin-runner \
+ $(LIBDIR)/mandos/plugins.d/password-prompt \
+ $(LIBDIR)/mandos/plugins.d/mandos-client \
+ $(LIBDIR)/mandos/plugins.d/usplash \
+ $(LIBDIR)/mandos/plugins.d/splashy \
+ $(LIBDIR)/mandos/plugins.d/askpass-fifo \
+ $(LIBDIR)/mandos/plugins.d/plymouth \
+ $(INITRAMFSTOOLS)/hooks/mandos \
+ $(INITRAMFSTOOLS)/conf-hooks.d/mandos \
+ $(INITRAMFSTOOLS)/scripts/init-premount/mandos \
+ $(INITRAMFSTOOLS)/scripts/local-premount/mandos \
+ $(DRACUTMODULE)/ask-password-mandos.path \
+ $(DRACUTMODULE)/ask-password-mandos.service \
+ $(DRACUTMODULE)/module-setup.sh \
+ $(DRACUTMODULE)/cmdline-mandos.sh \
+ $(DRACUTMODULE)/password-agent \
+ $(MANDIR)/man8/mandos-keygen.8.gz \
+ $(MANDIR)/man8/plugin-runner.8mandos.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/plymouth.8mandos.gz \
+ $(MANDIR)/man8/password-agent.8mandos.gz \
+ -rmdir $(LIBDIR)/mandos/plugins.d $(CONFDIR)/plugins.d \
+ $(LIBDIR)/mandos $(CONFDIR) $(KEYDIR) $(DRACUTMODULE)
+ if command -v update-initramfs >/dev/null; then \
+ update-initramfs -k all -u; \
+ elif command -v dracut >/dev/null; then \
+ for initrd in $(DESTDIR)/boot/initr*-$(LINUXVERSION); do \
+ test -w "$$initrd" && dracut --force "$$initrd"; \
+ done; \
+ fi
+
+purge: purge-server purge-client
+
+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 \
+ $(SYSTEMD)/mandos.service \
+ $(DESTDIR)/run/mandos.pid \
+ $(DESTDIR)/var/run/mandos.pid
+ -rmdir $(CONFDIR)
+
+purge-client: uninstall-client
+ -shred --remove $(KEYDIR)/seckey.txt $(KEYDIR)/tls-privkey.pem
+ -rm --force $(CONFDIR)/plugin-runner.conf \
+ $(KEYDIR)/pubkey.txt $(KEYDIR)/seckey.txt \
+ $(KEYDIR)/tls-pubkey.txt $(KEYDIR)/tls-privkey.txt
+ -rmdir $(KEYDIR) $(CONFDIR)/plugins.d $(CONFDIR)
=== added file 'NEWS'
--- NEWS 1970-01-01 00:00:00 +0000
+++ NEWS 2019-09-03 19:06:41 +0000
@@ -0,0 +1,570 @@
+This NEWS file records noteworthy changes, very tersely.
+See the manual for detailed information.
+
+Version 1.8.9 (2019-09-03)
+* No user-visible changes
+
+Version 1.8.8 (2019-08-18)
+* No user-visible changes
+
+Version 1.8.7 (2019-08-05)
+* Client:
+** Always compile with LFS (Large File Support) enabled.
+* Server
+** Improve intro(8mandos) manual page to cover dracut(8) support.
+
+Version 1.8.6 (2019-08-03)
+* Client:
+** dracut support: In password-agent, properly ignore deleted and
+ renamed question files, and also fix memory alignment issue.
+
+Version 1.8.5 (2019-07-30)
+* Client
+** Support dracut(8) as well as initramfs-tools(7).
+** Minor bug fix: Allow the mandos-keygen --passfile option to use
+ passfiles with names starting with "-".
+** Document known limitation of mandos-keygen --password; it strips
+ white space from start and end of the password.
+* Server
+** Bug fix: The server used to fail to restart if the "port" setting
+ was used. This has been fixed.
+** Minor bug fix: Reap zombies left over from checker runs. (Debian
+ bug #933387)
+
+Version 1.8.4 (2019-04-09)
+* Client
+** Fix minor memory leak in plugin-runner.
+* Server
+** mandos-ctl now has a --debug option to show D-Bus calls.
+
+Version 1.8.3 (2019-02-11)
+* No user-visible changes.
+
+Version 1.8.2 (2019-02-10)
+* Client
+** In mandos-keygen, ignore failures to remove files in some cases.
+
+Version 1.8.1 (2019-02-10)
+* Client
+** Only generate TLS keys using GnuTLS' certtool, of sufficient
+ version. Key generation of TLS keys will not happen until a
+ version of GnuTLS is installed with support for raw public keys.
+** Remove any bad keys created by 1.8.0 and openssl.
+* Server
+** On installation, edit clients.conf and remove the same bad key ID
+ which was erroneously reported by all 1.8.0 clients. Also do not
+ trust this key ID in the server.
+
+Version 1.8.0 (2019-02-10)
+* Client
+** Use new TLS keys for server communication and identification.
+ With GnuTLS 3.6 or later, OpenPGP keys are no longer supported.
+ The client can now use the new "raw public keys" (RFC 7250) API
+ instead, using GnuTLS 3.6.6. Please note: This *requires* new key
+ IDs to be added to server's client.conf file.
+** New --tls-privkey and --tls-pubkey options to load TLS key files.
+ If GnuTLS is too old, these options do nothing.
+* Server
+** Supports either old or new GnuTLS.
+ The server now supports using GnuTLS 3.6.6 and clients connecting
+ with "raw public keys" as identification. The server will read
+ both fingerprints and key IDs from clients.conf file, and will use
+ either one or the other, depending on what is supported by GnuTLS
+ on the system. Please note: both are *not* supported at once; if
+ one type is supported by GnuTLS, all values of the other type from
+ clients.conf are ignored.
+
+Version 1.7.20 (2018-08-19)
+* Client
+** Fix: Adapt to the Debian cryptsetup package 2.0.3 or later.
+ Important: in that version or later, the plugins "askpass-fifo",
+ "password-prompt", and "plymouth" will no longer be run, since they
+ would conflict with what cryptsetup is doing. Other plugins, such
+ as mandos-client and any user-supplied plugins, will still run.
+** Better error message if failing to decrypt secret data
+** Check for (and report) any key import failure from GPGME
+** Better error message if self-signature verification fails
+** Set system clock if not set; required by GnuPG for key import
+** When debugging plugin-runner, it will now show starting plugins
+
+Version 1.7.19 (2018-02-22)
+* Client
+** Do not print "unlink(...): No such file or directory".
+** Bug fixes: Fix file descriptor leaks.
+** Bug fix: Don't use leak sanitizer with leaking libraries.
+
+Version 1.7.18 (2018-02-12)
+* Client
+** Bug fix: Revert faulty fix for a nonexistent bug in the
+ plugin-runner
+
+Version 1.7.17 (2018-02-10)
+* Client
+** Bug fix: Fix a memory leak in the plugin-runner
+** Bug fix: Fix memory leaks in the plymouth plugin
+
+Version 1.7.16 (2017-08-20)
+* Client
+** Bug fix: ignore "resumedev" entries in initramfs' cryptroot file
+** Bug fix in plymouth plugin: fix memory leak, avoid warning output
+
+Version 1.7.15 (2017-02-23)
+* Server
+** Bug fix: Respect the mandos.conf "zeroconf" and "restore" options
+* Client
+** Bug fix in mandos-keygen: Handle backslashes in passphrases
+
+Version 1.7.14 (2017-01-25)
+* Server
+** Use "Requisite" instead of "RequisiteOverridable" in systemd
+ service file.
+
+Version 1.7.13 (2016-10-08)
+* Client
+** Minor bug fix: Don't ask for passphrase or fail when generating
+ keys using GnuPG 2.1 in a chrooted environment.
+
+Version 1.7.12 (2016-10-05)
+* Client
+** Bug fix: Don't crash after exit() when using DH parameters file
+
+Version 1.7.11 (2016-10-01)
+* Client
+** Security fix: Don't compile with AddressSanitizer
+* Server
+** Bug fix: Find GnuTLS library when gnutls28-dev is not installed
+** Bug fix: Include "Expires" and "Last Checker Status" in mandos-ctl
+ verbose output
+** New option for mandos-ctl: --dump-json
+
+Version 1.7.10 (2016-06-23)
+* Client
+** Security fix: restrict permissions of /etc/mandos/plugin-helpers
+* Server
+** Bug fix: Make the --interface flag work with Python 2.7 when "cc"
+ is not installed
+
+Version 1.7.9 (2016-06-22)
+* Client
+** Do not include intro(8mandos) man page
+
+Version 1.7.8 (2016-06-21)
+* Client
+** Include intro(8mandos) man page
+** mandos-keygen: Use ECDSA SSH keys by default
+** Bug fix: Work with GnuPG 2 when booting (Debian bug #819982)
+ by copying /usr/bin/gpg-agent into initramfs
+* Server
+** Bug fix: Work with GnuPG 2 (don't use --no-use-agent option)
+** Bug fix: Make the --interface option work when using Python 2.7
+ by trying harder to find SO_BINDTODEVICE
+
+Version 1.7.7 (2016-03-19)
+* Client
+** Fix bug in Plymouth client, broken since 1.7.2
+
+Version 1.7.6 (2016-03-13)
+* Server
+** Fix bug where stopping server would time out
+** Make server runnable with Python 3
+
+Version 1.7.5 (2016-03-08)
+* Server
+** Fix security restrictions in systemd service file.
+** Work around bug where stopping server would time out
+
+Version 1.7.4 (2016-03-05)
+* Client
+** Bug fix: Tolerate errors from configure_networking (Debian Bug
+ #816513)
+** Compilation: Only use sanitizing options which work with the
+ compiler used when building. This should fix compilation with GCC
+ 4.9 on mips, mipsel, and s390x.
+* Server
+** Add extra security restrictions in systemd service file.
+
+Version 1.7.3 (2016-02-29)
+* Client
+** Bug fix: Remove new type of keyring directory user by GnuPG 2.1.
+** Bug fix: Remove "nonnull" attribute from a function argument, which
+ would otherwise generate a spurious runtime warning.
+
+Version 1.7.2 (2016-02-28)
+* Server
+** Stop using python-gnutls library; it was not updated to GnuTLS 3.3.
+** Bug fix: Only send D-Bus signal ClientRemoved if using D-Bus.
+** Use GnuPG 2 if available.
+* Client
+** Compile with various sanitizing flags.
+
+Version 1.7.1 (2015-10-24)
+* Client
+** Bug fix: Can now really find Mandos server even if the server has
+ an IPv6 address on a network other than the one which the Mandos
+ server is on.
+
+Version 1.7.0 (2015-08-10)
+* Server
+** Bug fix: Handle local Zeroconf service name collisions better.
+** Bug fix: Finally fix "ERROR: Child process vanished" bug.
+** Bug fix: Fix systemd service file to start server correctly.
+** Bug fix: Be compatible with old 2048-bit DSA keys.
+** The D-Bus API now provides the standard D-Bus ObjectManager
+ interface, and deprecates older functionality. See the DBUS-API
+ file for the currently recommended API. Note: the original API
+ still works, but is deprecated.
+* Client
+** Can now find Mandos server even if the server has an IPv6 address
+ on a network without IPv6 Router Advertisment (like if the Mandos
+ client itself is the router, or there is an IPv6 router advertising
+ a network other than the one which the Mandos server is on.)
+** Use a better value than 1024 for the default number of DH bits.
+ This better value is either provided by a DH parameters file (see
+ below) or an appropriate number of DH bits is determined based on
+ the PGP key.
+** Bug fix: mandos-keygen now generates correct output for the
+ "Checker" variable even if the SSH server on the Mandos client has
+ multiple SSH key types.
+** Can now use pre-generated Diffie-Hellman parameters from a file.
+
+Version 1.6.9 (2014-10-05)
+* Server
+** Changed to emit standard D-Bus signal when D-Bus properties change.
+ (The old signal is still emitted too, but marked as deprecated.)
+
+Version 1.6.8 (2014-08-06)
+* Client
+** Bug fix: mandos-keygen now generates working SSH checker commands.
+* Server
+** Bug fix: "mandos-monitor" now really redraws screen on Ctrl-L.
+** Now requires Python 2.7.
+
+Version 1.6.7 (2014-07-17)
+* Client
+** Bug fix: Now compatible with GPGME 1.5.0.
+** Bug fix: Fixed minor memory leaks.
+* Server
+** "mandos-monitor" now has verbose logging, toggleable with "v".
+
+Version 1.6.6 (2014-07-13)
+* Client
+** If client host has an SSH server, "mandos-keygen --password" now
+ outputs "checker" option which uses "ssh-keyscan"; this is more
+ secure than the default "fping" checker.
+** Bug fix: allow "." in network hook names, to match documentation.
+** Better error messages.
+* Server
+** New --no-zeroconf option.
+** Bug fix: Fix --servicename option, broken since 1.6.4.
+** Bug fix: Fix --socket option work for --socket=0.
+
+Version 1.6.5 (2014-05-11)
+* Client
+** Work around bug in GnuPG
+** Give better error messages when run without sufficient privileges
+** Only warn if workaround for Debian bug #633582 was necessary and
+ failed, not if it failed and was unnecessary.
+
+Version 1.6.4 (2014-02-16)
+* Server
+** Very minor fix to self-test code.
+
+Version 1.6.3 (2014-01-21)
+* Server
+** Add systemd support.
+** For PID file, fall back to /var/run if /run does not exist.
+* Client
+** Moved files from /usr/lib/mandos to whatever the architecture
+ specifies, like /usr/lib/x86_64-linux-gnu/mandos or
+ /usr/lib64/mandos.
+
+Version 1.6.2 (2013-10-24)
+* Server
+** PID file moved from /var/run to /run.
+** Bug fix: Handle long secrets when saving client state.
+** Bug fix: Use more magic in the GnuTLS priority string to handle
+ both old DSA/ELG 2048-bit keys and new RSA/RSA 4096-bit keys.
+* Client
+** mandos-keygen: Bug fix: now generate RSA keys which GnuTLS can use.
+ Bug fix: Output passphrase prompts even when
+ redirecting standard output.
+
+Version 1.6.1 (2013-10-13)
+* Server
+** All client options for time intervals now also take an RFC 3339
+ duration. The same for all options to mandos-ctl.
+** Bug fix: Handle fast checkers (like ":") correctly.
+** Bug fix: Don't print output from checkers when running in
+ foreground.
+** Bug fix: Do not fail when client is removed from clients.conf but
+ saved settings remain.
+** Bug fix: mandos-monitor now displays standout (reverse video) again
+ using new version of Urwid.
+** Bug fix: Make boolean options work from the config file again.
+** Bug fix: Make --no-ipv6 work again.
+** New default priority string to be slightly more compatible with
+ older versions of GnuTLS.
+* Client
+** Bug fix: Fix bashism in mandos-keygen.
+** Default key and subkey types are now RSA and RSA, respectively.
+ Also, new default key size is 4096 bits.
+
+Version 1.6.0 (2012-06-18)
+* Server
+** Takes new --foreground option
+** Init script supports new "status" action.
+* Client
+** Now uses all interfaces by default; the --interface option can
+ still be used to restrict it, and the argument to --interface (as
+ well as the $DEVICE environment variable for the network hooks) is
+ now a comma-separated list of interfaces to use.
+
+Version 1.5.5 (2012-06-01)
+* Server
+** Server takes new --socket option
+
+Version 1.5.4 (2012-05-20)
+* Server
+** Bug fix: Regression fix: Make non-zero approval timeout values work.
+** Bug fix: Regression fix: Allow changing the Timeout D-Bus property.
+** Fall back to not bind to an interface if an invalid interface name
+ is given.
+** Removed support for undocumented feature of using plain "%%s" in
+ "checker" client option.
+** Old D-Bus interface are now marked as deprecated.
+** mandos-monitor: Bug fix: show approval timers correctly.
+** mandos-ctl: Show "Extended Timeout" correctly, not as milliseconds.
+
+Version 1.5.3 (2012-01-15)
+* Server
+** Add D-Bus property se.recompile.Client.LastCheckerStatus and use it
+ in mandos-monitor.
+* Client
+** Fix bugs in the example "bridge" network hook.
+
+Version 1.5.2 (2012-01-08)
+* Server
+** Removed D-Bus signal se.recompile.Mandos.NewRequest() added in
+ 1.5.0. It was buggy and was of questionable utility.
+
+Version 1.5.1 (2012-01-01)
+* Server
+** Include intro(8mandos) manual page, missing since migration from
+ README file in version 1.4.0.
+
+Version 1.5.0 (2012-01-01)
+* Client
+** Network hooks. The Mandos client can now run custom scripts to take
+ up a network interface before the client is run. Three example
+ scripts are provided: "wireless", "openvpn", and "bridge".
+ To facilitate this, the client now prefers network interfaces which
+ are up (if any) over all other interfaces.
+* Server
+** Persistent state. Client state is now saved between server
+ restarts.
+** clients.conf file can now contain "enabled" setting for clients.
+** Bug fix: Fix rare crash bug.
+** Bug fix: Send corrent D-Bus type in PropertyChanged for
+ "ApprovalDelay", "ApprovalDuration", "Timeout", and
+ "ExtendedTimeout".
+** mandos-ctl: Bare numbers as arguments are taken to be milliseconds.
+** Bug fix: mandos-ctl --secret option now works.
+** New D-Bus signal: se.recompile.Mandos.NewRequest(s).
+
+Version 1.4.1 (2011-10-15)
+* Server
+** Make D-Bus properties settable again, and handle checkers
+ for disabled clients correctly.
+* Miscellaneous fixes to "pedantic" Lintian warnings
+
+Version 1.4.0 (2011-10-09)
+* README file migrated to manual page intro(8mandos).
+* Client:
+** Fixed warning about "rmdir: Directory not empty".
+* Server:
+** Default values changed: timeout 5 minutes, interval 2 minutes.
+** Clients gets an expiration extension when receiving a password,
+ controlled by new "extended_timeout" setting.
+** New domain name: "fukt.bsnet.se" changes to "recompile.se". This
+ also affects the D-Bus bus and interface names (old names still
+ work). Users should start using the new names immediately.
+** New D-Bus Client object properties "Expires" and "ExtendedTimeout";
+ see DBUS-API for details.
+
+Version 1.3.1 (2011-07-27)
+* Client:
+** Client now retries all Mandos servers periodically.
+** Work around Debian bug #633582 - fixes "Permission denied" problem.
+
+Version 1.3.0 (2011-03-08)
+* Server:
+** Updated for Python 2.6.
+* Client:
+** Bug fix: Make the password-prompt plugin not conflict with
+ Plymouth.
+** Bug fix: Bug fix: update initramfs also when purging package.
+
+Version 1.2.3 (2010-10-11)
+* Server:
+** Bug fix: Expose D-Bus API also in non-debug mode.
+
+Version 1.2.2 (2010-10-07)
+* Client:
+** splashy: Minor fix to compile with non-Linux kernels.
+
+Version 1.2.1 (2010-10-02)
+* Server:
+** mandos-monitor(8): Documentation bug fix: Key for removing client
+ is "R", not "r".
+
+Version 1.2 (2010-09-28)
+* Client:
+** New "plymouth" plugin to ask for a password using the Plymouth
+ graphical boot system.
+** The Mandos client now automatically chooses a network interface if
+ the DEVICE setting in /etc/initramfs-tools/initramfs.conf is set to
+ the empty string. This is also the new default instead of "eth0".
+** The Mandos client --connect option now loops indefinitely until a
+ password is received from the specified server.
+** Bug fix: Quote directory correctly in mandos-keygen with --password
+** Bug fix: don't use "echo -e" in mandos-keygen; unsupported by dash.
+* Server:
+** Terminology change: clients are now "ENABLED" or "DISABLED", not
+ "valid" or "invalid".
+** New D-Bus API; see the file "DBUS-API".
+** New control utilities using the new D-Bus API:
+ + mandos-ctl A command-line based utility
+ + mandos-monitor A text-based GUI interface
+** New feature: manual interactive approval or denying of clients on a
+ case-by-case basis.
+** New --debuglevel option to control logging
+** Will not write PID file if --debug is passed
+** Bug fix: Avoid race conditions with short "interval" values or
+ fast checkers.
+** Bug fix: Don't try to bind to a network interface when none is
+ specified
+
+Version 1.0.14 (2009-10-25)
+Enable building without -pie and -fPIE if BROKEN_PIE is set.
+
+Version 1.0.13 (2009-10-22)
+* Client
+** Security bug fix: If Mandos server is also installed, do not copy
+ its config files (with encrypted passwords) into the initrd.img-*
+ files.
+
+Version 1.0.12 (2009-09-17)
+* Client
+** Bug fix: Allow network interface renaming by "udev" by taking down
+ the network interface after using it.
+** Bug fix: User-supplied plugins are now installed correctly.
+** Bug fix: If usplash was used but the password was instead provided
+ by the Mandos server, the usplash daemon used to ignore the first
+ command passed to it. This has been fixed.
+** Bug fix: Make the "--userid" and "--groupid" options in
+ "plugin-runner.conf" work.
+* Server
+** Bug fix: Fix the LSB header in the init.d script to make dependency
+ based booting work.
+** A client receiving its password now also counts as if a checker was
+ run successfully (i.e. the timeout timer is reset).
+
+Version 1.0.11 (2009-05-23)
+* Client
+** Bug fix: Use "pkg-config" instead of old "libgnutls-config".
+
+Version 1.0.10 (2009-05-17)
+* Client
+** Security bug fix: Fix permissions on initrd.img-*.bak files when
+ upgrading from older versions.
+
+Version 1.0.9 (2009-05-17)
+* Client
+** Security bug fix: Fix permissions on initrd.img file when
+ installing new linux-image-* packages calling mkinitramfs-kpkg (all
+ version lower than 2.6.28-1-* does this).
+
+Version 1.0.8 (2009-02-25)
+* Client
+** Bug fix: Fix missing quote characters in initramfs-tools-hook.
+
+Version 1.0.7 (2009-02-24)
+* Client
+** Bug fix: Do not depend on GNU awk.
+
+Version 1.0.6 (2009-02-13)
+* Server
+** Fix bug where server would stop responding, with a zombie checker
+** Support for disabling IPv6 (only for advanced users)
+** Fix bug which made server not change group ID
+
+* Client
+** Bug fix: Fix permission for /lib64 (on relevant architechtures).
+** Add support for IPv4 addresses.
+** Add support in mandos-client for not bringing up a network
+ interface by specifying an empty string to "--interface".
+** Make password prompt on boot not be mangled by kernel log messages
+ about network interface.
+** Get network interface from initramfs.conf and/or from kernel
+ command line.
+** If set by "ip=" kernel command line, configure network on boot.
+** Support connecting directly using "mandos=connect" kernel command.
+ line option, provided network is configured using "ip=".
+** Fix bug which made plugin-runner and mandos-client not change group
+ ID.
+** Fix bug where the "--options-for" option of plugin-runner would
+ truncate the value at the first colon character.
+** Fix bug where plugin-runner would not go to fallback if all plugins
+ failed.
+** Fix bug where mandos-client would not clean temporary directory on
+ a signal or on certain file systems.
+** Bug fix: remove bashism in /bin/sh script "mandos-keygen".
+
+Version 1.0.5 (2009-01-17)
+* Client
+** Fix small memory leak in plugin-runner.
+
+Version 1.0.4 (2009-01-15)
+* Server
+** Only find matched user/group pairs when searching for suitable
+ nonprivileged user/group to switch to.
+
+* Client
+** New kernel parameter "mandos=off" makes client not run at boot.
+** Fix linking errors and compilation warnings on AMD64.
+** Parse numbers in command line options better.
+** The splashy and usplash plugins are more robust while traversing
+ /proc, and will not abort if a process suddenly disappears.
+
+Version 1.0.3 (2009-01-06)
+* Server
+** Now tries to change to user and group "_mandos" before falling back
+ to trying the old values "mandos", "nobody:nogroup", and "65534".
+** Now does not abort on startup even if no clients are defined in
+ clients.conf.
+
+* Client
+** Plugins named "*.dpkg-bak" are now ignored.
+** Hopefully fixed compilation failure on some architectures where the
+ C compiler does not recognize the "-z" option as a linker option.
+
+Version 1.0.2 (2008-10-17)
+* mandos-keygen now signs the encrypted key blobs. This signature is
+ not currently verified by mandos-client, but this may change in the
+ future.
+
+Version 1.0.1 (2008-10-07)
+* Server
+** Expand environment variables and ~user in clients.conf's "secfile"
+ The "secfile" option in /etc/mandos/clients.conf now expands
+ "~user/foo" and "$ENVVAR" strings.
+
+* Client (plugin-runner, plugins, etc.)
+** Manual pages for the usplash, splashy, and askpass-fifo plugins.
+ All plugins now have man pages.
+** More secure compilation and linking flags.
+ All programs are now compiled with "-fstack-protector-all -fPIE
+ -pie", and linked using "-z relro -pie" for additional security.
+
+* There is now a "NEWS" file (this one), giving a history of
+ noteworthy changes.
=== added file 'README'
--- README 1970-01-01 00:00:00 +0000
+++ README 2016-03-23 07:11:22 +0000
@@ -0,0 +1,11 @@
+Please see: https://www.recompile.se/mandos/man/intro.8mandos
+
+This information previously in this file has been moved to the
+intro(8mandos) manual page. Go to the above URL, or install the
+Mandos server and run this command:
+
+ man 8mandos intro
+
+In short, this is the Mandos system; it allows computers to have
+encrypted root file systems and at the same time be capable of remote
+and/or unattended reboots.
=== modified file 'TODO'
--- TODO 2008-08-10 20:22:27 +0000
+++ TODO 2019-04-09 19:41:53 +0000
@@ -1,160 +1,124 @@
-*- org -*-
-* [#A] README file
-
-* [#A] COPYING file
- [[file:/usr/share/common-licenses/GPL-3][GPLv3]]
-
-* Mandos-client
-** [#A] Man page: man8/mandos-client.8mandos
-*** DESCRIPTION
- Describe the plus sign syntax for passing options from crypttab
-*** EXIT STATUS
- Text needed
-*** EXAMPLES
- Examples of normal usage, debug usage, debugging single or all
- plugins, examples of crypttab lines with plus syntax, etc.
-*** FILES
- Text needed
-*** SECURITY
- Text needed
-*** NOTES
- Text needed
-*** BUGS
- Text needed
-*** SEE ALSO
- Explaining test on what you can read
-** Use asprintf instead of malloc and strcat?
-** use strsep instead of strtok?
-** use config file in addition to arguments
-** pass things in environment, like device name, etc
-
-* Password-request
-** [#A] Man page: man8/password-request.8mandos
-*** DESCRIPTION
- Move options to new OPTIONS section.
- State that this command is not meant to be invoked directly, but
- is run as a plugin from mandos-client(8) and only run in the
- initrd environment, not the real system.
-*** EXIT STATUS
- Create this section
-*** EXAMPLES
- Examples of normal usage, debug usage, debugging by connecting
- directly, etc.
-*** FILES
- Describe the key files and the key ring files. Also note that
- they should normally have been automatically created.
-*** DIAGNOSTICS
- Create this section
-*** SECURITY
- Create this section
-*** NOTES
- Create this section (if needed)
-*** BUGS
- Create this section
-*** SEE ALSO
- Refer to mandos-client(8mandos) and password-prompt(8mandos)
-** Use asprintf instead of malloc and memcpy?
-** IPv4 support
-** use strsep instead of strtok?
-** Do not depend on GPG key rings on disk
- This would mean creating new GPG key rings with GPGME by importing
- the key files from scratch on every program start.
-
-* Password-prompt
-** [#A] Man page: man8/password-prompt.8mandos
-*** DESCRIPTION
- Move options to new OPTIONS section.
-*** EXIT STATUS
- Create this section
-*** EXAMPLES
- Examples of normal usage, debug usage, with a prefix, etc.
-*** DIAGNOSTICS
- Create this section
-*** SECURITY
- Create this section
- Not much to do here but it is noteworthy to state the danger of
- not having a fallback option.
-*** NOTES
- Note that this is more or less a simple getpass(3) wrapper, even
- though actual use of getpass(3) is not guaranteed.
-*** BUGS
- Create this section
-*** SEE ALSO
- Refer to mandos-client(8mandos) and password-request(8mandos)
-** Use getpass(3)?
- Man page says "obsolete", but [[info:libc:getpass][GNU LibC Manual: Reading Passwords]]
- does not. See also [[http://sources.redhat.com/ml/libc-alpha/2003-05/msg00251.html][Marcus Brinkmann: Re: getpass obsolete?]] and
- [[http://article.gmane.org/gmane.comp.lib.glibc.alpha/4906][Petter Reinholdtsen: Re: getpass obsolete?]], and especially also
- [[http://www.steve.org.uk/Reference/Unix/faq_4.html#SEC48][Unix Programming FAQ 3.1 How can I make my program not echo input?]]
-
-* Mandos (server)
-** [#A] Command man page: man8/mandos.8
-*** DESCRIPTION
- Move options to new OPTIONS section
-*** EXIT STATUS
- Create this section
-*** EXAMPLES
- Create this section
-*** FILES
- Describe briefly that the server gets global settings from
- mandos.conf and clients from clients.conf, but refer to their man
- pages for more details.
-*** DIAGNOSTICS
- Create this section
-*** SECURITY
- Create this section
-*** NOTES
- Create this section (if needed)
-*** BUGS
- Create this section
-*** SEE ALSO
- Refer to the client man page
-** [#A] Config file man page: man5/mandos.conf (mandos.conf)
-** [#A] Config file man page: man5/mandos-clients.conf (clients.conf)
-** [#A] /etc/init.d/mandos-server :teddy:
-** Log level
-** /etc/mandos/clients.d/*.conf
- Watch this directory and add/remove/update clients?
-** config for TXT record
-** Run-time communication with server
- Probably using D-Bus
- See also [[*Mandos-tools]]
-** Implement --foreground
- [[info:standards:Option%20Table][Table of Long Options]]
-** Implement --socket
- [[info:standards:Option%20Table][Table of Long Options]]
-
-* Mandos-tools/utilities
- All of this probably using D-Bus
-** List clients
-** Disable client
-** Enable client
-
-* Installer
-** DONE [#A] Change initrd.img file to not be publically readable
- /etc/initramfs-tools/conf.d/mandos
- UMASK=027
-** Update initrd.img after installation
-** [#A] Create mandos user and group for server
-** [#A] Create /var/run/mandos directory with perm and ownership
-
-* [#A] Package
-** /etc/bash_completion.d/mandos
-** /etc/initramfs-tools/hooks/mandos
- [[file:/usr/share/doc/initramfs-tools/examples/example_hook][Example initramfs-tools hook script]]
-*** Create GPG key ring files in initrd
-** unperish
-** bzr-builddeb
-
-* INSTALL file
-
-* Web site
-
-* Mailing list
-
-* Announce project on news
- [[news:comp.os.linux.announce]]
+* Testing
+** python-nemu
+
+* mandos-applet
+
+* mandos-client
+** TODO A --server option which only adds to the server list.
+ (Unlike --connect, which implicitly disables zeroconf.)
+** TODO [#B] Use capabilities instead of seteuid().
+ https://forums.grsecurity.net/viewtopic.php?f=7&t=2522
+** TODO [#B] Use getaddrinfo(hints=AI_NUMERICHOST) instead of inet_pton()
+** TODO [#C] Make start_mandos_communication() take "struct server".
+** TODO [#C] --interfaces=regex,eth*,noregex (bridge-utils-interfaces(5))
+** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL
+** TODO [#B] Use reallocarray() with GNU LibC 2.29 or later.
+
+* splashy
+** TODO [#B] use scandir(3) instead of readdir(3)
+** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL
+
+* usplash (Deprecated)
+** TODO [#B] Make it work again
+** TODO [#B] use scandir(3) instead of readdir(3)
+** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL
+
+* askpass-fifo
+** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL
+
+* password-prompt
+** TODO [#B] lock stdin (with flock()?)
+** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL
+
+* plymouth
+** TODO [#A] Detect partial writes to stdout and exit with EX_TEMPFAIL
+** TODO [#B] Use reallocarray() with GNU LibC 2.29 or later.
+
+* TODO [#B] passdev
+
+* plugin-runner
+** TODO handle printing for errors for plugins
+*** Hook up stderr of plugins, buffer them, and prepend "Mandos Plugin [plugin name]"
+** TODO [#C] use same file name rules as run-parts(8)
+** kernel command line option for debug info
+** TODO [#A] Restart plugins which exit with EX_TEMPFAIL
+
+* mandos (server)
+** TODO [#B] --notify-command
+ This would allow the mandos.service to use
+ --notify-command="systemd-notify --pid --ready"
+** TODO [#B] python-systemd
+*** import systemd.daemon; systemd.daemon.notify()
+** TODO [#B] Log level :BUGS:
+*** TODO /etc/mandos/clients.d/*.conf
+ Watch this directory and add/remove/update clients?
+** TODO [#C] config for TXT record
+** TODO Log level dbus option
+ SetLogLevel D-Bus call
+** TODO [#C] DBusServiceObjectUsingSuper
+** TODO [#B] Global enable/disable flag
+** TODO [#B] By-client countdown on number of secrets given
+** D-Bus Client method NeedsPassword(50) - Timeout, default disapprove
+ + SetPass(u"gazonk", True) -> Approval, persistent
+ + Approve(False) -> Close client connection immediately
+** TODO [#C] python-parsedatetime
+** TODO Separate logging logic to own object
+** TODO [#B] Limit approval_delay to max gnutls/tls timeout value
+** TODO [#B] break the wait on approval_delay if connection dies
+** TODO Generate Client.runtime_expansions from client options + extra
+** TODO Allow %%(checker)s as a runtime expansion
+** TODO D-Bus AddClient() method on server object
+** TODO Use org.freedesktop.DBus.Method.NoReply annotation on async methods. :2:
+** TODO Save state periodically to recover better from hard shutdowns
+** TODO CheckerCompleted method, deprecate CheckedOK
+** TODO Secret Service API?
+ https://standards.freedesktop.org/secret-service/
+** TODO Remove D-Bus interfaces with old domain name :2:
+** TODO Remove old string_to_delta format :2:
+** TODO http://0pointer.de/blog/projects/stateless.html
+*** File in /usr/lib/sysusers.d to create user+group "_mandos"
+** TODO Error handling on error parsing config files
+** TODO init.d script error handling
+** TODO D-Bus server properties; address, port, interface, etc. :2:
+** Python 3 :2:
+*** TODO [#C] In Python 3.3, use shlex.quote() instead of re.escape()
+
+* mandos-ctl
+** TODO Remove old string_to_delta format :2:
+
+* TODO mandos-dispatch
+ Listens for specified D-Bus signals and spawns shell commands with
+ arguments.
+
+* mandos-monitor
+** TODO --servicename :BUGS:
+** TODO help should be toggleable
+** Urwid client data displayer
+ Better view of client data in the listing
+*** Properties popup
+** Print a nice "We are sorry" message, save stack trace to log.
+
+* mandos-keygen
+** TODO "--secfile" option
+ Using the "secfile" option instead of "secret"
+** TODO [#B] "--test" option
+ For testing decryption before rebooting.
+
+* Package
+** /usr/share/initramfs-tools/hooks/mandos
+*** TODO [#C] use same file name rules as run-parts(8)
+*** TODO [#C] Do not install in initrd.img if configured not to.
+ Use "/etc/initramfs-tools/hooksconf.d/mandos"?
+** TODO [#C] $(pkg-config --variable=completionsdir bash-completion)
+ From XML sources directly?
+
+* Side Stuff
+** TODO Locate which package moves the other bin/sh when busybox is deactivated
+** TODO contact owner of package, and ask them to have that shell static in position regardless of busybox
+
+* [[http://www.undeadly.org/cgi?action=article&sid=20110530221728][OpenBSD]]
#+STARTUP: showall
=== added file 'bugs.xml'
--- bugs.xml 1970-01-01 00:00:00 +0000
+++ bugs.xml 2016-03-05 21:42:56 +0000
@@ -0,0 +1,11 @@
+
+
+
+ Please report bugs to the Mandos development mailing list:
+ mandos-dev@recompile.se (subscription required).
+ Note that this list is public. The developers can be reached
+ privately at mandos@recompile.se (OpenPGP key
+ fingerprint 153A 37F1 0BBA 0435 987F 2C4A 7223 2973 CA34
+ C2C4 for encrypted mail).
+
=== modified file 'clients.conf'
--- clients.conf 2008-08-10 20:35:01 +0000
+++ clients.conf 2019-02-09 23:34:15 +0000
@@ -2,25 +2,45 @@
# 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.
-;timeout = 1h
+# How long until a client is disabled and not be allowed to get the
+# data this server holds.
+;timeout = PT5M
# How often to run the checker to confirm that a client is still up.
# Note: a new checker will not be started if an old one is still
# running. The server will wait for a checker to complete until the
-# "timeout" above occurs, at which time the client will be marked
-# invalid, and any running checker killed.
-;interval = 5m
+# above "timeout" occurs, at which time the client will be disabled,
+# and any running checker killed.
+;interval = PT2M
+
+# Extended timeout is an added timeout that is given once after a
+# password has been sent sucessfully to a client. This allows for
+# additional delays caused by file system checks and quota checks.
+;extended_timeout = PT15M
# 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 = PT0S
+
+# How long one approval will last.
+;approval_duration = PT1S
+
+# Whether this client is enabled by default
+;enabled = True
+
;####
;# Example client
;[foo]
;
+;# TLS public key ID
+;key_id = f33fcbed11ed5e03073f6a55b86ffe92af0e24c045fb6e3b40547b3dc0c030ed
+;
;# OpenPGP key fingerprint
;fingerprint = 7788 2722 5BA7 DE53 9C5A 7CFA 59CF F7CD BD9A 5920
;
@@ -43,7 +63,6 @@
; 5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm
; 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O
; QlnHIvPzEArRQLo=
-; =iHhv
;
;# Host name; used only by the checker, not used by the server itself.
;host = foo.example.org
@@ -52,15 +71,23 @@
;####
;# Another example client, named "bar".
;[bar]
+;# The key ID is not space or case sensitive
+;key_id = F33FCBED11ED5E03073F6A55B86FFE92 AF0E24C045FB6E3B40547B3DC0C030ED
+;
;# The fingerprint is not space or case sensitive
;fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27
;
;# If "secret" is not specified, a file can be read for the data.
-;;secfile = /etc/mandos/bar-secret.txt.asc
+;secfile = /etc/keys/mandos/bar-secret.bin
;
;# An IP address for host is also fine, if the checker accepts it.
;host = 192.0.2.3
;
;# Parameters from the [DEFAULT] section can be overridden per client.
-;interval = 5m
+;interval = PT1M
+;
+;# This client requires manual approval before it receives its secret.
+;approved_by_default = False
+;# Require approval within 30 seconds.
+;approval_delay = PT30S
;####
=== added file 'common.ent'
--- common.ent 1970-01-01 00:00:00 +0000
+++ common.ent 2019-09-03 19:06:41 +0000
@@ -0,0 +1,3 @@
+
+
+
=== added file 'dbus-mandos.conf'
--- dbus-mandos.conf 1970-01-01 00:00:00 +0000
+++ dbus-mandos.conf 2011-10-02 19:18:24 +0000
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
=== added directory 'debian'
=== added file 'debian/changelog'
--- debian/changelog 1970-01-01 00:00:00 +0000
+++ debian/changelog 2019-09-04 21:17:42 +0000
@@ -0,0 +1,928 @@
+mandos (1.8.9-2) unstable; urgency=medium
+
+ * Fix failing autopkgtest.
+ * debian/tests/control (mandos-check/Restrictions): Add "allow-stderr".
+
+ -- Teddy Hogeborn Wed, 04 Sep 2019 23:14:06 +0200
+
+mandos (1.8.9-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Fix "Python2 removal in sid/bullseye" by using Python 3 instead
+ (Closes: #936987)
+ * debian/control (Build-Depends, Build-Depends-Indep): Move "systemd"
+ from indep to regular build-depends.
+ (Build-Depends-Indep, Package: mandos/Depends): Depend on Python 3 and
+ Python 3 modules instead of Python 2.
+
+ -- Teddy Hogeborn Tue, 03 Sep 2019 20:58:27 +0200
+
+mandos (1.8.8-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/po/de.po: New; Fix "[INTL:de] Initial German debconf
+ translation" by including the contributed translation (Closes:
+ #934373)
+ * debian/po/fr.po: New; Fix "[INTL:fr] French debconf templates
+ translation" by including the contributed translation (Closes:
+ #934888)
+ * debian/po/sv.po: New Swedish translation.
+ * debian/mandos.postinst: Only reload D-Bus daemon if new user was
+ created.
+ * debian/mandos.dirs (usr/lib/sysusers.d): New.
+ * debian/mandos-client.dirs (usr/lib/sysusers.d): - '' -
+
+ -- Teddy Hogeborn Sun, 18 Aug 2019 22:01:13 +0200
+
+mandos (1.8.7-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/upstream/metadata: New.
+ * debian/mandos-client.postrm: Use the same logic as the
+ update_initramfs function in debian/mandos-client.postinst.
+ * debian/mandos-client.templates (mandos-client/key_id): Line which
+ should not be wrapped should be prefixed by a space.
+ * debian/mandos.templates (mandos/key_id): - '' -
+ * debian/po/en_US.po: New "translation" from ASCII to UTF-8.
+ * debian/po/templates.pot: Updated.
+ * debian/source/lintian-overrides
+ (package-uses-old-debhelper-compat-version): New; set to "10".
+ * debian/mandos-client.lintian-overrides
+ (maintainer-script-supports-ancient-package-version): New.
+ debian/mandos.lintian-overrides
+ (maintainer-script-supports-ancient-package-version): - '' -
+
+ -- Teddy Hogeborn Mon, 05 Aug 2019 23:22:00 +0200
+
+mandos (1.8.6-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Fix "mandos FTCBFS: hard codes build architecture pkg-config"
+ by making pkg-config overridable (Closes: #933701)
+ * debian/mandos.postinst (configure): After creating (or renaming) user
+ & group, reload D-Bus daemon (if present).
+
+ -- Teddy Hogeborn Sat, 03 Aug 2019 14:51:01 +0200
+
+mandos (1.8.5-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Fix "does not reap children" by reaping children (Closes: #933387)
+ * debian/mandos-client.README.Debian: Use new-style interface name.
+ * debian/tests/control: New file; implements autopkgtest support.
+ * debian/mandos-client.lintian-overrides
+ (manpage-has-errors-from-man): Remove; unnecessary.
+ * debian/mandos.lintian-overrides
+ (init.d-script-needs-depends-on-lsb-base): - '' -
+ * debian/mandos-client.postinst (update_initramfs): Upstream now
+ supports dracut(8), so update commands here to and run the correct
+ command to update initramfs.
+ * debian/control (Build-Depends): Add GLib -dev package.
+ (mandos-client/Depends): Add dracut(8) as an alternative dependency to
+ initramfs-tools.
+ (mandos-client/Conflicts): New; set to "dracut-config-generic".
+ (debian/mandos-client.README.Debian): Update for dracut(8) support.
+ * debian/mandos-client.templates: Reflowed by debconf-gettextize(1).
+ * debian/mandos.templates: - '' -
+ * debian/po/POTFILES.in: New.
+ * debian/po/templates.pot: - '' -
+ * debian/source/lintian-overrides: New.
+ * debian/control (Standards-Version): Update to "4.4.0".
+
+ -- Teddy Hogeborn Tue, 30 Jul 2019 20:41:29 +0200
+
+mandos (1.8.4-1) unstable; urgency=medium
+
+ * Fix "dirs in initrd are not accessible by mandos plugin-runner" by
+ making sure UMASK is set, no matter what other packages have installed
+ in "/usr/share/initramfs-tools/conf-hooks.d". (Closes: #926641)
+ * Fix "LeakSanitizer: detected memory leaks, fails to decrypt"
+ by fixing memory leak in plugin-runner. (Closes: #926643)
+ * debian/mandos-client.dirs: Add
+ "usr/share/initramfs-tools/conf-hooks.d", needed by fix for #926641.
+
+ -- Teddy Hogeborn Tue, 09 Apr 2019 22:05:39 +0200
+
+mandos (1.8.3-3) unstable; urgency=medium
+
+ * Fix "src:mandos: modifies d/control during build" by not doing that
+ anymore. (Closes: #922202)
+ * debian/rules (override_dh_shlibdeps-arch): Commented out.
+
+ -- Teddy Hogeborn Wed, 13 Feb 2019 09:52:39 +0100
+
+mandos (1.8.3-2) unstable; urgency=medium
+
+ * debian/rules (override_dh_shlibdeps-arch): New; conditionally edit
+ debian/control before running dh_shlibdeps.
+
+ -- Teddy Hogeborn Mon, 11 Feb 2019 12:49:57 +0100
+
+mandos (1.8.3-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/watch: Make the ".orig" file name suffix non-optional;
+ otherwise uscan thinks that ".orig" is part of the version number.
+ * debian/control (Build-Depends): Changed GnuTLS dependencies; move
+ 3.6.6 alternative to first in list, and remove dependencies on the
+ virtual package "gnutls-dev", since we need the version restrictions.
+ (Package: mandos/Depends): Remove dependency on libgnutls28-dev
+ package.
+ (Package: mandos/Suggests): New; set to "libc6-dev, c-compiler". (Used
+ to find value of "SO_BINDTODEVICE").
+ (Package: mandos-client/Depends): Don't depend on openssl anymore;
+ instead depend on either a gnutls-bin (>= 3.6.6) (in which case TLS
+ key generation will work), or on libgnutls30 (<< 3.6.0) (in which case
+ TLS key generation will not be needed).
+
+ -- Teddy Hogeborn Mon, 11 Feb 2019 07:30:32 +0100
+
+mandos (1.8.2-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/mandos-client.postinst (create_keys): Ignore failure to remove
+ bad keys.
+
+ -- Teddy Hogeborn Sun, 10 Feb 2019 11:44:56 +0100
+
+mandos (1.8.1-1) unstable; urgency=high
+
+ * New upstream release.
+ * debian/mandos-client.postinst (create_keys): Remove any bad keys
+ created by 1.8.0-1. Only create TLS keys if certtool succeeds.
+ * debian/mandos.postinst (configure): Remove any bad keys from
+ clients.conf, and inform the user if any were found.
+ * debian/mandos.templates (mandos/removed_bad_key_ids): New message.
+
+ -- Teddy Hogeborn Sun, 10 Feb 2019 10:00:21 +0100
+
+mandos (1.8.0-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Fix "(tries to) use GnuTLS OpenPGP support" by using raw public keys
+ when available (Closes: #879538)
+ * Fix "mandos : Depends: libgnutls30 (< 3.6.0) but 3.6.5-2 is to be
+ installed" by now also allowing GnuTLS >= 3.6.6 (Closes: #916673)
+ * debian/control (Standards-Version): Update to "4.3.0".
+ (Package: mandos-client/Depends): Change from "cryptsetup" to
+ "cryptsetup (<< 2:2.0.3-1) | cryptsetup-initramfs". Add "debconf (>=
+ 1.5.5) | debconf-2.0".
+ (Source: mandos/Build-Depends): Also allow libgnutls30 (>= 3.6.6).
+ (Package: mandos/Depends): - '' - and add debconf (>= 1.5.5) |
+ debconf-2.0".
+ (Package: mandos/Description): Alter description to match new design.
+ (Package: mandos-client/Description): - '' -
+ (Package: mandos-client/Depends): Move "gnutls-bin | openssl" to here
+ from "Recommends".
+ * debian/mandos-client.README.Debian: Add --tls-privkey and --tls-pubkey
+ options to test command.
+ * debian/mandos-client.postinst (create_key): Renamed to "create_keys"
+ - all callers changed - and also create TLS key files. Show notice if
+ new TLS key files were created.
+ * debian/mandos-client.postrm (purge): Also remove TLS key files.
+ * debian/mandos-client.lintian-overrides: Override warnings.
+ * debian/mandos-client.templates: New.
+ * debian/mandos.lintian-overrides: Override warnings.
+ * debian/mandos.postinst (configure): If GnuTLS 3.6.6 or later is
+ detected, show an important notice (once) about the new key_id option
+ required in clients.conf.
+ * debian/mandos.templates: New.
+ * debian/copyright: Update copyright year to 2019.
+
+ -- Teddy Hogeborn Sun, 10 Feb 2019 05:52:49 +0100
+
+mandos (1.7.20-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Fix "[tethys] mandos-client: Mandos client fails while booting but
+ works from chroot into unpacked initramfs" by setting system clock if
+ necessary (Closes: #894495)
+ * Fix "initramfs boot script assumes internal cryptsetup implementation
+ details and is now broken" by only using documented
+ interfaces (Closes: #904899)
+ * debian/mandos-client.dirs: Add
+ "usr/share/initramfs-tools/scripts/local-premount" and
+ "usr/share/initramfs-tools/conf.d", and remove
+ "usr/share/initramfs-tools/conf-hooks.d".
+ * debian/control (mandos-client/Depends): Add "(>= 0.99)" to dependency
+ on "initramfs-tools".
+ * debian/control (Source: mandos/Rules-Requires-Root): New; set to
+ "binary-targets".
+ (Standards-Version): Update to "4.2.0".
+
+ -- Teddy Hogeborn Sun, 19 Aug 2018 22:14:04 +0200
+
+mandos (1.7.19-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Fix "fails with "LeakSanitizer has encountered a fatal error"" by not
+ using LeakSanitizer in affected binary (Closes: #886595)
+
+ -- Teddy Hogeborn Thu, 22 Feb 2018 19:47:59 +0100
+
+mandos (1.7.18-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Mon, 12 Feb 2018 16:00:11 +0100
+
+mandos (1.7.17-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Fix "fails with "LeakSanitizer has encountered a fatal error""
+ by fixing memory leak in plugin-runner (Closes: #886595)
+ * debian/control (Build-Depends): Also depend on "libgnutls28-dev (<<
+ 3.6.0) | libgnutls30 (<< 3.6.0)".
+ (Package: mandos/Depends): - '' -
+ * debian/compat: Change to "10".
+ * debian/watch (version): Change to "4".
+ (opts/pgpsigurlmangle): Remove.
+ (opts/pgpmode): New; set to "auto".
+ (URL): Change to "https://ftp.recompile.se/pub/@PACKAGE@/@PACKAGE@
+ @ANY_VERSION@(?:\.orig)?@ARCHIVE_EXT@".
+ * debian/copyright: Update copyright year to 2018.
+ * debian/rules: Support the "noopt" and "parallel" DEB_BUILD_OPTIONS.
+ (override_dh_fixperms-arch): Use the DEB_HOST_MULTIARCH
+ variable directly instead of shelling out to "dpkg-architecture".
+ * debian/control (Standards-Version): Update to "4.1.3".
+ (Build-Depends): Change version of debhelper dependency to ">= 10".
+ * debian/mandos.lintian-overrides
+ (init.d-script-needs-depends-on-lsb-base): Change line number to "46".
+
+ -- Teddy Hogeborn Sat, 10 Feb 2018 19:09:50 +0100
+
+mandos (1.7.16-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/copyright (License): Use program name explicitly.
+ (Format): Use https in URL.
+ * debian/control (Priority): Change from "extra" to "optional".
+ (Standards-Version): Update to "4.0.1".
+
+ -- Teddy Hogeborn Sun, 20 Aug 2017 21:05:26 +0200
+
+mandos (1.7.15-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Upstream release fixes "Seems not to be honoring zeroconf option at
+ mandos.conf" (Closes: #855589)
+ * debian/mandos.lintian-overrides (mandos): Add new line
+ "init.d-script-needs-depends-on-lsb-base etc/init.d/mandos (line 49)".
+ * debian/copyright: Update copyright year to 2017.
+
+ -- Teddy Hogeborn Thu, 23 Feb 2017 21:29:36 +0100
+
+mandos (1.7.14-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/mandos-client.postinst (create_key): Stop GPG agent after
+ running mandos-keygen.
+ * debian/control (Package: mandos/Depends): Add "systemd-sysv | lsb-base
+ (>= 3.0-6)", change "gnupg" to "gnupg2 | gnupg", and change
+ "libgpgme11-dev" to "libgpgme-dev | libgpgme11-dev".
+
+ -- Teddy Hogeborn Wed, 25 Jan 2017 20:36:03 +0100
+
+mandos (1.7.13-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Fix "fails to install noninteractively" by using the "%no-protection"
+ statement in the GnuPG batch parameter file. (Closes: #840001)
+
+ -- Teddy Hogeborn Sat, 08 Oct 2016 06:31:07 +0200
+
+mandos (1.7.12-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Wed, 05 Oct 2016 22:06:55 +0200
+
+mandos (1.7.11-1) unstable; urgency=high
+
+ * New upstream release.
+ * debian/control (Source: mandos/Vcs-Bzr): Change to use HTTPS.
+ (Vcs-Browser): - '' -
+
+ -- Teddy Hogeborn Sat, 01 Oct 2016 16:20:48 +0200
+
+mandos (1.7.10-1) unstable; urgency=high
+
+ * New upstream release.
+ * debian/rules (override_dh_fixperms-arch): Also exclude
+ "etc/mandos/plugin-helpers" from changes by dh_fixperms.
+ * debian/mandos-client.postinst: Fix the permissions of
+ "/etc/mandos/plugin-helpers" for those systems which had a fresh
+ install of an older version.
+
+ -- Teddy Hogeborn Thu, 23 Jun 2016 22:00:29 +0200
+
+mandos (1.7.9-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Wed, 22 Jun 2016 07:30:12 +0200
+
+mandos (1.7.8-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Fix "bad gpgme_op_decrypt: GPGME: Decryption failed." by copying
+ /usr/bin/gpg-agent into initramfs (Closes: #819982)
+ * debian/control (Homepage): Change URL to use HTTPS.
+ (Standards-Version): Update to 3.9.8.
+ * debian/copyright (Source): Change URL to HTTPS.
+ * debian/mandos-client.README.Debian: Change wording to match updated
+ capabilities.
+
+ -- Teddy Hogeborn Tue, 21 Jun 2016 21:36:10 +0200
+
+mandos (1.7.7-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/mandos-client.postinst (configure): If older version, fix
+ permissions on plugin helper directory. Also fix permissions on
+ plugin helper local override directory (/etc/mandos/plugin-helpers),
+ but only if not listed by "dpkg-statoverride".
+ * debian/rules (override_dh_fixperms-arch): Exclude plugin helper
+ directory from dh_fixperms.
+ * debian/mandos.postinst (configure): Fix state directory permissions,
+ but only if not listed by "dpkg-statoverride".
+ * debian/mandos-client.lintian-overrides: Do not warn about permissions
+ on plugin helper directory.
+ * debian/mandos.dirs (usr/lib/tmpfiles.d): Added.
+
+ -- Teddy Hogeborn Sat, 19 Mar 2016 22:58:49 +0100
+
+mandos (1.7.6-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/control (Source: mandos/Build-Depends-Indep): Remove
+ "python-avahi".
+ (Source: mandos/Build-Depends-Indep): Change "python-gi |
+ python-gobject" to "python-gi"; i.e. remove "python-gobject".
+
+ -- Teddy Hogeborn Sun, 13 Mar 2016 22:58:23 +0100
+
+mandos (1.7.5-1) unstable; urgency=high
+
+ * New upstream release.
+ * debian/mandos.postinst (configure): If old version was 1.7.4-1 or
+ 1.7.4-1~bpo8+1, fix situation where clients.pickle file is owned by
+ root.
+
+ -- Teddy Hogeborn Tue, 08 Mar 2016 01:09:55 +0100
+
+mandos (1.7.4-1) unstable; urgency=medium
+
+ * New upstream release.
+ * initramfs-tools-script: Fix "Call to configure_network in initramfs
+ script broken due to set -e" by surrounding call by "set +x" and "set
+ -e" (Closes: #816513)
+ * debian/control: (Source: mandos/Build-Depends-Indep): Change
+ "python-gobject | python-gi" to "python-gi | python-gobject"
+ (Package: mandos/Depends): - '' -
+
+ -- Teddy Hogeborn Sat, 05 Mar 2016 23:10:07 +0100
+
+mandos (1.7.3-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Mon, 29 Feb 2016 22:26:38 +0100
+
+mandos (1.7.2-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Fix "Uses unneeded and obsolete version specific python packages"
+ by removing version-specific dependencies (Closes: #811159)
+ * debian/control (Source: mandos/Build-Depends): Add (>= 3.3.0) to
+ "libgnutls28-dev" and "gnutls-dev".
+ (Source: mandos/Build-Depends-Indep): Remove "python2.7-gnutls",
+ "python2.7", "python2.7-dbus", "python2.7-avahi", and
+ "python2.7-gobject"; replace with "python (>= 2.7), python (<< 3)",
+ "python-dbus", "python-avahi", "python-gobject | python-gi".
+ (Package: mandos/Depends): Remove "python-gnutls" and
+ "python2.7-gnutls", add "libgnutls28-dev (>= 3.3.0) | libgnutls30 (>=
+ 3.3.0)". Add "python (<< 3)". Remove "python2.7-dbus",
+ "python2.7-avahi", "python2.7-gobject", and "python2.7-urwid".
+ Replace "python-gobject" with "python-gobject | python-gi" and "gnupg
+ (<< 2)" with "gnupg".
+ (Package: mandos-client/Depends): Replace
+ "gnupg (<< 2)" with "gnupg".
+ (Source: mandos/Standards-Version): Change to 3.9.7.
+ * debian/copyright (Copyright): Update copyright year.
+
+ -- Teddy Hogeborn Sun, 28 Feb 2016 16:09:01 +0100
+
+mandos (1.7.1-2) unstable; urgency=medium
+
+ * debian/control (Package: mandos/Depends): Fix "Please drop versioned
+ dependency on initscripts package" by removing initscripts dependency
+ (Closes: #804967)
+ * debian/rules (override_dh_fixperms) Fix "FTBFS when built with
+ dpkg-buildpackage -A (No such file or directory)" by splitting into
+ "override_dh_fixperms-arch" and "override_dh_fixperms-indep".
+ (Closes: #806073)
+
+ -- Teddy Hogeborn Sat, 05 Dec 2015 02:27:40 +0100
+
+mandos (1.7.1-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Sat, 24 Oct 2015 19:43:40 +0200
+
+mandos (1.7.0-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/control (Standards-Version): Updated to "3.9.6".
+ (Build-Depends): Add "libnl-route-3-dev".
+ (Package: mandos-client/Recommends): Added "gnutls-bin | openssl" for
+ the generating of DH parameters.
+ * debian/mandos-client.README.Debian: Update example command line to use
+ new MANDOSPLUGINHELPERDIR environment variable. Also document the new
+ dhparams.pem file.
+ * debian/mandos-client.postinst: Create DH parameters file.
+ * debian/mandos.prerm: Don't run init script, use only invoke-rc.d.
+ * debian/mandos-client.postinst: Don't use absolute paths to commands.
+ * debian/mandos-client.postrm: Don't use absolute paths to commands.
+ Also remove dhparams.pem file.
+ * debian/copyright (Copyright): Update copyright year.
+ * Upstream changed systemd service file to implicitly be of
+ "Type=dbus". (Closes: #786845)
+
+ -- Teddy Hogeborn Mon, 10 Aug 2015 22:00:29 +0200
+
+mandos (1.6.9-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/control (Build-Depends): Fix "still uses GnutLS 2.x" by
+ changing from "libgnutls-dev" to "libgnutls28-dev | gnutls-dev"
+ (Closes: #762349)
+
+ -- Teddy Hogeborn Sun, 05 Oct 2014 22:05:06 +0200
+
+mandos (1.6.8-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/control (Source: mandos/Build-Depends-Indep): Since upstream
+ now requires Python 2.7, depend on exactly the python2.7 package and
+ all the Python 2.7 versions of the python modules.
+ (Package: mandos/Depends): - '' - but still depend on python (>=2.7)
+ and the generic versions of the Python modules; this is for mandos-ctl
+ and mandos-monitor, both of which are compatible with Python 3, and
+ use #!/usr/bin/python.
+
+ -- Teddy Hogeborn Wed, 06 Aug 2014 22:55:24 +0200
+
+mandos (1.6.7-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Thu, 17 Jul 2014 05:22:45 +0200
+
+mandos (1.6.6-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/mandos.postinst: Fix typo in comment.
+ * debian/control (mandos/Recommends): Changed to "ssh-client | fping".
+ (mandos-client/Recommends): New; set to "ssh".
+
+ -- Teddy Hogeborn Sun, 13 Jul 2014 22:49:21 +0200
+
+mandos (1.6.5-3) unstable; urgency=medium
+
+ * debian/control (mandos-client/Depends): Add "dpkg-dev (>=1.16.0)";
+ initramfs-tools-hook runs "dpkg-architecture -qDEB_HOST_MULTIARCH".
+ (Closes: #750221)
+
+ -- Teddy Hogeborn Fri, 06 Jun 2014 04:27:15 +0200
+
+mandos (1.6.5-2) unstable; urgency=medium
+
+ * debian/rules (override_dh_auto_test-arch): New; does nothing. Fixes
+ FTBFS for build-indep.
+
+ -- Teddy Hogeborn Tue, 13 May 2014 08:08:31 +0200
+
+mandos (1.6.5-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/copyright: Change year to "2014".
+ * debian/control (Build-Depends, Build-Depends-Indep): Moved build
+ dependencies of "mandos" package to "Build-Depends-Indep".
+ * debian/upstream/signing-key.asc: New; upstream source public key.
+ * debian/control (Standards-Version): Updated to "3.9.5".
+ * debian/control (mandos/Depends): Remove the dependency on
+ "avahi-daemon (>= 0.6.31-3) | systemd-sysv". It is unnecessary
+ since we have a workaround in debian/mandos.postinst anyway.
+
+ -- Teddy Hogeborn Sun, 11 May 2014 22:16:33 +0200
+
+mandos (1.6.4-1) unstable; urgency=medium
+
+ * New upstream release.
+ * debian/control (Build-Depends): Add Python dependencies to
+ successfully run self-tests.
+ * debian/copyright: GPLv3 now has its own license file - use it.
+ * debian/watch: Set PGP signature URL.
+
+ -- Teddy Hogeborn Sun, 16 Feb 2014 14:09:25 +0100
+
+mandos (1.6.3-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/control (Build-Depends): Added "systemd".
+ * debian/mandos.dirs (lib/systemd/system): New.
+ * debian/mandos-client.README.Debian: Refer to architecture libdir.
+ * debian/control (mandos/Depends): Add "avahi-daemon (>= 0.6.31-3) |
+ systemd-sysv".
+ * debian/mandos.postinst: If avahi-daemon is version 0.6.31-2 or older,
+ edit /etc/init.d script headers Required-Start
+ and Required-Stop to have "avahi" instead of
+ "avahi-daemon", before insserv(8) sees it.
+ * debian/mandos-client.lintian-overrides: Libdir changes.
+ * debian/rules (override_dh_fixperms): - '' -
+
+ -- Teddy Hogeborn Tue, 21 Jan 2014 22:01:30 +0100
+
+mandos (1.6.2-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/compat: Changed to "9".
+ * debian/control (Build-Depends): Changed debhelper version to (>= 9).
+ (Standards-Version): Updated to "3.9.4".
+ (DM-Upload-Allowed): Removed.
+ (mandos/Depends): Add "initscripts (>= 2.88dsf-13.3)" to be able to
+ use the "/run" directory (for mandos.pid).
+ * debian/copyright (Copyright): Update year.
+ * Fix "Mandos/gnutls fails to establish connection, "an algorithm that
+ is not enabled was negotiated"" fixed by upstream. (Closes: #702120)
+
+ -- Teddy Hogeborn Thu, 24 Oct 2013 22:33:40 +0200
+
+mandos (1.6.1-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/control (mandos/Depends): No longer depends on
+ python-gnupginterface, but does
+ depend on gnupg (<< 2).
+ (Build-Depends): Depend on debhelper 8.9.7 for using "override-*-arch"
+ and "override-*-indep" targets in debian/rules.
+ * debian/mandos-client.README: Update Linux documentation link.
+ * debian/rules: Completely rewritten to use debhelper v7.
+ * initramfs-tools-hook: Bug fix: Make sure the right version of GnuPG is
+ copied into the initramfs image. Always assume that GPGME is used to
+ avoid searching for it since the path might not be /usr/lib. Thanks
+ to Félix Sipma for the initial bug report,
+ and also thanks to Dick Middleton for some more
+ debugging. (Closes: #721903)
+ * Fix "bashism in /bin/sh script" fixed by upstream. (Closes: #690639)
+
+ -- Teddy Hogeborn Sun, 13 Oct 2013 19:03:23 +0200
+
+mandos (1.6.0-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/copyright (Copyright): Join the two lines to a single line.
+ * debian/mandos-client.README.Debian: Update to refer to the new
+ location of the example network hooks, and the new feature of using
+ all network interfaces.
+ * debian/mandos-client.docs (network-hooks.d): Removed.
+ * debian/mandos-client.examples (network-hooks.d): New.
+ * debian/rules (binary-common): Added "dh_installexamples".
+ (binary-common/dh_fixperms): Exclude new location of
+ "network-hooks.d".
+
+ -- Teddy Hogeborn Mon, 18 Jun 2012 00:15:23 +0200
+
+mandos (1.5.5-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/copyright (Format): Updated to
+ "http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/".
+ * debian/control (Build-Depends): Removed "man, locales-all".
+
+ -- Teddy Hogeborn Fri, 01 Jun 2012 20:30:41 +0200
+
+mandos (1.5.4-1) unstable; urgency=low
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Sun, 20 May 2012 15:38:34 +0200
+
+mandos (1.5.3-1.2) unstable; urgency=low
+
+ * Non-maintainer upload.
+ * Set Architecture to linux-any. (Closes: #647670)
+
+ -- Robert Millan Sun, 22 Apr 2012 16:22:01 +0200
+
+mandos (1.5.3-1.1) unstable; urgency=low
+
+ * Non-maintainer upload.
+ * Fix "mandos FTBFS on buildds": add build-dependency on locales-all and
+ pass LC_ALL to dh_auto_build to make sure we have and use the en_US.UTF-8
+ locale for manpage creation.
+ (Closes: #656178)
+
+ -- gregor herrmann Tue, 31 Jan 2012 17:56:05 +0100
+
+mandos (1.5.3-1) unstable; urgency=low
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Sun, 15 Jan 2012 22:05:54 +0100
+
+mandos (1.5.2-1) unstable; urgency=low
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Sun, 08 Jan 2012 11:17:20 +0100
+
+mandos (1.5.1-1) unstable; urgency=low
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Sun, 01 Jan 2012 21:53:31 +0100
+
+mandos (1.5.0-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/control (mandos-client/Depends): Added "initramfs-tools".
+ * debian/mandos-client.README.Debian: Corrected mail address and adjust
+ wording.
+ * debian/rules (binary-common): Exclude new nework-hooks.d directory
+ from dh_fixperms.
+ * debian/mandos-client.README.Debian: Document network hook facility.
+ * debian/mandos-client.docs (network-hooks.d): Added.
+ * debian/mandos.dirs (var/lib/mandos): Added.
+ * debian/mandos.postinst: Fix ownership of /var/lib/mandos.
+ * debian/control (mandos/Depends): Added "python-gnupginterface".
+
+ -- Teddy Hogeborn Sun, 01 Jan 2012 05:58:11 +0100
+
+mandos (1.4.1-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/control (Build-Depends): Added "man".
+ * debian/control (Conflicts): Changed to "Breaks:".
+ * debian/copyright: Updated format.
+ * debian/mandos-client.postinst: Use "set -e" instead of "#!/bin/sh -e".
+ * debian/mandos-client.postrm: - '' -
+ * debian/mandos.postinst: - '' -
+ * debian/mandos.prerm: Consistent magic.
+
+ -- Björn Påhlsson Sat, 15 Oct 2011 18:18:52 +0200
+
+mandos (1.4.0-1) unstable; urgency=low
+
+ * New upstream release.
+ * Fix "FTBFS with binutils-gold": Added "-Xlinker --as-needed" to
+ LDFLAGS in Makefile. (Closes: #632145)
+ * Fix "/run transition: uses obsolete /dev/.initramfs": Try both old and
+ new PID file locations. (Closes: #643554)
+ * debian/source/local-options: New; contains "--single-debian-patch".
+ * debian/control (Standards-Version): Upgraded to "3.9.2".
+ (DM-Upload-Allowed): New; set to "yes".
+ * debian/control: Changed domain from "fukt.bsnet.se" to "recompile.se".
+ * debian/copyright: - '' -
+ * debian/mandos-client.README.Debian: - '' -
+ * debian/mandos.README.Debian: - '' -
+ * debian/watch: - '' -
+ * debian/control (mandos/Description): Fix language to placate lintian.
+
+ -- Teddy Hogeborn Sun, 09 Oct 2011 19:15:08 +0200
+
+mandos (1.3.1-1) unstable; urgency=low
+
+ * New upstream release.
+ * Conflict with correct version of dropbear.
+ * New version uses argparse; depend on python (<=2.7) | python-argparse.
+
+ -- Teddy Hogeborn Wed, 27 Jul 2011 19:47:17 +0200
+
+mandos (1.3.0-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/control (mandos): Depend on Python 2.6, remove dependency on
+ python-multiprocessing.
+ (mandos-client): Conflict with dropbear (<< 0.52-5).
+ * debian/mandos-client.postrm (purge): Bug fix: update initramfs also on
+ purge.
+ * debian/mandos-client.lintian-overrides: Added plugins.d/plymouth.
+
+ -- Teddy Hogeborn Tue, 08 Mar 2011 20:22:57 +0100
+
+mandos (1.2.3-1) experimental; urgency=low
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Mon, 11 Oct 2010 19:37:31 +0200
+
+mandos (1.2.2-1) experimental; urgency=low
+
+ * New upstream release.
+ * plugins.d/splashy.c: Only use ELIBBAD if defined. (Closes: #599256)
+
+ -- Teddy Hogeborn Thu, 07 Oct 2010 20:27:54 +0200
+
+mandos (1.2.1-3) experimental; urgency=low
+
+ * debian/changelog: Include entry for NMU of version 1.0.14-1.1.
+
+ -- Teddy Hogeborn Tue, 05 Oct 2010 20:58:38 +0200
+
+mandos (1.2.1-2) unstable; urgency=low
+
+ * debian/source/format: New; contains "3.0 (quilt)". Really.
+
+ -- Björn Påhlsson Sat, 02 Oct 2010 19:46:59 +0200
+
+mandos (1.2.1-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/source/format: New; contains "3.0 (quilt)".
+
+ -- Björn Påhlsson Sat, 02 Oct 2010 19:03:58 +0200
+
+mandos (1.2-1) unstable; urgency=low
+
+ * New upstream release.
+ * Makefile (LINK_FORTIFY_LD): Remove "-fPIE". (Closes: #557076)
+ * debian/control: Add gnupg dependency to "mandos-client" and removed it
+ from "mandos". Added dependency on "python-urwid" "mandos" since the
+ new "mandos-monitor" utility needs it, and on "python (>=2.6) |
+ python-multiprocessing" since the Mandos server now uses it.
+ * debian/rules: Set BROKEN_PIE on mips and mipsel if a known buggy
+ version of binutils is used.
+ * debian/mandos.docs: Also install "/usr/share/doc/mandos/DBUS-API".
+ * debian/mandos.dirs: Added "etc/dbus-1/system.d".
+ * debian/mandos-client.README.Debian: Update info about DEVICE setting
+ of initramfs.conf.
+ * debian/mandos-client.README.Debian: Remove warning about --connect not
+ looping, since it now does.
+
+ -- Teddy Hogeborn Tue, 28 Sep 2010 20:46:11 +0200
+
+mandos (1.0.14-1.1) unstable; urgency=low
+
+ * Non-maintainer upload.
+ * Rebuild against libavahi-core-dev (>= 0.6.26-1).
+
+ -- Michael Biebl Mon, 12 Jul 2010 16:34:34 +0200
+
+mandos (1.0.14-1) unstable; urgency=low (HIGH on mips and mipsel)
+
+ * New upstream release.
+ * debian/rules: Build with BROKEN_PIE set on mips and mipsel
+ architectures - fixes FTBFS there.
+
+ -- Teddy Hogeborn Sun, 25 Oct 2009 20:10:09 +0100
+
+mandos (1.0.13-1) unstable; urgency=high
+
+ * New upstream release.
+ * Do not copy unnecessary files to initrd (Closes: #551907)
+
+ -- Teddy Hogeborn Thu, 22 Oct 2009 00:53:21 +0200
+
+mandos (1.0.12-1) unstable; urgency=low
+
+ * New upstream release.
+ * init.d-mandos: Correct dependencies (Closes: #546928)
+ * debian/control (Standards-Version): Changed to "3.8.3".
+ * debian/mandos-client.README.Debian: Improved wording and formatting.
+ Updated location of nfsroot.txt.
+ * debian/mandos.README.Debian: Improved wording and formatting.
+ * debian/mandos-client.postinst (configure): Don't look for user and
+ group with the old name if upgrading from a new enough version.
+ * debian/mandos.postinst (configure): - '' -
+ * debian/mandos-client.README.Debian: Added text about non-usability of
+ pseudo-network interfaces.
+
+ -- Teddy Hogeborn Thu, 17 Sep 2009 15:03:59 +0200
+
+mandos (1.0.11-1) unstable; urgency=low
+
+ * debian/control (Standards-Version): Changed to "3.8.1".
+ * Makefile (GNUTLS_CFLAGS, GNUTLS_CFLAGS): Use "pkg-config" instead of
+ the old "libgnutls-config" script. (Closes: #529836)
+
+ -- Teddy Hogeborn Sat, 23 May 2009 07:12:20 +0200
+
+mandos (1.0.10-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/mandos-client.postinst (update_initramfs): Fix permissions of
+ old initrd.img-*.bak files.
+
+ -- Teddy Hogeborn Sun, 17 May 2009 04:56:35 +0200
+
+mandos (1.0.9-1) unstable; urgency=low
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Sun, 17 May 2009 02:59:45 +0200
+
+mandos (1.0.8-1) unstable; urgency=low
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Wed, 25 Feb 2009 02:26:57 +0100
+
+mandos (1.0.7-1) unstable; urgency=low
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Tue, 24 Feb 2009 12:58:06 +0100
+
+mandos (1.0.6-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/mandos-client.postinst: Converted to Bourne shell. Also
+ minor message change.
+ * debian/mandos-client.postrm: Minor message change.
+ * debian/mandos.postinst: Converted to Bourne shell. Also minor
+ message change.
+ * debian/mandos.prerm: Minor message change.
+ * debian/rules (install-indep): Removed "--no-start" from
+ dh_installinit.
+ * debian/mandos-client.lintian-overrides: Remove obsolete override for
+ unbreakable line in plugin-runner manual page.
+ * debian/control (mandos/Depends): Added "python-gobject".
+ * debian/mandos-client.dirs: Change
+ "usr/share/initramfs-tools/scripts/local-top" to
+ "usr/share/initramfs-tools/scripts/init-premount".
+ * debian/mandos-client.README.Debian: Add reference to initramfs.conf
+ and nfsroot.txt. New section about the new non-local connection
+ feature.
+
+ -- Teddy Hogeborn Fri, 13 Feb 2009 09:27:25 +0100
+
+mandos (1.0.5-1) unstable; urgency=low
+
+ * New upstream release.
+
+ -- Teddy Hogeborn Sat, 17 Jan 2009 02:26:00 +0100
+
+mandos (1.0.4-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/watch: New file.
+ * debian/mandos-client.README.Debian: Document new "mandos=off" kernel
+ parameter.
+
+ -- Teddy Hogeborn Thu, 15 Jan 2009 05:49:22 +0100
+
+mandos (1.0.3-2) unstable; urgency=low
+
+ * Removed some now-unused debconf files.
+ * Changed postinst scripts to not source debconf/confmodule.
+ * Removed po-debconf from build-depends.
+
+ -- Teddy Hogeborn Tue, 06 Jan 2009 21:28:20 +0100
+
+mandos (1.0.3-1) unstable; urgency=low
+
+ * New upstream release.
+ * Add -Xlinker to linker flags to fix FTBFS for some architectures.
+ Thanks to Thiemo Seufer for the report and
+ fix. (Closes: #509398)
+ * Remove debconf use altogether, thereby stopping debconf abuse. Thanks
+ to Christian Perrier . (Closes: #509653)
+ * Add NEWS file to /usr/share/doc directories.
+ * Use and create "_mandos" user+group. Rename old user+group created by
+ older versions of this package.
+ * Fix manual pages by adding build-depend on "docbook-xml".
+
+ -- Teddy Hogeborn Tue, 06 Jan 2009 01:21:20 +0100
+
+mandos (1.0.2-1) unstable; urgency=low
+
+ * New upstream release.
+ * debian/copyright: Rewritten to conform to
+ .
+
+ -- Teddy Hogeborn Fri, 17 Oct 2008 20:42:12 +0200
+
+mandos (1.0.1-1) unstable; urgency=low
+
+ * New upstream release.
+ * Separate /usr/share/doc/mandos-client/README.Debian into sections with
+ headlines. Add instructions on how to test the server and verify the
+ password.
+
+ -- Teddy Hogeborn Tue, 07 Oct 2008 23:07:23 +0200
+
+mandos (1.0-2) unstable; urgency=low
+
+ * Added comments in debian/*.lintian-overrides files. Added Debian
+ revison number to version number.
+
+ -- Teddy Hogeborn Wed, 01 Oct 2008 17:23:35 +0200
+
+mandos (1.0-1) unstable; urgency=low
+
+ * Initial Release. (Closes: #500727).
+
+ -- Teddy Hogeborn Tue, 30 Sep 2008 21:58:43 +0200
=== added file 'debian/compat'
--- debian/compat 1970-01-01 00:00:00 +0000
+++ debian/compat 2018-02-06 20:03:50 +0000
@@ -0,0 +1,1 @@
+10
=== added file 'debian/control'
--- debian/control 1970-01-01 00:00:00 +0000
+++ debian/control 2019-10-20 03:39:15 +0000
@@ -0,0 +1,70 @@
+Source: mandos
+Section: admin
+Priority: optional
+Maintainer: Mandos Maintainers
+Uploaders: Teddy Hogeborn ,
+ Björn Påhlsson
+Build-Depends: debhelper (>= 10), docbook-xml, docbook-xsl,
+ libavahi-core-dev, libgpgme-dev | libgpgme11-dev,
+ libglib2.0-dev (>=2.40), libgnutls28-dev (>= 3.3.0),
+ libgnutls28-dev (>= 3.6.6) | libgnutls28-dev (<< 3.6.0),
+ xsltproc, pkg-config, libnl-route-3-dev, systemd
+Build-Depends-Indep: python3 (>= 3), python3-dbus, python3-gi,
+ po-debconf
+Standards-Version: 4.4.1
+Vcs-Bzr: https://ftp.recompile.se/pub/mandos/trunk
+Vcs-Browser: https://bzr.recompile.se/loggerhead/mandos/trunk/files
+Homepage: https://www.recompile.se/mandos
+Rules-Requires-Root: binary-targets
+
+Package: mandos
+Architecture: all
+Depends: ${misc:Depends}, python3 (>= 3), libgnutls30 (>= 3.3.0),
+ libgnutls30 (>= 3.6.6) | libgnutls30 (<< 3.6.0),
+ python3-dbus, python3-gi, avahi-daemon, adduser,
+ python3-urwid, gnupg2 | gnupg,
+ systemd-sysv | lsb-base (>= 3.0-6),
+ debconf (>= 1.5.5) | debconf-2.0
+Recommends: ssh-client | fping
+Suggests: libc6-dev | libc-dev, c-compiler
+Description: server giving encrypted passwords to Mandos clients
+ This is the server part of the Mandos system, which allows
+ computers to have encrypted root file systems and at the
+ same time be capable of remote and/or unattended reboots.
+ .
+ The computers run a small client program in the initial RAM
+ disk environment which will communicate with a server over a
+ network. All network communication is encrypted using TLS.
+ The clients are identified by the server using a TLS public
+ key; each client has one unique to it. The server sends the
+ clients an encrypted password. The encrypted password is
+ decrypted by the clients using an OpenPGP key, and the
+ password is then used to unlock the root file system,
+ whereupon the computers can continue booting normally.
+
+Package: mandos-client
+Architecture: linux-any
+Depends: ${shlibs:Depends}, ${misc:Depends}, adduser,
+ cryptsetup (<< 2:2.0.3-1) | cryptsetup-initramfs,
+ initramfs-tools (>= 0.99) | dracut (>= 044+241-3),
+ dpkg-dev (>=1.16.0),
+ gnutls-bin (>= 3.6.6) | libgnutls30 (<< 3.6.0),
+ debconf (>= 1.5.5) | debconf-2.0
+Recommends: ssh
+Breaks: dropbear (<= 0.53.1-1)
+Enhances: cryptsetup
+Conflicts: dracut-config-generic
+Description: do unattended reboots with an encrypted root file system
+ This is the client part of the Mandos system, which allows
+ computers to have encrypted root file systems and at the
+ same time be capable of remote and/or unattended reboots.
+ .
+ The computers run a small client program in the initial RAM
+ disk environment which will communicate with a server over a
+ network. All network communication is encrypted using TLS.
+ The clients are identified by the server using a TLS public
+ key; each client has one unique to it. The server sends the
+ clients an encrypted password. The encrypted password is
+ decrypted by the clients using an OpenPGP key, and the
+ password is then used to unlock the root file system,
+ whereupon the computers can continue booting normally.
=== added file 'debian/copyright'
--- debian/copyright 1970-01-01 00:00:00 +0000
+++ debian/copyright 2019-02-10 04:20:26 +0000
@@ -0,0 +1,26 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: Mandos
+Upstream-Contact: Mandos
+Source:
+
+Files: *
+Copyright: Copyright © 2008-2019 Teddy Hogeborn
+ Copyright © 2008-2019 Björn Påhlsson
+License: GPL-3+
+ This file is part of Mandos.
+ .
+ Mandos 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.
+ .
+ Mandos 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 Mandos. If not, see .
+ .
+ On Debian systems, the complete text of the GNU General Public
+ License can be found in "/usr/share/common-licenses/GPL-3".
=== added file 'debian/mandos-client.README.Debian'
--- debian/mandos-client.README.Debian 1970-01-01 00:00:00 +0000
+++ debian/mandos-client.README.Debian 2019-07-27 10:11:45 +0000
@@ -0,0 +1,119 @@
+This file documents the next steps to take after installation of the
+Debian package, and also contain some notes specific to the Debian
+packaging which are not also in the manual.
+
+* Adding a Client Password to the Server
+
+ The server must be given a password to give back to the client on
+ boot time. This password must be a one which can be used to unlock
+ the root file system device. On the *client*, run this command:
+
+ mandos-keygen --password
+
+ It will prompt for a password and output a config file section.
+ This output should be copied to the Mandos server and added to the
+ file "/etc/mandos/clients.conf" there.
+
+* Testing that it Works (Without Rebooting)
+
+ After the server has been started with this client's key added, it
+ is possible to verify that the correct password will be received by
+ this client by running the command, on the client:
+
+ MANDOSPLUGINHELPERDIR=/usr/lib/$(dpkg-architecture \
+ -qDEB_HOST_MULTIARCH)/mandos/plugin-helpers \
+ /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH \
+ )/mandos/plugins.d/mandos-client \
+ --pubkey=/etc/keys/mandos/pubkey.txt \
+ --seckey=/etc/keys/mandos/seckey.txt \
+ --tls-privkey=/etc/keys/mandos/tls-privkey.pem \
+ --tls-pubkey=/etc/keys/mandos/tls-pubkey.pem; echo
+
+ This command should retrieve the password from the server, decrypt
+ it, and output it to standard output. There it can be verified to
+ be the correct password, before rebooting.
+
+* Emergency Escape
+
+ If it ever should be necessary, the Mandos client can be temporarily
+ prevented from running at startup by passing the parameter
+ "mandos=off" to the kernel.
+
+* Specifying a Client Network Interface
+
+ At boot time the network interfaces to use will by default be
+ automatically detected. If this should result in incorrect
+ interfaces, edit the DEVICE setting in the
+ "/etc/initramfs-tools/initramfs.conf" file. (The default setting is
+ empty, meaning it will autodetect the interfaces.) *If* the DEVICE
+ setting is changed, it will be necessary to update the initrd image
+ by running this command:
+
+ (For initramfs-tools:)
+ update-initramfs -k all -u
+
+ (For dracut:)
+ dpkg-reconfigure dracut
+
+ The device can also be overridden at boot time on the Linux kernel
+ command line using the sixth colon-separated field of the "ip="
+ option; for exact syntax, read the documentation in the file
+ "/usr/share/doc/linux-doc-*/Documentation/filesystems/nfs/nfsroot.txt",
+ available in the "linux-doc-*" package.
+
+ Note that since the network interfaces are used in the initial RAM
+ disk environment, the network interfaces *must* exist at that stage.
+ Thus, an interface can *not* be a pseudo-interface such as "br0" or
+ "tun0"; instead, only real interfaces (such as "enp1s0" or "eth0")
+ can be used. This can be overcome by writing a "network hook"
+ program to create an interface (see mandos-client(8mandos)) and
+ placing it in "/etc/mandos/network-hooks.d", from where it will be
+ copied into the initial RAM disk. Example network hook scripts can
+ be found in "/usr/share/doc/mandos-client/examples/network-hooks.d".
+
+* User-Supplied Plugins
+
+ Any plugins found in "/etc/mandos/plugins.d" will override and add
+ to the normal Mandos plugins. When adding or changing plugins, do
+ not forget to update the initital RAM disk image:
+
+ (For initramfs-tools:)
+ update-initramfs -k all -u
+
+ (For dracut:)
+ dpkg-reconfigure dracut
+
+* Do *NOT* Edit "/etc/crypttab"
+
+ It is NOT necessary to edit "/etc/crypttab" to specify
+ "/usr/lib/mandos/plugin-runner" as a keyscript for the root file
+ system; if no keyscript is given for the root file system, the
+ Mandos client will be the new default way for getting a password for
+ the root file system when booting.
+
+* Non-local Connection (Not Using ZeroConf)
+
+ If the "ip=" kernel command line option is used to specify a
+ complete IP address and device name, as noted above, it then becomes
+ possible to specify a specific IP address and port to connect to,
+ instead of using ZeroConf. The syntax for doing this is
+ "mandos=connect::" on the kernel command
+ line.
+
+ For very advanced users, it is 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
+ any extra "--connect" options to mandos-client at boot. For this to
+ work, "--options-for=mandos-client:--connect=:" needs
+ to be manually added to the file "/etc/mandos/plugin-runner.conf".
+
+* Diffie-Hellman Parameters
+
+ On installation, a file with Diffie-Hellman parameters,
+ /etc/keys/mandos/dhparams.pem, will be generated and automatically
+ installed into the initital RAM disk image and also used by the
+ Mandos Client on boot. If different parameters are needed for
+ policy or other reasons, simply replace the existing dhparams.pem
+ file and update the initital RAM disk image.
+
+ -- Teddy Hogeborn , Mon, 15 Jul 2019 16:47:02 +0200
=== added file 'debian/mandos-client.dirs'
--- debian/mandos-client.dirs 1970-01-01 00:00:00 +0000
+++ debian/mandos-client.dirs 2019-08-18 04:14:31 +0000
@@ -0,0 +1,8 @@
+usr/share/man/man8
+usr/sbin
+usr/share/initramfs-tools/hooks
+usr/share/initramfs-tools/conf.d
+usr/share/initramfs-tools/conf-hooks.d
+usr/share/initramfs-tools/scripts/init-premount
+usr/share/initramfs-tools/scripts/local-premount
+usr/lib/sysusers.d
=== added file 'debian/mandos-client.docs'
--- debian/mandos-client.docs 1970-01-01 00:00:00 +0000
+++ debian/mandos-client.docs 2012-06-01 21:48:12 +0000
@@ -0,0 +1,3 @@
+NEWS
+README
+TODO
=== added file 'debian/mandos-client.examples'
--- debian/mandos-client.examples 1970-01-01 00:00:00 +0000
+++ debian/mandos-client.examples 2012-06-01 21:48:12 +0000
@@ -0,0 +1,1 @@
+network-hooks.d
=== added file 'debian/mandos-client.links'
--- debian/mandos-client.links 1970-01-01 00:00:00 +0000
+++ debian/mandos-client.links 2008-09-19 13:50:22 +0000
@@ -0,0 +1,1 @@
+usr/share/man/man8/plugin-runner.8mandos.gz usr/share/man/man5/plugin-runner.conf.5mandos.gz
=== added file 'debian/mandos-client.lintian-overrides'
--- debian/mandos-client.lintian-overrides 1970-01-01 00:00:00 +0000
+++ debian/mandos-client.lintian-overrides 2019-08-05 21:14:05 +0000
@@ -0,0 +1,44 @@
+# This directory contains secret client key files.
+#
+mandos-client binary: non-standard-dir-perm etc/keys/mandos/ 0700 != 0755
+
+# The directory /usr/lib//mandos/plugins.d contains setuid
+# binaries which are not meant to be run outside an initial RAM disk
+# environment (except for test purposes). It would be insecure to
+# allow anyone to run them.
+#
+mandos-client binary: non-standard-dir-perm usr/lib/*/mandos/plugins.d/ 0700 != 0755
+# Likewise for helper executables for plugins
+mandos-client binary: non-standard-dir-perm usr/lib/*/mandos/plugin-helpers/ 0700 != 0755
+
+# These binaries must be setuid root, since they need root powers, but
+# are started by plugin-runner(8mandos), which runs all plugins as
+# user/group "_mandos". These binaries are not run in a running
+# system, but in an initial RAM disk environment. Here they are
+# protected from non-root access by the directory permissions, above.
+#
+mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/mandos-client 4755 root/root
+mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/askpass-fifo 4755 root/root
+mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/splashy 4755 root/root
+mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/usplash 4755 root/root
+mandos-client binary: setuid-binary usr/lib/*/mandos/plugins.d/plymouth 4755 root/root
+
+# The directory /etc/mandos/plugins.d can be used by local system
+# administrators to place plugins in, overriding and complementing
+# /usr/lib//mandos/plugins.d, and must be likewise protected.
+#
+mandos-client binary: non-standard-dir-perm etc/mandos/plugins.d/ 0700 != 0755
+# Likewise for plugin-helpers directory
+mandos-client binary: non-standard-dir-perm etc/mandos/plugin-helpers/ 0700 != 0755
+
+# The debconf templates is only used for displaying information
+# detected in the postinst, not for saving answers to questions, so we
+# don't need a .config file.
+mandos-client binary: no-debconf-config
+
+# The notice displayed from the postinst script really is critical
+mandos-client binary: postinst-uses-db-input
+
+# These are very important to work around bugs or changes in the old
+# versions, and there is no pressing need to remove them.
+mandos-client binary: maintainer-script-supports-ancient-package-version *
=== added file 'debian/mandos-client.postinst'
--- debian/mandos-client.postinst 1970-01-01 00:00:00 +0000
+++ debian/mandos-client.postinst 2019-07-27 10:11:45 +0000
@@ -0,0 +1,204 @@
+#!/bin/sh
+# This script can be called in the following ways:
+#
+# After the package was installed:
+# configure
+#
+#
+# If prerm fails during upgrade or fails on failed upgrade:
+# abort-upgrade
+#
+# If prerm fails during deconfiguration of a package:
+# abort-deconfigure in-favour
+# removing
+#
+# If prerm fails during replacement due to conflict:
+# abort-remove in-favour
+
+. /usr/share/debconf/confmodule
+
+set -e
+
+# Update the initial RAM file system image
+update_initramfs()
+{
+ if command -v update-initramfs >/dev/null; then
+ update-initramfs -k all -u
+ elif command -v dracut >/dev/null; then
+ dracut_version="`dpkg-query --showformat='${Version}' --show dracut`"
+ if dpkg --compare-versions "$dracut_version" lt 043-1 \
+ && bash -c '. /etc/dracut.conf; . /etc/dracut.conf.d/*; [ "$hostonly" != yes ]'; then
+ echo 'Dracut is not configured to use hostonly mode!' >&2
+ return 1
+ fi
+ # Logic taken from dracut.postinst
+ for kernel in /boot/vmlinu[xz]-*; do
+ kversion="${kernel#/boot/vmlinu[xz]-}"
+ # Dracut preserves old permissions of initramfs image
+ # files, so we adjust permissions before creating new
+ # initramfs image containing secret keys.
+ chmod go-r /boot/initrd.img-"$kversion"
+ if [ "$kversion" != "*" ]; then
+ /etc/kernel/postinst.d/dracut "$kversion"
+ fi
+ done
+ fi
+
+ if dpkg --compare-versions "$2" lt-nl "1.0.10-1"; then
+ # Make old initrd.img files unreadable too, in case they were
+ # created with mandos-client 1.0.8 or older.
+ find /boot -maxdepth 1 -type f -name "initrd.img-*.bak" \
+ -print0 | xargs --null --no-run-if-empty chmod o-r
+ fi
+}
+
+# Add user and group
+add_mandos_user(){
+ # Rename old "mandos" user and group
+ if dpkg --compare-versions "$2" lt "1.0.3-1"; then
+ case "`getent passwd mandos`" in
+ *:Mandos\ password\ system,,,:/nonexistent:/bin/false)
+ usermod --login _mandos mandos
+ groupmod --new-name _mandos mandos
+ return
+ ;;
+ esac
+ fi
+ # Create new user and group
+ if ! getent passwd _mandos >/dev/null; then
+ adduser --system --force-badname --quiet --home /nonexistent \
+ --no-create-home --group --disabled-password \
+ --gecos "Mandos password system" _mandos
+ fi
+}
+
+# Create client key pairs
+create_keys(){
+ # If the OpenPGP key files do not exist, generate all keys using
+ # mandos-keygen
+ if ! [ -r /etc/keys/mandos/pubkey.txt \
+ -a -r /etc/keys/mandos/seckey.txt ]; then
+ mandos-keygen
+ gpg-connect-agent KILLAGENT /bye || :
+ return 0
+ fi
+
+ # Remove any bad TLS keys by 1.8.0-1
+ if dpkg --compare-versions "$2" eq "1.8.0-1" \
+ || dpkg --compare-versions "$2" eq "1.8.0-1~bpo9+1"; then
+ # Is the key bad?
+ if ! certtool --password='' \
+ --load-privkey=/etc/keys/mandos/tls-privkey.pem \
+ --outfile=/dev/null --pubkey-info --no-text \
+ 2>/dev/null; then
+ shred --remove -- /etc/keys/mandos/tls-privkey.pem \
+ 2>/dev/null || :
+ rm --force -- /etc/keys/mandos/tls-pubkey.pem
+ fi
+ fi
+
+ # If the TLS keys already exists, do nothing
+ if [ -r /etc/keys/mandos/tls-privkey.pem \
+ -a -r /etc/keys/mandos/tls-pubkey.pem ]; then
+ return 0
+ fi
+
+ # Try to create the TLS keys
+
+ TLS_PRIVKEYTMP="`mktemp -t mandos-client-privkey.XXXXXXXXXX`"
+
+ if certtool --generate-privkey --password='' \
+ --outfile "$TLS_PRIVKEYTMP" --sec-param ultra \
+ --key-type=ed25519 --pkcs8 --no-text 2>/dev/null; then
+
+ local umask=$(umask)
+ umask 077
+ cp --archive "$TLS_PRIVKEYTMP" /etc/keys/mandos/tls-privkey.pem
+ shred --remove -- "$TLS_PRIVKEYTMP" 2>/dev/null || :
+
+ # First try certtool from GnuTLS
+ if ! certtool --password='' \
+ --load-privkey=/etc/keys/mandos/tls-privkey.pem \
+ --outfile=/etc/keys/mandos/tls-pubkey.pem --pubkey-info \
+ --no-text 2>/dev/null; then
+ # Otherwise try OpenSSL
+ if ! openssl pkey -in /etc/keys/mandos/tls-privkey.pem \
+ -out /etc/keys/mandos/tls-pubkey.pem -pubout; then
+ rm --force /etc/keys/mandos/tls-pubkey.pem
+ # None of the commands succeded; give up
+ umask $umask
+ return 1
+ fi
+ fi
+ umask $umask
+
+ key_id=$(mandos-keygen --passfile=/dev/null \
+ | grep --regexp="^key_id[ =]")
+
+ db_version 2.0
+ db_fset mandos-client/key_id seen false
+ db_reset mandos-client/key_id
+ db_subst mandos-client/key_id key_id $key_id
+ db_input critical mandos-client/key_id || true
+ db_go
+ db_stop
+ else
+ shred --remove -- "$TLS_PRIVKEYTMP" 2>/dev/null || :
+ fi
+}
+
+create_dh_params(){
+ if [ -r /etc/keys/mandos/dhparams.pem ]; then
+ return 0
+ fi
+ # Create a Diffe-Hellman parameters file
+ DHFILE="`mktemp -t mandos-client-dh-parameters.XXXXXXXXXX.pem`"
+ # First try certtool from GnuTLS
+ if ! certtool --generate-dh-params --sec-param high \
+ --outfile "$DHFILE"; then
+ # Otherwise try OpenSSL
+ if ! openssl genpkey -genparam -algorithm DH -out "$DHFILE" \
+ -pkeyopt dh_paramgen_prime_len:3072; then
+ # None of the commands succeded; give up
+ rm -- "$DHFILE"
+ return 1
+ fi
+ fi
+ sed --in-place --expression='0,/^-----BEGIN DH PARAMETERS-----$/d' \
+ "$DHFILE"
+ sed --in-place --expression='1i-----BEGIN DH PARAMETERS-----' \
+ "$DHFILE"
+ cp --archive "$DHFILE" /etc/keys/mandos/dhparams.pem
+ rm -- "$DHFILE"
+}
+
+case "$1" in
+ configure)
+ add_mandos_user "$@"
+ create_keys "$@"
+ create_dh_params "$@" || :
+ update_initramfs "$@"
+ if dpkg --compare-versions "$2" lt-nl "1.7.10-1"; then
+ PLUGINHELPERDIR=/usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null)/mandos/plugin-helpers
+ if ! dpkg-statoverride --list "$PLUGINHELPERDIR" \
+ >/dev/null 2>&1; then
+ chmod u=rwx,go= -- "$PLUGINHELPERDIR"
+ fi
+ if ! dpkg-statoverride --list /etc/mandos/plugin-helpers \
+ >/dev/null 2>&1; then
+ chmod u=rwx,go= -- /etc/mandos/plugin-helpers
+ fi
+ fi
+ ;;
+ abort-upgrade|abort-deconfigure|abort-remove)
+ ;;
+
+ *)
+ echo "$0 called with unknown argument '$1'" 1>&2
+ exit 1
+ ;;
+esac
+
+#DEBHELPER#
+
+exit 0
=== added file 'debian/mandos-client.postrm'
--- debian/mandos-client.postrm 1970-01-01 00:00:00 +0000
+++ debian/mandos-client.postrm 2019-08-05 14:31:51 +0000
@@ -0,0 +1,73 @@
+#!/bin/sh
+# This script can be called in the following ways:
+#
+# After the package was removed:
+# remove
+#
+# After the package was purged:
+# purge
+#
+# After the package was upgraded:
+# upgrade
+# if that fails:
+# failed-upgrade
+#
+#
+# After all of the packages files have been replaced:
+# disappear
+#
+#
+# If preinst fails during install:
+# abort-install
+#
+# If preinst fails during upgrade of removed package:
+# abort-install
+#
+# If preinst fails during upgrade:
+# abort-upgrade
+
+set -e
+
+# Update the initial RAM file system image
+update_initramfs()
+{
+ if command -v update-initramfs >/dev/null; then
+ update-initramfs -k all -u
+ elif command -v dracut >/dev/null; then
+ # Logic taken from dracut.postinst
+ for kernel in /boot/vmlinu[xz]-*; do
+ kversion="${kernel#/boot/vmlinu[xz]-}"
+ if [ "$kversion" != "*" ]; then
+ /etc/kernel/postinst.d/dracut "$kversion"
+ fi
+ done
+ fi
+}
+
+case "$1" in
+ remove)
+ update_initramfs
+ ;;
+
+ purge)
+ shred --remove /etc/keys/mandos/seckey.txt 2>/dev/null || :
+ rm --force /etc/mandos/plugin-runner.conf \
+ /etc/keys/mandos/pubkey.txt \
+ /etc/keys/mandos/seckey.txt \
+ /etc/keys/mandos/tls-privkey.pem \
+ /etc/keys/mandos/tls-pubkey.pem \
+ /etc/keys/mandos/dhparams.pem 2>/dev/null
+ update_initramfs
+ ;;
+ upgrade|failed-upgrade|disappear|abort-install|abort-upgrade)
+ ;;
+
+ *)
+ echo "$0 called with unknown argument '$1'" 1>&2
+ exit 1
+ ;;
+esac
+
+#DEBHELPER#
+
+exit 0
=== added file 'debian/mandos-client.templates'
--- debian/mandos-client.templates 1970-01-01 00:00:00 +0000
+++ debian/mandos-client.templates 2019-08-05 21:00:35 +0000
@@ -0,0 +1,19 @@
+Template: mandos-client/key_id
+Type: note
+_description: New client option "${key_id}" is REQUIRED on server
+ A new "key_id" client option is REQUIRED in the server's clients.conf
+ file, otherwise this computer most likely will not reboot unattended.
+ This option:
+ .
+ ${key_id}
+ .
+ must be added (all on one line!) on the Mandos server host, in the file
+ /etc/mandos/clients.conf, right before the "fingerprint" option for this
+ Mandos client. You must edit that file on that server and add this
+ option.
+ .
+ With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as
+ TLS session keys. A new TLS key pair has been generated and will be used
+ as identification, but the key ID of the public key needs to be added to
+ the server, since this will now be used to identify the client to the
+ server.
=== added file 'debian/mandos.README.Debian'
--- debian/mandos.README.Debian 1970-01-01 00:00:00 +0000
+++ debian/mandos.README.Debian 2011-10-05 16:00:56 +0000
@@ -0,0 +1,10 @@
+The Mandos server is useless without at least one configured client in
+/etc/mandos/clients.conf. To create one, install the "mandos-client"
+package on a client computer, and, on the client, run the command
+
+ # mandos-keygen --password
+
+to get a config file stanza. Append the output of that command to the
+file "/etc/mandos/clients.conf" on the Mandos server computer.
+
+ -- Teddy Hogeborn , Wed, 5 Oct 2011 17:51:22 +0200
=== added file 'debian/mandos.dirs'
--- debian/mandos.dirs 1970-01-01 00:00:00 +0000
+++ debian/mandos.dirs 2019-08-18 00:23:21 +0000
@@ -0,0 +1,10 @@
+usr/share/man/man5
+usr/share/man/man8
+etc/init.d
+etc/default
+etc/dbus-1/system.d
+usr/sbin
+var/lib/mandos
+lib/systemd/system
+usr/lib/tmpfiles.d
+usr/lib/sysusers.d
=== added file 'debian/mandos.docs'
--- debian/mandos.docs 1970-01-01 00:00:00 +0000
+++ debian/mandos.docs 2010-09-12 03:00:40 +0000
@@ -0,0 +1,4 @@
+NEWS
+README
+TODO
+DBUS-API
=== added file 'debian/mandos.lintian-overrides'
--- debian/mandos.lintian-overrides 1970-01-01 00:00:00 +0000
+++ debian/mandos.lintian-overrides 2019-08-05 21:14:05 +0000
@@ -0,0 +1,16 @@
+# This config file will normally have encrypted secret client keys in
+# it, so it must be kept unreadable for non-root users.
+#
+mandos binary: non-standard-file-perm etc/mandos/clients.conf 0600 != 0644
+
+# The debconf templates is only used for displaying information
+# detected in the postinst, not for saving answers to questions, so we
+# don't need a .config file.
+mandos binary: no-debconf-config
+
+# The notice displayed from the postinst script really is critical
+mandos binary: postinst-uses-db-input
+
+# These are very important to work around bugs or changes in the old
+# versions, and there is no pressing need to remove them.
+mandos binary: maintainer-script-supports-ancient-package-version *
=== added file 'debian/mandos.postinst'
--- debian/mandos.postinst 1970-01-01 00:00:00 +0000
+++ debian/mandos.postinst 2019-08-18 00:05:36 +0000
@@ -0,0 +1,118 @@
+#!/bin/sh
+# This script can be called in the following ways:
+#
+# After the package was installed:
+# configure
+#
+#
+# If prerm fails during upgrade or fails on failed upgrade:
+# abort-upgrade
+#
+# If prerm fails during deconfiguration of a package:
+# abort-deconfigure in-favour
+# removing
+#
+# If prerm fails during replacement due to conflict:
+# abort-remove in-favour
+
+. /usr/share/debconf/confmodule
+
+set -e
+
+case "$1" in
+ configure)
+ # Rename old "mandos" user and group
+ if dpkg --compare-versions "$2" lt "1.0.3-1"; then
+ case "`getent passwd mandos`" in
+ *:Mandos\ password\ system,,,:/nonexistent:/bin/false)
+ usermod --login _mandos mandos
+ groupmod --new-name _mandos mandos
+ # Reload D-Bus daemon to be aware of the _mandos
+ # user & group
+ if [ -x /etc/init.d/dbus ]; then
+ invoke-rc.d dbus force-reload || :
+ fi
+ ;;
+ esac
+ fi
+ # Create new user and group
+ if ! getent passwd _mandos >/dev/null; then
+ adduser --system --force-badname --quiet \
+ --home /nonexistent --no-create-home --group \
+ --disabled-password --gecos "Mandos password system" \
+ _mandos
+ # Reload D-Bus daemon to be aware of the _mandos user &
+ # group
+ if [ -x /etc/init.d/dbus ]; then
+ invoke-rc.d dbus force-reload || :
+ fi
+ elif dpkg --compare-versions "$2" eq 1.7.4-1 \
+ || dpkg --compare-versions "$2" eq "1.7.4-1~bpo8+1"
+ then
+ start=no
+ if ! [ -f /var/lib/mandos/clients.pickle ]; then
+ invoke-rc.d mandos stop
+ start=yes
+ fi
+ chown _mandos:_mandos /var/lib/mandos/clients.pickle \
+ 2>/dev/null || :
+ if [ "$start" = yes ]; then
+ invoke-rc.d mandos start
+ fi
+ fi
+ if ! dpkg-statoverride --list "/var/lib/mandos" >/dev/null \
+ 2>&1; then
+ chown _mandos:_mandos /var/lib/mandos
+ chmod u=rwx,go= /var/lib/mandos
+ fi
+
+ if dpkg --compare-versions "$2" eq "1.8.0-1" \
+ || dpkg --compare-versions "$2" eq "1.8.0-1~bpo9+1"; then
+ if grep --quiet --regexp='^[[:space:]]*key_id[[:space:]]*=[[:space:]]*[Ee]3[Bb]0[Cc]44298[Ff][Cc]1[Cc]149[Aa][Ff][Bb][Ff]4[Cc]8996[Ff][Bb]92427[Aa][Ee]41[Ee]4649[Bb]934[Cc][Aa]495991[Bb]7852[Bb]855[[:space:]]*$' /etc/mandos/clients.conf; then
+ sed --in-place \
+ --expression='/^[[:space:]]*key_id[[:space:]]*=[[:space:]]*[Ee]3[Bb]0[Cc]44298[Ff][Cc]1[Cc]149[Aa][Ff][Bb][Ff]4[Cc]8996[Ff][Bb]92427[Aa][Ee]41[Ee]4649[Bb]934[Cc][Aa]495991[Bb]7852[Bb]855[[:space:]]*$/d' \
+ /etc/mandos/clients.conf
+ invoke-rc.d mandos restart
+ db_version 2.0
+ db_fset mandos/removed_bad_key_ids seen false
+ db_reset mandos/removed_bad_key_ids
+ db_input critical mandos/removed_bad_key_ids || true
+ db_go
+ db_stop
+ fi
+ fi
+
+ gnutls_version=$(dpkg-query --showformat='${Version}' \
+ --show libgnutls30 \
+ 2>/dev/null || :)
+ if [ -n "$gnutls_version" ] \
+ && dpkg --compare-versions $gnutls_version ge 3.6.6; then
+ db_version 2.0
+ db_input critical mandos/key_id || true
+ db_go
+ db_stop
+ fi
+ ;;
+
+ abort-upgrade|abort-deconfigure|abort-remove)
+ ;;
+
+ *)
+ echo "$0 called with unknown argument '$1'" 1>&2
+ exit 1
+ ;;
+esac
+
+# Avahi version 0.6.31-2 and older provides "avahi" (instead of
+# "avahi-daemon") in its /etc/init.d script header. To make
+# insserv(8) happy, we edit our /etc/init.d script header to contain
+# the correct string before the code added by dh_installinit calls
+# update.rc-d, which calls insserv.
+avahi_version="`dpkg-query --showformat='${Version}' --show avahi-daemon`"
+if dpkg --compare-versions "$avahi_version" le 0.6.31-2; then
+ sed --in-place --expression='/^### BEGIN INIT INFO$/,/^### END INIT INFO$/s/^\(# Required-\(Stop\|Start\):.*avahi\)-daemon\>/\1/g' /etc/init.d/mandos
+fi
+
+#DEBHELPER#
+
+exit 0
=== added file 'debian/mandos.prerm'
--- debian/mandos.prerm 1970-01-01 00:00:00 +0000
+++ debian/mandos.prerm 2015-07-12 01:57:54 +0000
@@ -0,0 +1,32 @@
+#!/bin/sh
+# prerm script for mandos
+#
+# see: dh_installdeb(1)
+
+set -e
+
+# summary of how this script can be called:
+# * 'remove'
+# * 'upgrade'
+# * 'failed-upgrade'
+# * 'remove' 'in-favour'
+# * 'deconfigure' 'in-favour'
+# 'removing'
+#
+# for details, see /usr/share/doc/packaging-manual/
+
+case "$1" in
+ remove|deconfigure)
+ invoke-rc.d mandos stop || :
+ ;;
+ upgrade|failed-upgrade)
+ ;;
+ *)
+ echo "prerm called with unknown argument '$1'" >&2
+ exit 0
+ ;;
+esac
+
+#DEBHELPER#
+
+exit 0
=== added file 'debian/mandos.templates'
--- debian/mandos.templates 1970-01-01 00:00:00 +0000
+++ debian/mandos.templates 2019-08-05 21:00:35 +0000
@@ -0,0 +1,29 @@
+Template: mandos/key_id
+Type: note
+_Description: New client option "key_id" is REQUIRED on server
+ A new "key_id" client option is REQUIRED in the clients.conf file,
+ otherwise the client most likely will not reboot unattended. This option:
+ .
+ key_id =
+ .
+ must be added in the file /etc/mandos/clients.conf, right before the
+ "fingerprint" option, for each Mandos client. You must edit that file and
+ add this option for all clients. To see the correct key ID for each
+ client, run this command (on each client):
+ .
+ mandos-keygen -F/dev/null|grep ^key_id
+ .
+ Note: the clients must all also be using GnuTLS 3.6.6 or later; the server
+ cannot serve passwords for both old and new clients!
+ .
+ Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP
+ keys as TLS session keys. A new TLS key pair will be generated on each
+ client and will be used as identification, but the key ID of the public
+ key needs to be added to this server, since this will now be used to
+ identify the client to the server.
+
+Template: mandos/removed_bad_key_ids
+Type: note
+_Description: Bad key IDs have been removed from clients.conf
+ Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been
+ removed from /etc/mandos/clients.conf
=== added directory 'debian/po'
=== added file 'debian/po/POTFILES.in'
--- debian/po/POTFILES.in 1970-01-01 00:00:00 +0000
+++ debian/po/POTFILES.in 2019-07-27 19:28:14 +0000
@@ -0,0 +1,2 @@
+[type: gettext/rfc822deb] mandos.templates
+[type: gettext/rfc822deb] mandos-client.templates
=== added file 'debian/po/de.po'
--- debian/po/de.po 1970-01-01 00:00:00 +0000
+++ debian/po/de.po 2019-08-16 19:28:16 +0000
@@ -0,0 +1,155 @@
+# German debconf translation of mandos.
+# This file is distributed under the same license as the mandos package.
+# Copyright (C) 2008-2019 Teddy Hogeborn and Björn Påhlsson
+# Copyright (C) of this file 2019 Chris Leick .
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: mandos 1.8.7-1\n"
+"Report-Msgid-Bugs-To: mandos@packages.debian.org\n"
+"POT-Creation-Date: 2019-08-05 22:57+0200\n"
+"PO-Revision-Date: 2019-08-10 12:06+0100\n"
+"Last-Translator: Chris Leick \n"
+"Language-Team: German \n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid "New client option \"key_id\" is REQUIRED on server"
+msgstr "Auf diesem Server ist die Client-Option »key_id« ERFORDERLICH"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the clients.conf file, "
+"otherwise the client most likely will not reboot unattended. This option:"
+msgstr ""
+"In der Datei clients.conf ist eine neue Client-Option »key_id« ERFORDERLICH, "
+"andernfalls werden die Clients höchstwahrscheinlich nicht unbeaufsichtigt neu "
+"starten. Die Option"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid " key_id = "
+msgstr " key_id = "
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"must be added in the file /etc/mandos/clients.conf, right before the "
+"\"fingerprint\" option, for each Mandos client. You must edit that file and "
+"add this option for all clients. To see the correct key ID for each client, "
+"run this command (on each client):"
+msgstr ""
+"muss der Datei /etc/mandos/clients.conf kurz vor der Option »fingerprint« "
+"auf jedem Mandos-Client hinzugefügt werden. Sie müssen diese Datei bearbeiten "
+"und diese Option auf allen Clients hinzufügen. Um die korrekte "
+"Schlüsselkennung für jeden Client anzusehen, führen Sie (auf jedem Client) "
+"diesen Befehl aus:"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid " mandos-keygen -F/dev/null|grep ^key_id"
+msgstr " mandos-keygen -F/dev/null|grep ^key_id"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server "
+"cannot serve passwords for both old and new clients!"
+msgstr ""
+"Hinweis: Die Clients müssen außerdem alle GnuTLS 3.6.6 oder neuer nutzen; der "
+"Server kann keine Passwörter für sowohl alte als auch neue Clients anbieten!"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP "
+"keys as TLS session keys. A new TLS key pair will be generated on each "
+"client and will be used as identification, but the key ID of the public key "
+"needs to be added to this server, since this will now be used to identify "
+"the client to the server."
+msgstr ""
+"Begründung: Mit GnuTLS 3.6.6 wurde erzwungen, dass Mandos die Benutzung von "
+"OpenPGP als TLS-Sitzungsschlüssel stoppt. Auf jedem Client wird ein neues "
+"TLS-Schlüsselpaar erzeugt und zur Identifizierung benutzt, aber der "
+"öffentliche Schlüssel muss auf diesem Server hinzugefügt werden, da dies nun "
+"zur Identifizierung des Clients auf dem Server verwendet wird."
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid "Bad key IDs have been removed from clients.conf"
+msgstr "Falsche Schlüsselkennungen wurden aus der clients.conf entfernt."
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid ""
+"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been "
+"removed from /etc/mandos/clients.conf"
+msgstr ""
+"Falsche Schlüsselkennungen, die durch einen Fehler im Mandos-Client 1.8.0 "
+"erzeugt wurden, wurden aus /etc/mandos/clients.conf entfernt."
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid "New client option \"${key_id}\" is REQUIRED on server"
+msgstr "Auf dem Server ist die neue Client-Option »${key_id}« ERFORDERLICH."
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the server's clients.conf "
+"file, otherwise this computer most likely will not reboot unattended. This "
+"option:"
+msgstr ""
+"In der Datei clients.conf des Servers ist eine neue Client-Option »key_id« "
+"ERFORDERLICH, andernfalls wird dieser Rechner höchstwahrscheinlich nicht "
+"unbeaufsichtigt neu starten. Die Option "
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid " ${key_id}"
+msgstr " ${key_id}"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"must be added (all on one line!) on the Mandos server host, in the file /etc/"
+"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos "
+"client. You must edit that file on that server and add this option."
+msgstr ""
+"muss (in einer einzigen Zeile!) der Datei /etc/mandos/clients.conf auf dem "
+"Mandos-Server kurz vor der Option »fingerprint« für diesen Mandos-Client "
+"hinzugefügt werden. Sie müssen diese Datei auf diesem Server bearbeiten und "
+"diese Option hinzufügen."
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS "
+"session keys. A new TLS key pair has been generated and will be used as "
+"identification, but the key ID of the public key needs to be added to the "
+"server, since this will now be used to identify the client to the server."
+msgstr ""
+"Mit GnuTLS 3.6.6 wurde erzwungen, dass Mandos die Benutzung von OpenPGP als "
+"TLS-Sitzungsschlüssel stoppt. Ein neues TLS-Schlüsselpaar wurde erzeugt und "
+"wird zur Identifizierung benutzt, aber die Schlüsselkennung des öffentlichen "
+"Schlüssels muss auf diesem Server hinzugefügt werden, da dies nun "
+"zur Identifizierung des Clients auf dem Server verwendet wird."
=== added file 'debian/po/en_US.po'
--- debian/po/en_US.po 1970-01-01 00:00:00 +0000
+++ debian/po/en_US.po 2019-08-05 21:00:35 +0000
@@ -0,0 +1,150 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the mandos package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: mandos\n"
+"Report-Msgid-Bugs-To: mandos@packages.debian.org\n"
+"POT-Creation-Date: 2019-08-05 22:57+0200\n"
+"PO-Revision-Date: 2019-08-05 22:59+0200\n"
+"Last-Translator: Teddy Hogeborn \n"
+"Language-Team: English\n"
+"Language: en_US\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid "New client option \"key_id\" is REQUIRED on server"
+msgstr "New client option “key_id” is REQUIRED on server"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the clients.conf file, "
+"otherwise the client most likely will not reboot unattended. This option:"
+msgstr ""
+"A new “key_id” client option is REQUIRED in the clients.conf file, otherwise "
+"the client most likely will not reboot unattended. This option:"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid " key_id = "
+msgstr " key_id = "
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"must be added in the file /etc/mandos/clients.conf, right before the "
+"\"fingerprint\" option, for each Mandos client. You must edit that file and "
+"add this option for all clients. To see the correct key ID for each client, "
+"run this command (on each client):"
+msgstr ""
+"must be added in the file /etc/mandos/clients.conf, right before the "
+"“fingerprint” option, for each Mandos client. You must edit that file and "
+"add this option for all clients. To see the correct key ID for each client, "
+"run this command (on each client):"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid " mandos-keygen -F/dev/null|grep ^key_id"
+msgstr " mandos-keygen -F/dev/null|grep ^key id"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server "
+"cannot serve passwords for both old and new clients!"
+msgstr ""
+"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server "
+"cannot serve passwords for both old and new clients!"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP "
+"keys as TLS session keys. A new TLS key pair will be generated on each "
+"client and will be used as identification, but the key ID of the public key "
+"needs to be added to this server, since this will now be used to identify "
+"the client to the server."
+msgstr ""
+"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP "
+"keys as TLS session keys. A new TLS key pair will be generated on each "
+"client and will be used as identification, but the key ID of the public key "
+"needs to be added to this server, since this will now be used to identify "
+"the client to the server. "
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid "Bad key IDs have been removed from clients.conf"
+msgstr "Bad key IDs have been removed from clients.conf"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid ""
+"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been "
+"removed from /etc/mandos/clients.conf"
+msgstr ""
+"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been "
+"removed from /etc/mandos/clients.conf"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid "New client option \"${key_id}\" is REQUIRED on server"
+msgstr "New client option “${key_id}” is REQUIRED on server"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the server's clients.conf "
+"file, otherwise this computer most likely will not reboot unattended. This "
+"option:"
+msgstr ""
+"A new “key_id” client option is REQUIRED in the server’s clients.conf file, "
+"otherwise this computer most likely will not reboot unattended. This option:"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid " ${key_id}"
+msgstr " ${key_id}"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"must be added (all on one line!) on the Mandos server host, in the file /etc/"
+"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos "
+"client. You must edit that file on that server and add this option."
+msgstr ""
+"must be added (all on one line!) on the Mandos server host, in the file /"
+"etc/ mandos/clients.conf, right before the “fingerprint” option for this "
+"Mandos client. You must edit that file on that server and add this option."
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS "
+"session keys. A new TLS key pair has been generated and will be used as "
+"identification, but the key ID of the public key needs to be added to the "
+"server, since this will now be used to identify the client to the server."
+msgstr ""
+"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS "
+"session keys. A new TLS key pair has been generated and will be used as "
+"identification, but the key ID of the public key needs to be added to the "
+"server, since this will now be used to identify the client to the server."
=== added file 'debian/po/fr.po'
--- debian/po/fr.po 1970-01-01 00:00:00 +0000
+++ debian/po/fr.po 2019-08-16 19:32:47 +0000
@@ -0,0 +1,156 @@
+# Translation of mandos debconf templates to French
+# Copyright (C) 2019, French l10n team
+# This file is distributed under the same license as the mandos package.
+# Grégoire Scano , 2019.
+msgid ""
+msgstr ""
+"Project-Id-Version: mandos\n"
+"Report-Msgid-Bugs-To: mandos@packages.debian.org\n"
+"POT-Creation-Date: 2019-07-27 21:06+0200\n"
+"PO-Revision-Date: 2019-08-11 15:58+0800\n"
+"Last-Translator: Grégoire Scano \n"
+"Language-Team: French \n"
+"Language: fr_FR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid "New client option \"key_id\" is REQUIRED on server"
+msgstr "La nouvelle option de client « key_id » est NÉCESSAIRE sur le serveur"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the clients.conf file, "
+"otherwise the client most likely will not reboot unattended. This option:"
+msgstr ""
+"Une nouvelle option de client « key_id » est NÉCESSAIRE dans le fichier "
+"clients.conf, autrement le client ne redémarrera probablement pas de lui-"
+"même. Cette option :"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid "key_id = "
+msgstr "key_id = "
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"must be added in the file /etc/mandos/clients.conf, right before the "
+"\"fingerprint\" option, for each Mandos client. You must edit that file and "
+"add this option for all clients. To see the correct key ID for each client, "
+"run this command (on each client):"
+msgstr ""
+"doit être ajoutée dans le fichier /etc/mandos/clients.conf, juste avant "
+"l'option « fingerprint », pour chaque client Mandos. Vous devez éditer ce "
+"fichier et ajouter cette option pour tous les clients. Pour voir "
+"l'identifiant de clef correct pour chaque client, exécutez la commande (sur "
+"chaque client) :"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid "mandos-keygen -F/dev/null|grep ^key_id"
+msgstr "mandos-keygen -F/dev/null|grep ^key_id"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server "
+"cannot serve passwords for both old and new clients!"
+msgstr ""
+"Note : les clients doivent également tous utiliser GnuTLS 3.6.6 ou "
+"ultérieur ; le serveur ne peut pas servir des mots de passe pour des clients "
+"anciens et récents en même temps !"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP "
+"keys as TLS session keys. A new TLS key pair will be generated on each "
+"client and will be used as identification, but the key ID of the public key "
+"needs to be added to this server, since this will now be used to identify "
+"the client to the server."
+msgstr ""
+"Explication : avec GnuTLS 3.6.6, Mandos a été contraint d'arrêter d'utiliser "
+"des clefs OpenPGP comme clefs de session TLS. Une nouvelle paire de clefs "
+"TLS sera générée pour chaque client et sera utilisée pour l'identification, "
+"mais l'identifiant de la clef publique doit être ajouté à ce serveur, "
+"puisqu'il sera utilisé pour identifier le client auprès du serveur."
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid "Bad key IDs have been removed from clients.conf"
+msgstr "Les identifiants de clef incorrects ont été supprimés de clients.conf"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid ""
+"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been "
+"removed from /etc/mandos/clients.conf"
+msgstr ""
+"Les identifiants de clef incorrects, créés par un bogue dans le client "
+"Mandos 1.8.0, ont été supprimés de /etc/mandos/clients.conf"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid "New client option \"${key_id}\" is REQUIRED on server"
+msgstr ""
+"La nouvelle option de client « ${key_id} » est NÉCESSAIRE sur le serveur"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the server's clients.conf "
+"file, otherwise this computer most likely will not reboot unattended. This "
+"option:"
+msgstr ""
+"Une nouvelle option de client « key_id » est NÉCESSAIRE dans le fichier "
+"clients.conf du serveur, autrement cette machine ne pourra pas redémarrer "
+"d'elle-même. Cette option :"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid "${key_id}"
+msgstr "${key_id}"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"must be added (all on one line!) on the Mandos server host, in the file /etc/"
+"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos "
+"client. You must edit that file on that server and add this option."
+msgstr ""
+"doit être ajoutée (tout sur une seule ligne !) sur le serveur Mandos hôte, "
+"dans le fichier /etc/mandos/clients.conf, juste avant l'option "
+"« fingerprint » de ce client Mandos. Vous devez éditer ce fichier sur ce "
+"serveur et ajouter cette option."
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS "
+"session keys. A new TLS key pair has been generated and will be used as "
+"identification, but the key ID of the public key needs to be added to the "
+"server, since this will now be used to identify the client to the server."
+msgstr ""
+"Avec GnuTLS 3.6.6, Mandos a été contraint d'arrêter d'utiliser des clefs "
+"OpenPGP comme clefs de session TLS. Une nouvelle paire de clefs TLS a été "
+"générée et sera utilisée pour l'identification, mais l'identifiant de la "
+"clef publique doit être ajouté au serveur, puisqu'il sera utilisé pour "
+"identifier le client auprès du serveur."
=== added file 'debian/po/pt.po'
--- debian/po/pt.po 1970-01-01 00:00:00 +0000
+++ debian/po/pt.po 2019-10-19 17:37:00 +0000
@@ -0,0 +1,158 @@
+# Translation of mandos debconf messages to European Portuguese
+# Copyright (C) 2019 THE mandos'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the mandos package.
+#
+# Américo Monteiro , 2019.
+msgid ""
+msgstr ""
+"Project-Id-Version: mandos 1.8.9-2\n"
+"Report-Msgid-Bugs-To: mandos@packages.debian.org\n"
+"POT-Creation-Date: 2019-08-05 22:57+0200\n"
+"PO-Revision-Date: 2019-10-18 18:45+0000\n"
+"Last-Translator: Américo Monteiro \n"
+"Language-Team: Portuguese <>\n"
+"Language: pt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Lokalize 2.0\n"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid "New client option \"key_id\" is REQUIRED on server"
+msgstr "Nova opção \"key_id\" de cliente é NECESSÁRIA no servidor"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the clients.conf file, "
+"otherwise the client most likely will not reboot unattended. This option:"
+msgstr ""
+"Uma nova opção de cliente \"key_id\" é NECESSÁRIA no ficheiro clients.conf, "
+"caso contrário o mais provável é o cliente não conseguir reinicicar sozinho. "
+"Esta opção:"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid " key_id = "
+msgstr " key_id = "
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"must be added in the file /etc/mandos/clients.conf, right before the "
+"\"fingerprint\" option, for each Mandos client. You must edit that file and "
+"add this option for all clients. To see the correct key ID for each client, "
+"run this command (on each client):"
+msgstr ""
+"tem de ser adicionada ao ficheiro /etc/mandos/clients.conf, logo antes "
+"da opção \"fingerprint\", para cada cliente Mandos. Você tem de editar esse "
+"ficheiro e adicionar esta opção para todos os clientes. Para ver a key ID "
+"para cada cliente, corra este comando (em cada cliente):"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid " mandos-keygen -F/dev/null|grep ^key_id"
+msgstr " mandos-keygen -F/dev/null|grep ^key_id"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server "
+"cannot serve passwords for both old and new clients!"
+msgstr ""
+"Note: os clientes têm de também usar GnuTLS 3.6.6 ou posterior; o servidor "
+"não consegue servir palavras passe para ambos clientes antigos e novos!"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP "
+"keys as TLS session keys. A new TLS key pair will be generated on each "
+"client and will be used as identification, but the key ID of the public key "
+"needs to be added to this server, since this will now be used to identify "
+"the client to the server."
+msgstr ""
+"Razão: Com GnuTLS 3.6.6, o Mandos foi forçado a parar de usar chaves OpenPGP "
+"como chaves de sessão TLS. Será gerado um novo par de chaves TLS em cada "
+"cliente e será usado como identificação, mas o ID de chave da chave pública "
+"precisa de ser adicionada a este servidor, pois esta irá agora ser usada "
+"para identificar o cliente no servidor."
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid "Bad key IDs have been removed from clients.conf"
+msgstr "IDs de chave errados foram removidos de clients.conf"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid ""
+"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been "
+"removed from /etc/mandos/clients.conf"
+msgstr ""
+"IDs de chave errados, que foram criados por um bug no cliente Mandos 1.8.0, "
+"foram removidos de /etc/mandos/clients.conf"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid "New client option \"${key_id}\" is REQUIRED on server"
+msgstr "Nova opção \"${key_id}\" de cliente é NECESSÁRIA no servidor"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the server's clients.conf "
+"file, otherwise this computer most likely will not reboot unattended. This "
+"option:"
+msgstr ""
+"Uma nova opção \"key_id\" de cliente é NECESSÁRIA no ficheiro clients.conf "
+"do servidor, caso contrário, é bem provável que este computador não consiga "
+"reiniciar sozinho. Esta opção:"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid " ${key_id}"
+msgstr " ${key_id}"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"must be added (all on one line!) on the Mandos server host, in the file /etc/"
+"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos "
+"client. You must edit that file on that server and add this option."
+msgstr ""
+"tem de ser adicionada (toda numa linha) na máquina servidor do Mandos, no "
+"ficheiro /etc/mandos/clients.conf, logo antes da opção \"fingerprint\" para "
+"este cliente Mandos. Você tem de editar esse ficheiro nesse servidor e "
+"adicionar esta opção."
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS "
+"session keys. A new TLS key pair has been generated and will be used as "
+"identification, but the key ID of the public key needs to be added to the "
+"server, since this will now be used to identify the client to the server."
+msgstr ""
+"Com GnuTLS 3.6.6, o Mandos foi forçado a parar de usar chaves OpenPGP "
+"como chaves de sessão TLS. Foi gerado um novo par de chaves TLS e será "
+"usado como identificação, mas o ID de chave da chave pública precisa de ser "
+"adicionada ao servidor, pois esta irá agora ser usada para identificar o "
+"cliente no servidor."
+
+
=== added file 'debian/po/sv.po'
--- debian/po/sv.po 1970-01-01 00:00:00 +0000
+++ debian/po/sv.po 2019-08-16 20:47:52 +0000
@@ -0,0 +1,156 @@
+# Translation of mandos debconf templates to Swedish
+# Copyright (C) 2019, Mandos Maintainers
+# This file is distributed under the same license as the mandos package.
+# Teddy Hogeborn , 2019.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: mandos\n"
+"Report-Msgid-Bugs-To: mandos@packages.debian.org\n"
+"POT-Creation-Date: 2019-08-05 22:57+0200\n"
+"PO-Revision-Date: 2019-08-16 22:45+0200\n"
+"Last-Translator: Teddy Hogeborn \n"
+"Language-Team: Swedish \n"
+"Language: sv\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid "New client option \"key_id\" is REQUIRED on server"
+msgstr "Ny klientinställning ”key_id” KRÄVS på servern"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the clients.conf file, "
+"otherwise the client most likely will not reboot unattended. This option:"
+msgstr ""
+"En ny klientinställning, ”key_id”, KRÄVS i filen clients.conf, annars\n"
+"kommer klienten antagligen inte att starta upp av sig själv. Denna\n"
+"inställning:"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid " key_id = "
+msgstr " key_id = "
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"must be added in the file /etc/mandos/clients.conf, right before the "
+"\"fingerprint\" option, for each Mandos client. You must edit that file and "
+"add this option for all clients. To see the correct key ID for each client, "
+"run this command (on each client):"
+msgstr ""
+"måste läggas till i filen /etc/mandos/clients.conf, precis ovanför\n"
+"inställningen ”fingerprint”, för varje Mandosklient. Du måste ändra i\n"
+"den filen och lägga till den inställningen för alla klienter. För att\n"
+"se det korrekta nyckel-IDt för varje klient, kör följande kommando (på\n"
+"varje klient):"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid " mandos-keygen -F/dev/null|grep ^key_id"
+msgstr " mandos-keygen -F/dev/null|grep ^key_id"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server "
+"cannot serve passwords for both old and new clients!"
+msgstr ""
+"Observera: Alla klienter måste också använda GnuTLS 3.6.6 eller nyare;\n"
+"servern kan inte ge lösenord till både nya och gamla klienter!"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP "
+"keys as TLS session keys. A new TLS key pair will be generated on each "
+"client and will be used as identification, but the key ID of the public key "
+"needs to be added to this server, since this will now be used to identify "
+"the client to the server."
+msgstr ""
+"Förklaring: Med GnuTLS 3.6.6 så har Mandos nödgats att sluta använda\n"
+"OpenPGP-nycklar som TLS-sessionsnycklar. Ett nytt TLS-nyckelpar\n"
+"kommer att genereras på varje klient och kommer att användas för\n"
+"identifiering, men nyckel-IDt för den publika nyckeln måste läggas\n"
+"till på denna server, då denna numera kommer att användas för att\n"
+"identifiera klienten för servern."
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid "Bad key IDs have been removed from clients.conf"
+msgstr "Dåliga nyckel-IDn har tagits bort från clients.conf"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid ""
+"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been "
+"removed from /etc/mandos/clients.conf"
+msgstr ""
+"Dåliga nyckel-IDn, som skapats av en bugg i Mandosklienten 1.8.0, har\n"
+"tagits bort från /etc/mandos/clients.conf"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid "New client option \"${key_id}\" is REQUIRED on server"
+msgstr "Ny klientinställning ”${key_id}” KRÄVS på servern"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the server's clients.conf "
+"file, otherwise this computer most likely will not reboot unattended. This "
+"option:"
+msgstr ""
+"En ny klientinställning, ”key_id”, KRÄVS i serverns clients.conf-fil,\n"
+"annars kommer denna dator antagligen inte att starta upp av sig själv.\n"
+"Denna inställning:"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid " ${key_id}"
+msgstr " ${key_id}"
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"must be added (all on one line!) on the Mandos server host, in the file /etc/"
+"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos "
+"client. You must edit that file on that server and add this option."
+msgstr ""
+"måste läggas till (allt på en rad!) på Mandosservervärddatorn, i filen\n"
+"/etc/mandos/clients.conf, precis ovanför inställningen ”fingerprint”,\n"
+"för denna Mandosklient. Du måste ändra i den filen och lägga till den\n"
+"inställningen."
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS "
+"session keys. A new TLS key pair has been generated and will be used as "
+"identification, but the key ID of the public key needs to be added to the "
+"server, since this will now be used to identify the client to the server."
+msgstr ""
+"Med GnuTLS 3.6.6 så har Mandos nödgats att sluta använda\n"
+"OpenPGP-nycklar som TLS-sessionsnycklar. Ett nytt TLS-nyckelpar har\n"
+"genererats och kommer att användas för identifiering, men nyckel-IDt\n"
+"för den publika nyckeln måste läggas till på servern, då detta numera\n"
+"kommer att användas för att identifiera klienten för servern."
=== added file 'debian/po/templates.pot'
--- debian/po/templates.pot 1970-01-01 00:00:00 +0000
+++ debian/po/templates.pot 2019-08-05 21:00:35 +0000
@@ -0,0 +1,127 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the mandos package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: mandos\n"
+"Report-Msgid-Bugs-To: mandos@packages.debian.org\n"
+"POT-Creation-Date: 2019-08-05 22:57+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid "New client option \"key_id\" is REQUIRED on server"
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the clients.conf file, "
+"otherwise the client most likely will not reboot unattended. This option:"
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid " key_id = "
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"must be added in the file /etc/mandos/clients.conf, right before the "
+"\"fingerprint\" option, for each Mandos client. You must edit that file and "
+"add this option for all clients. To see the correct key ID for each client, "
+"run this command (on each client):"
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid " mandos-keygen -F/dev/null|grep ^key_id"
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Note: the clients must all also be using GnuTLS 3.6.6 or later; the server "
+"cannot serve passwords for both old and new clients!"
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../mandos.templates:1001
+msgid ""
+"Rationale: With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP "
+"keys as TLS session keys. A new TLS key pair will be generated on each "
+"client and will be used as identification, but the key ID of the public key "
+"needs to be added to this server, since this will now be used to identify "
+"the client to the server."
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid "Bad key IDs have been removed from clients.conf"
+msgstr ""
+
+#. Type: note
+#. Description
+#: ../mandos.templates:2001
+msgid ""
+"Bad key IDs, which were created by a bug in Mandos client 1.8.0, have been "
+"removed from /etc/mandos/clients.conf"
+msgstr ""
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid "New client option \"${key_id}\" is REQUIRED on server"
+msgstr ""
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"A new \"key_id\" client option is REQUIRED in the server's clients.conf "
+"file, otherwise this computer most likely will not reboot unattended. This "
+"option:"
+msgstr ""
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid " ${key_id}"
+msgstr ""
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"must be added (all on one line!) on the Mandos server host, in the file /etc/"
+"mandos/clients.conf, right before the \"fingerprint\" option for this Mandos "
+"client. You must edit that file on that server and add this option."
+msgstr ""
+
+#. Type: note
+#. description
+#: ../mandos-client.templates:1001
+msgid ""
+"With GnuTLS 3.6.6, Mandos has been forced to stop using OpenPGP keys as TLS "
+"session keys. A new TLS key pair has been generated and will be used as "
+"identification, but the key ID of the public key needs to be added to the "
+"server, since this will now be used to identify the client to the server."
+msgstr ""
=== added file 'debian/rules'
--- debian/rules 1970-01-01 00:00:00 +0000
+++ debian/rules 2019-04-09 22:31:23 +0000
@@ -0,0 +1,61 @@
+#!/usr/bin/make -f
+
+ifeq (,$(filter noopt,$(DEB_BUILD_OPTIONS)))
+ MAKEFLAGS += OPTIMIZE=-O0
+endif
+
+ifneq (,$(filter parallel=%,$(DEB_BUILD_OPTIONS)))
+ NUMJOBS = $(patsubst parallel=%,%,$(filter parallel=%,$(DEB_BUILD_OPTIONS)))
+ MAKEFLAGS += -j$(NUMJOBS)
+endif
+
+%:
+ dh $@
+
+override_dh_auto_build-arch:
+ LC_ALL=en_US.utf8 dh_auto_build -- all doc
+
+override_dh_auto_build-indep:
+ LC_ALL=en_US.utf8 dh_auto_build -- doc
+
+override_dh_installinit-indep:
+ dh_installinit --onlyscripts \
+ --update-rcd-params="defaults 25 15"
+
+override_dh_auto_install-indep:
+ $(MAKE) DESTDIR=$(CURDIR)/debian/mandos install-server
+
+override_dh_auto_install-arch:
+ $(MAKE) DESTDIR=$(CURDIR)/debian/mandos-client \
+ install-client-nokey
+
+override_dh_fixperms-arch:
+ dh_fixperms --exclude etc/keys/mandos \
+ --exclude etc/mandos/plugins.d \
+ --exclude etc/mandos/plugin-helpers \
+ --exclude usr/lib/$(DEB_HOST_MULTIARCH)/mandos/plugins.d \
+ --exclude usr/lib/$(DEB_HOST_MULTIARCH)/mandos/plugin-helpers \
+ --exclude usr/share/doc/mandos-client/examples/network-hooks.d
+ chmod --recursive g-w -- \
+ "$(CURDIR)/debian/mandos-client/usr/share/doc/mandos-client/examples/network-hooks.d"
+
+override_dh_fixperms-indep:
+ dh_fixperms --exclude etc/mandos/clients.conf
+
+override_dh_auto_test-arch: ;
+
+#bpo## dpkg-shlibdeps sees the "libgnutls28-dev (>= 3.6.6) |
+#bpo## libgnutls28-dev (<< 3.6.0)," in the build-dependencies not as two
+#bpo## alternatives, but as an absolute dependency on libgnutls30 >= 3.6.6.
+#bpo## So we have to do this ugly hack to hide this build dependency if we
+#bpo## compiled with libgnutls30 << 3.6.0.
+#bpo#override_dh_shlibdeps-arch:
+#bpo# -gnutls_version=$$(dpkg-query --showformat='$${Version}' \
+#bpo# --show libgnutls30); \
+#bpo# dpkg --compare-versions $$gnutls_version lt 3.6.0 \
+#bpo# && { cp --archive debian/control debian/control.orig; sed --in-place --expression='s/libgnutls28-dev (>= 3\.6\.6) |//' debian/control; }
+#bpo# dh_shlibdeps
+#bpo# -gnutls_version=$$(dpkg-query --showformat='$${Version}' \
+#bpo# --show libgnutls30); \
+#bpo# dpkg --compare-versions $$gnutls_version lt 3.6.0 \
+#bpo# && mv debian/control.orig debian/control
=== added directory 'debian/source'
=== added file 'debian/source/format'
--- debian/source/format 1970-01-01 00:00:00 +0000
+++ debian/source/format 2010-10-02 17:41:05 +0000
@@ -0,0 +1,1 @@
+3.0 (quilt)
=== added file 'debian/source/lintian-overrides'
--- debian/source/lintian-overrides 1970-01-01 00:00:00 +0000
+++ debian/source/lintian-overrides 2019-08-05 21:03:31 +0000
@@ -0,0 +1,7 @@
+# We are both upstream and Debian maintainer for this package, so the
+# .asc signature can not exist until after the orig.tar.gz has been
+# built as part of the Debian package build.
+mandos source: orig-tarball-missing-upstream-signature mandos_*.tar.gz
+
+# We want to backport to stretch for as long as reasonably practical
+mandos source: package-uses-old-debhelper-compat-version 10
=== added file 'debian/source/local-options'
--- debian/source/local-options 1970-01-01 00:00:00 +0000
+++ debian/source/local-options 2011-10-08 21:13:46 +0000
@@ -0,0 +1,1 @@
+--single-debian-patch
=== added directory 'debian/tests'
=== added file 'debian/tests/control'
--- debian/tests/control 1970-01-01 00:00:00 +0000
+++ debian/tests/control 2019-09-04 05:31:20 +0000
@@ -0,0 +1,33 @@
+Test-Command: /usr/sbin/mandos --check
+Restrictions: superficial, allow-stderr
+Features: test-name=mandos-check
+Depends: mandos
+
+Test-Command: /usr/sbin/mandos-ctl --check --verbose
+Restrictions: allow-stderr
+Features: test-name=mandos-ctl
+Depends: mandos
+
+Test-Command: /usr/sbin/mandos-keygen --version
+Restrictions: superficial
+Features: test-name=mandos-keygen-version
+Depends: mandos-client
+
+Test-Command: /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null)/mandos/plugin-runner --version
+Restrictions: needs-root, superficial
+Features: test-name=plugin-runner-version
+Depends: mandos-client
+
+Test-Command: /usr/lib/$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null)/mandos/plugin-helpers/mandos-client-iprouteadddel --version
+Restrictions: needs-root, superficial
+Features: test-name=mandos-client-iprouteadddel-version
+Depends: mandos-client
+
+Test-Command: /usr/lib/dracut/modules.d/90mandos/password-agent --test --verbose
+Features: test-name=password-agent
+Depends: mandos-client
+
+Test-Command: /usr/lib/dracut/modules.d/90mandos/password-agent --test --verbose -p /task-creators/start_mandos_client/suid
+Restrictions: needs-root
+Features: test-name=password-agent-suid
+Depends: mandos-client
=== added directory 'debian/upstream'
=== added file 'debian/upstream/metadata'
--- debian/upstream/metadata 1970-01-01 00:00:00 +0000
+++ debian/upstream/metadata 2019-08-04 12:39:39 +0000
@@ -0,0 +1,13 @@
+# -*- yaml -*-
+---
+Bug-Submit: mailto:mandos-dev@recompile.se
+Changelog: https://bzr.recompile.se/loggerhead/mandos/trunk/view/head:/NEWS
+Contact: mandos@recompile.se
+Documentation: https://www.recompile.se/mandos/man/intro.8mandos
+FAQ: https://www.recompile.se/mandos/man/intro.8mandos#faq
+Name: Mandos
+Other-References: https://www.recompile.se/mandos
+Registration: https://mail.recompile.se/cgi-bin/mailman/listinfo/mandos-dev
+Repository: https://ftp.recompile.se/pub/mandos/trunk
+Repository-Browse: https://bzr.recompile.se/loggerhead/mandos/trunk/files
+Security-Contact: mandos@recompile.se
=== added file 'debian/upstream/signing-key.asc'
--- debian/upstream/signing-key.asc 1970-01-01 00:00:00 +0000
+++ debian/upstream/signing-key.asc 2014-03-28 22:32:21 +0000
@@ -0,0 +1,52 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.12 (GNU/Linux)
+
+mQINBFJQOFYBEACoWsEGlOxVWFUAxOxdd3GDLaqEKkKihJwLp102Ks7JKMd9friR
+7+OZuo3U0gdqLU9q1jPJn36J1QbaUTOvcaKtZp+QpUoYJ2OaGtlOY5ML8LSoC0rZ
+MIzGYTtvriwpU/YplLNGPl/90KsB2VqjrY1l1he5M8zziWDlPdJxwg8GFvmPWoif
+6oo+1iCswL5IdQ6c5MVO53zYu0cgyUSazLsVD5Xzy59lefgtaDydahJpPycf5aEQ
+DAoC9fZt2mgG3FLIUCZdXIhZdOJGCMdjLThBnJXYgGbG4rbGLNlI4W/uA5aqa4ME
+WYSAcCyX3ucKY/LkXRtC+z5s05e7tZ3Z+uAJy1eDsbhDXgZERye7a/zPWx1tAlzQ
+E80Oltjh1uXWjQORyx99a0jK87zjm49YjhYw1ZN6Z0HfSaws4Yj2QOzp9t4B3l7f
+DIUYoWBfHW7mseQeQ+t3TwQU5gjFCNu7oDeATqi5A5MksXN0+BcksterbGRBhEyp
+CybIEyrZE033jIs407Ool4Kv10cnjc8oy609BXex/dxwcvVr2vQHle4NPUZd+Xhg
+zC+9Z4jFwE0M/EPvtyieA/DWQse+TZ5itDGMYDub/GJfv1U61ANOgPIbTEF7iSa9
+5nWmq7zyUy/txmABka842Kt0Vp6ayoKcF8EIXCaDrVfPnXj+JlKf3c2u6wARAQAB
+tCxNYW5kb3MgTWFpbnRhaW5lciBUZWFtIDxtYW5kb3NAcmVjb21waWxlLnNlPokC
+PQQTAQgAJwUCUlA4VgIbAwUJCWYBgAULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAK
+CRByIylzyjTCxETXEACi56jCV9lJNSBbTp0Iet4X/i7Mx0Z8UkFFa3l7o0i4jFQj
+CIBrWECDlcxqZziii2dgh7L0ma93vB3rfjfCWeYLcEQw43MFBsd4dHuobrLXTcqU
+7n0Zmc8BsXwk5B25CnEYgvlbWX9BCYtxHGRcZzQrqOFjCMKatq0EIIVuWaz5yuCU
+V2rEgnr+veTd/rBOE9ez6Ju6xH11Teob5G7pMM0YPKHtZG/J3rvWPw4BDM6Tc60B
+G0sTDZNgkrGWxuB8YaLIwVWzliQK/17Jv/0alajyA3cWLWMkcK9Yhi/einzdMRoD
+IjnbtoHcSC9g6i0VGelwnMpHlFTwXriXEBSttULarK3iKE4tOv9nxMAEwicqlw14
+X96PPgz6ogtJG7FiwZfy1CQ81Uby+YuIhHZx3ZEyR1TFq70e98EgCuvjMZWMIhSy
+gB5Vssfq7c2lXQjltV3ujhK1PD+7/iHlL7t4QRxPDN8fbMS2VPfAdtnWS1K48d5J
+D/jP1LrGWS81HIaX8GFVLVw+jSQEu9cn3TFiZxK/4MMsITmlouJdtZWmQ/otMSMl
+wiCCZp3dGpRXMmaqR1N8V0nMKshM8mci7bD92ubd/t6cR/G6l+VIp45WyFONvtde
+F3ccfmfrKJuroMDfHxPxMf58EroAuWwzJKCPRH4JmwDvSSQoIIAGNL1lOKp6wrkC
+DQRSUDhWARAAzN7pbpAu7XLNPODotV/N+JaCFvNAIqTcr9PrbhxiKFCDs9/IExwP
+sGENL9GZd1DfoGEgxQ8j3l8VGw9VSeUoN7uMY2NwwbXTilAFkn/S8xnr2zQDRZ+n
+EeFSq9MMFxj4Kt1TqVYDbO8vmFfOT3gRCRHeJ+pn4yJeSPau/ndNrmbQ1/Z6vaUG
+yfo931ottx7SXZwkHA6jJVFT9rbHTyx9tzOqMKDJiMrx8qKaHpE9B45oHNR0WJJJ
+75zoDVuOZ6wAxXZuqBFu2lKPqDTZeawfzcu5qplrm1RPgSOjz6w1A41HBLqGe+v9
+7Twx5wfNMgnKC0V2wUe0xR6hQHQlyZoCwcGyrasiu+v/joZ1p66SSWKjs+LGjoe4
+Lwh6VjZU2x+irVBjcgIoRWf3k4JAef0nGYsm0cFjAnwXac+/CYxVt+7Y4+HG8wPB
+oUNZkW+bvdHQouxEClxnccIEgX/AkriJTYDQ4q3tkl2HVE/51R4pdQVLer2a5ov+
+Jwk44DdqzYstMsvqu+iD48hXzADg6HFvofkpct15h463pMaJf99uVVM9ZDNQ6B34
+l3tPX9ZDkIDl6n9dE2Jkaxx1yNVhPXpeMf4EzL+CEdUErVStB66lUkw+tNkuZyVT
+ZTQel9h0196+CNSiqAaL8+ZZdbjKKfzlcB4Qnd897XzMfsFQ7mzJ9QsAEQEAAYkC
+JQQYAQgADwUCUlA4VgIbDAUJCWYBgAAKCRByIylzyjTCxFmlEACBTOg5NqX63d8D
+mwk4smlFPppQBIduxZaMG9HsLcPi3VKTG9Zg6WI6rEdr/4MnoINsudLsEbrQLgRH
+2q1Zs+HqIIP5H2/sYHmswyokYB10zKB6gNUUg/GSlcAcrelsHVKx5B8kccWGT5gk
+Wo/X0BGMUTOvQ6lJ6YNo1idcQ2ZjsyfZoz3G8JS7/EXN//jAZf+017yj8WsAS7hw
+JRFMy7VET4g00JcBoNOAMP7PkozimZ2OwwsggJSYWkR1RaU2tKR1VmDF8R6UxuEd
+BJzwFmz+wNC1Kq+FoSaRNsrKEmzLnfV9unDnF2z7Lc4LqOysXdzOk9zTBPur0gd2
+Lh5H/g5rTAMQBARqXfvIwiTtrBGgil8JW8e4Bc0LQUuHAE7x9gMRil+OtkQrCRk9
+0LWXVS+K0tvvruE4EDtCGiS5046+BEI3aYsp4hNzjHADq0TJeCYjNg9kY0CjxcEq
+cfuMoUbQ0MkARGuBbykCdlylfTrkxrj/dPhr49lctY3H+Pj6F4fMDM4TP6UTGA8k
+993RRNYhkDWSxIp6G7RJpBZobHN+eHQ3r8A4tWdYb4Fvd2lvwEDjUFT9uD6WAff4
+8A1hM2uSy91UYBOPrIjqYdRFKJc9rThYdXH2T6SiRMYtZMrEKhqPffB/i9mqVBlD
+6vKRsaQikZujRdP9Dkf0mLmJ7LANWw==
+=9Noe
+-----END PGP PUBLIC KEY BLOCK-----
=== added file 'debian/watch'
--- debian/watch 1970-01-01 00:00:00 +0000
+++ debian/watch 2019-02-11 05:15:24 +0000
@@ -0,0 +1,3 @@
+version=4
+opts=pgpmode=auto \
+ https://ftp.recompile.se/pub/@PACKAGE@/@PACKAGE@@ANY_VERSION@\.orig@ARCHIVE_EXT@
=== added file 'default-mandos'
--- default-mandos 1970-01-01 00:00:00 +0000
+++ default-mandos 2008-09-17 00:34:09 +0000
@@ -0,0 +1,7 @@
+# Directory where configuration files are located. Default is
+# "/etc/mandos".
+#
+#CONFIGDIR=/etc/mandos
+
+# Additional options that are passed to the Daemon.
+DAEMON_ARGS=""
=== added directory 'dracut-module'
=== added file 'dracut-module/ask-password-mandos.path'
--- dracut-module/ask-password-mandos.path 1970-01-01 00:00:00 +0000
+++ dracut-module/ask-password-mandos.path 2019-07-27 10:11:45 +0000
@@ -0,0 +1,47 @@
+# -*- systemd -*-
+#
+# Copyright © 2019 Teddy Hogeborn
+# Copyright © 2019 Björn Påhlsson
+#
+# This file is part of Mandos.
+#
+# Mandos 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.
+#
+# Mandos 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 Mandos. If not, see .
+#
+# Contact the authors at .
+#
+# This systemd.path(5) unit will wait until there are any password
+# questions present, represented by files named "ask.*" in the
+# /run/systemd/ask-password directory, and then start the
+# "ask-password-mandos.service" systemd.service(5) unit.
+
+# This file should be installed in the root file system as
+# "/usr/lib/dracut/modules.d/90mandos/ask-password-mandos.path" and
+# will be installed in the initramfs image file as
+# "/lib/systemd/system/ask-password-mandos.path", and symlinked to
+# "/lib/systemd/system//sysinit.target.wants/ask-password-mandos.path"
+# by dracut when dracut creates the initramfs image file.
+
+[Unit]
+Description=Forward Password Requests to remote Mandos server
+Documentation=man:intro(8mandos) man:password-agent(8mandos) man:mandos-client(8mandos)
+DefaultDependencies=no
+Conflicts=shutdown.target
+Before=basic.target shutdown.target
+ConditionKernelCommandLine=!mandos=off
+ConditionFileIsExecutable=/lib/mandos/password-agent
+ConditionPathIsMountPoint=!/sysroot
+
+[Path]
+PathExistsGlob=/run/systemd/ask-password/ask.*
+MakeDirectory=yes
=== added file 'dracut-module/ask-password-mandos.service'
--- dracut-module/ask-password-mandos.service 1970-01-01 00:00:00 +0000
+++ dracut-module/ask-password-mandos.service 2019-07-27 10:11:45 +0000
@@ -0,0 +1,51 @@
+# -*- systemd -*-
+#
+# Copyright © 2019 Teddy Hogeborn
+# Copyright © 2019 Björn Påhlsson
+#
+# This file is part of Mandos.
+#
+# Mandos 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.
+#
+# Mandos 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 Mandos. If not, see .
+#
+# Contact the authors at .
+#
+# This systemd.service(5) unit file will start the Mandos
+# password-agent(8mandos) program, which will in turn run
+# mandos-client(8mandos) to get a password and send the password to
+# any and all active password questions using the systemd “Password
+# Agent” mechanism.
+
+# This file should be installed in the root file system as
+# "/usr/lib/dracut/modules.d/90mandos/ask-password-mandos.service" and
+# will be installed in the initramfs image file as
+# "/lib/systemd/system/ask-password-mandos.service" by dracut when
+# dracut creates the initramfs image file.
+
+[Unit]
+Description=Forward Password Requests to remote Mandos server
+Documentation=man:intro(8mandos) man:password-agent(8mandos) man:mandos-client(8mandos)
+DefaultDependencies=no
+Conflicts=shutdown.target
+Before=shutdown.target
+ConditionKernelCommandLine=!mandos=off
+ConditionFileIsExecutable=/lib/mandos/password-agent
+ConditionFileIsExecutable=/lib/mandos/mandos-client
+ConditionFileNotEmpty=/etc/mandos/keys/pubkey.txt
+ConditionFileNotEmpty=/etc/mandos/keys/seckey.txt
+ConditionFileNotEmpty=/etc/mandos/keys/tls-pubkey.pem
+ConditionFileNotEmpty=/etc/mandos/keys/tls-privkey.pem
+ConditionPathIsMountPoint=!/sysroot
+
+[Service]
+ExecStart=/lib/mandos/password-agent -- /lib/mandos/mandos-client --pubkey=/etc/mandos/keys/pubkey.txt --seckey=/etc/mandos/keys/seckey.txt --tls-pubkey=/etc/mandos/keys/tls-pubkey.pem --tls-privkey=/etc/mandos/keys/tls-privkey.pem
=== added file 'dracut-module/cmdline-mandos.sh'
--- dracut-module/cmdline-mandos.sh 1970-01-01 00:00:00 +0000
+++ dracut-module/cmdline-mandos.sh 2019-07-27 10:11:45 +0000
@@ -0,0 +1,74 @@
+#!/bin/sh
+#
+# This file should be present in the root file system directory
+# /usr/lib/dracut/modules.d/90mandos. When dracut creates the
+# initramfs image, dracut will run the "module-setup.sh" file in the
+# same directory, which (when *not* using the "systemd" dracut module)
+# will copy this file ("cmdline-mandos.sh") into the initramfs as
+# "/lib/dracut/hooks/cmdline/20-cmdline-mandos.sh".
+#
+# Despite the above #!/bin/sh line and the executable flag, this file
+# is not executed; this file is sourced by the /init script in the
+# initramfs image created by dracut.
+
+if getargbool 1 mandos && [ -e /lib/dracut-crypt-lib.sh ]; then
+ cat >> /lib/dracut-crypt-lib.sh <<- "EOF"
+ ask_for_password(){
+ local cmd; local prompt; local tries=3
+ local ply_cmd; local ply_prompt; local ply_tries=3
+ local tty_cmd; local tty_prompt; local tty_tries=3
+ local ret
+
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ --cmd) ply_cmd="$2"; tty_cmd="$2"; shift;;
+ --ply-cmd) ply_cmd="$2"; shift;;
+ --tty-cmd) tty_cmd="$2"; shift;;
+ --prompt) ply_prompt="$2"; tty_prompt="$2"; shift;;
+ --ply-prompt) ply_prompt="$2"; shift;;
+ --tty-prompt) tty_prompt="$2"; shift;;
+ --tries) ply_tries="$2"; tty_tries="$2"; shift;;
+ --ply-tries) ply_tries="$2"; shift;;
+ --tty-tries) tty_tries="$2"; shift;;
+ --tty-echo-off) tty_echo_off=yes;;
+ -*) :;;
+ esac
+ shift
+ done
+ if [ -z "$ply_cmd" ]; then
+ ply_cmd="$tty_cmd"
+ fi
+ # Extract device and luksname from $ply_cmd
+ set -- $ply_cmd
+ shift
+ for arg in "$@"; do
+ case "$arg" in
+ -*) :;;
+ *)
+ if [ -z "$device" ]; then
+ device="$arg"
+ else
+ luksname="$arg"
+ break
+ fi
+ ;;
+ esac
+ done
+ { flock -s 9;
+ if [ -z "$ply_prompt" ]; then
+ if [ -z "$tty_prompt" ]; then
+ CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd
+ else
+ CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --options-for=password-prompt:--prompt="${tty_prompt}" --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd
+ fi
+ else
+ if [ -z "$tty_prompt" ]; then
+ CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --options-for=plymouth:--prompt="${ply_prompt}" --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd
+ else
+ CRYPTTAB_SOURCE="$device" cryptsource="$device" CRYPTTAB_NAME="$luksname" crypttarget="$luksname" /lib/mandos/plugin-runner --options-for=password-prompt:--prompt="${tty_prompt}" --options-for=plymouth:--prompt="${ply_prompt}" --config-file=/etc/mandos/plugin-runner.conf | $ply_cmd
+ fi
+ fi
+ } 9>/.console_lock
+ }
+ EOF
+fi
=== added file 'dracut-module/module-setup.sh'
--- dracut-module/module-setup.sh 1970-01-01 00:00:00 +0000
+++ dracut-module/module-setup.sh 2019-07-27 10:11:45 +0000
@@ -0,0 +1,253 @@
+#!/bin/sh
+#
+# This file should be present in the root file system directory
+# /usr/lib/dracut/modules.d/90mandos. When dracut creates the
+# initramfs image, dracut will source this file and run the shell
+# functions defined in this file: "install", "check", "depends",
+# "cmdline", and "installkernel".
+#
+# Despite the above #!/bin/sh line and the executable flag, this file
+# is not executed; this file is sourced by dracut when creating the
+# initramfs image file.
+
+mandos_libdir(){
+ for dir in /usr/lib \
+ "/usr/lib/`dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null`" \
+ "`rpm --eval='%{_libdir}' 2>/dev/null`" /usr/local/lib; do
+ if [ -d "$dir"/mandos ]; then
+ echo "$dir"/mandos
+ return
+ fi
+ done
+ # Mandos not found
+ return 1
+}
+
+mandos_keydir(){
+ for dir in /etc/keys/mandos /etc/mandos/keys; do
+ if [ -d "$dir" ]; then
+ echo "$dir"
+ return
+ fi
+ done
+ # Mandos key directory not found
+ return 1
+}
+
+check(){
+ if [ "${hostonly:-no}" = "no" ]; then
+ dwarning "Mandos: Dracut not in hostonly mode"
+ return 1
+ fi
+
+ local libdir=`mandos_libdir`
+ if [ -z "$libdir" ]; then
+ dwarning "Mandos lib directory not found"
+ return 1
+ fi
+
+ local keydir=`mandos_keydir`
+ if [ -z "$keydir" ]; then
+ dwarning "Mandos key directory not found"
+ return 1
+ fi
+}
+
+install(){
+ chmod go+w,+t "$initdir"/tmp
+ local libdir=`mandos_libdir`
+ local keydir=`mandos_keydir`
+ set `{ getent passwd _mandos \
+ || getent passwd nobody \
+ || echo ::65534:65534:::; } \
+ | cut --delimiter=: --fields=3,4 --only-delimited \
+ --output-delimiter=" "`
+ local mandos_user="$1"
+ local mandos_group="$2"
+ inst "${libdir}" /lib/mandos
+ if dracut_module_included "systemd"; then
+ plugindir=/lib/mandos
+ inst "${libdir}/plugins.d/mandos-client" \
+ "${plugindir}/mandos-client"
+ chmod u-s "${initdir}/${plugindir}/mandos-client"
+ inst "${moddir}/ask-password-mandos.service" \
+ "${systemdsystemunitdir}/ask-password-mandos.service"
+ if [ ${mandos_user} != 65534 ]; then
+ sed --in-place \
+ --expression="s,^ExecStart=/lib/mandos/password-agent ,&--user=${mandos_user} ," \
+ "${initdir}/${systemdsystemunitdir}/ask-password-mandos.service"
+ fi
+ if [ ${mandos_group} != 65534 ]; then
+ sed --in-place \
+ --expression="s,^ExecStart=/lib/mandos/password-agent ,&--group=${mandos_group} ," \
+ "${initdir}/${systemdsystemunitdir}/ask-password-mandos.service"
+ fi
+ else
+ inst_hook cmdline 20 "$moddir"/cmdline-mandos.sh
+ plugindir=/lib/mandos/plugins.d
+ inst "${libdir}/plugin-runner" /lib/mandos/plugin-runner
+ inst /etc/mandos/plugin-runner.conf
+ sed --in-place \
+ --expression='1i--options-for=mandos-client:--pubkey=/etc/mandos/keys/pubkey.txt,--seckey=/etc/mandos/keys/seckey.txt,--tls-pubkey=/etc/mandos/keys/tls-pubkey.pem,--tls-privkey=/etc/mandos/keys/tls-privkey.pem' \
+ "${initdir}/etc/mandos/plugin-runner.conf"
+ if [ ${mandos_user} != 65534 ]; then
+ sed --in-place --expression="1i--userid=${mandos_user}" \
+ "${initdir}/etc/mandos/plugin-runner.conf"
+ fi
+ if [ ${mandos_group} != 65534 ]; then
+ sed --in-place \
+ --expression="1i--groupid=${mandos_group}" \
+ "${initdir}/etc/mandos/plugin-runner.conf"
+ fi
+ inst "${libdir}/plugins.d" "$plugindir"
+ chown ${mandos_user}:${mandos_group} "${initdir}/${plugindir}"
+ # Copy the packaged plugins
+ for file in "$libdir"/plugins.d/*; do
+ base="`basename \"$file\"`"
+ # Is this plugin overridden?
+ if [ -e "/etc/mandos/plugins.d/$base" ]; then
+ continue
+ fi
+ case "$base" in
+ *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert)
+ : ;;
+ "*") dwarning "Mandos client plugin directory is empty." >&2 ;;
+ askpass-fifo) : ;; # Ignore packaged for dracut
+ *) inst "${file}" "${plugindir}/${base}" ;;
+ esac
+ done
+ # Copy any user-supplied plugins
+ for file in /etc/mandos/plugins.d/*; do
+ base="`basename \"$file\"`"
+ case "$base" in
+ *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert)
+ : ;;
+ "*") : ;;
+ *) inst "$file" "${plugindir}/${base}" ;;
+ esac
+ done
+ # Copy any user-supplied plugin helpers
+ for file in /etc/mandos/plugin-helpers/*; do
+ base="`basename \"$file\"`"
+ case "$base" in
+ *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert)
+ : ;;
+ "*") : ;;
+ *) inst "$file" "/lib/mandos/plugin-helpers/$base";;
+ esac
+ done
+ fi
+ # Copy network hooks
+ for hook in /etc/mandos/network-hooks.d/*; do
+ basename=`basename "$hook"`
+ case "$basename" in
+ "*") continue ;;
+ *[!A-Za-z0-9_.-]*) continue ;;
+ *) test -d "$hook" || inst "$hook" "/lib/mandos/network-hooks.d/$basename" ;;
+ esac
+ if [ -x "$hook" ]; then
+ # Copy any files needed by the network hook
+ MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=files \
+ VERBOSITY=0 "$hook" files | while read file target; do
+ if [ ! -e "${file}" ]; then
+ dwarning "WARNING: file ${file} not found, requested by Mandos network hook '${basename}'" >&2
+ fi
+ if [ -z "${target}" ]; then
+ inst "$file"
+ else
+ inst "$file" "$target"
+ fi
+ done
+ fi
+ done
+ # Copy the packaged plugin helpers
+ for file in "$libdir"/plugin-helpers/*; do
+ base="`basename \"$file\"`"
+ # Is this plugin overridden?
+ if [ -e "/etc/mandos/plugin-helpers/$base" ]; then
+ continue
+ fi
+ case "$base" in
+ *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert)
+ : ;;
+ "*") : ;;
+ *) inst "$file" "/lib/mandos/plugin-helpers/$base";;
+ esac
+ done
+ local gpg=/usr/bin/gpg
+ if [ -e /usr/bin/gpgconf ]; then
+ inst /usr/bin/gpgconf
+ gpg="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg:[^:]*://p'`"
+ gpgagent="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg-agent:[^:]*://p'`"
+ # Newer versions of GnuPG 2 requires the gpg-agent binary
+ if [ -e "$gpgagent" ]; then
+ inst "$gpgagent"
+ fi
+ fi
+ inst "$gpg"
+ if dracut_module_included "systemd"; then
+ inst "${moddir}/password-agent" /lib/mandos/password-agent
+ inst "${moddir}/ask-password-mandos.path" \
+ "${systemdsystemunitdir}/ask-password-mandos.path"
+ ln_r "${systemdsystemunitdir}/ask-password-mandos.path" \
+ "${systemdsystemunitdir}/sysinit.target.wants/ask-password-mandos.path"
+ fi
+ # Key files
+ for file in "$keydir"/*; do
+ if [ -d "$file" ]; then
+ continue
+ fi
+ case "$file" in
+ *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert)
+ : ;;
+ "*") : ;;
+ *)
+ inst "$file" "/etc/mandos/keys/`basename \"$file\"`"
+ chown ${mandos_user}:${mandos_group} \
+ "${initdir}/etc/mandos/keys/`basename \"$file\"`"
+ if [ `basename "$file"` = dhparams.pem ]; then
+ # Use Diffie-Hellman parameters file
+ if dracut_module_included "systemd"; then
+ sed --in-place \
+ --expression='/^ExecStart/s/$/ --dh-params=\/etc\/mandos\/keys\/dhparams.pem/' \
+ "${initdir}/${systemdsystemunitdir}/ask-password-mandos.service"
+ else
+ sed --in-place \
+ --expression="1i--options-for=mandos-client:--dh-params=/etc/mandos/keys/dhparams.pem" \
+ "${initdir}/etc/mandos/plugin-runner.conf"
+ fi
+ fi
+ ;;
+ esac
+ done
+}
+
+installkernel(){
+ instmods =drivers/net
+ hostonly='' instmods ipv6
+ # Copy any kernel modules needed by network hooks
+ for hook in /etc/mandos/network-hooks.d/*; do
+ basename=`basename "$hook"`
+ case "$basename" in
+ "*") continue ;;
+ *[!A-Za-z0-9_.-]*) continue ;;
+ esac
+ if [ -x "$hook" ]; then
+ # Copy and load any modules needed by the network hook
+ MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=modules \
+ VERBOSITY=0 "$hook" modules | while read module; do
+ if [ -z "${target}" ]; then
+ instmods "$module"
+ fi
+ done
+ fi
+ done
+}
+
+depends(){
+ echo crypt
+}
+
+cmdline(){
+ :
+}
=== added file 'dracut-module/password-agent.c'
--- dracut-module/password-agent.c 1970-01-01 00:00:00 +0000
+++ dracut-module/password-agent.c 2019-10-20 01:48:38 +0000
@@ -0,0 +1,8112 @@
+/* -*- mode: c; coding: utf-8; after-save-hook: (lambda () (let* ((find-build-directory (lambda (try-directory &optional base-directory) (let ((base-directory (or base-directory try-directory))) (cond ((equal try-directory "/") base-directory) ((file-readable-p (concat (file-name-as-directory try-directory) "Makefile")) try-directory) ((funcall find-build-directory (directory-file-name (file-name-directory try-directory)) base-directory)))))) (build-directory (funcall find-build-directory (buffer-file-name))) (local-build-directory (if (fboundp 'file-local-name) (file-local-name build-directory) (or (file-remote-p build-directory 'localname) build-directory))) (command (file-relative-name (file-name-sans-extension (buffer-file-name)) build-directory))) (pcase (progn (if (get-buffer "*Test*") (kill-buffer "*Test*")) (process-file-shell-command (let ((qbdir (shell-quote-argument local-build-directory)) (qcmd (shell-quote-argument command))) (format "cd %s && CFLAGS=-Werror make --silent %s && %s --test --verbose" qbdir qcmd qcmd)) nil "*Test*")) (0 (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w)))) (_ (with-current-buffer "*Test*" (compilation-mode) (cd-absolute build-directory)) (display-buffer "*Test*" '(display-buffer-in-side-window)))))); -*- */
+/*
+ * Mandos password agent - Simple password agent to run Mandos client
+ *
+ * Copyright © 2019 Teddy Hogeborn
+ * Copyright © 2019 Björn Påhlsson
+ *
+ * This file is part of Mandos.
+ *
+ * Mandos 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.
+ *
+ * Mandos 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 Mandos. If not, see .
+ *
+ * Contact the authors at .
+ */
+
+#define _GNU_SOURCE
+#include /* uintmax_t, PRIuMAX, PRIdMAX,
+ intmax_t, uint32_t, SCNx32,
+ SCNuMAX, SCNxMAX */
+#include /* size_t */
+#include /* pid_t, uid_t, gid_t, getuid(),
+ getpid() */
+#include /* bool, true, false */
+#include /* struct sigaction, sigset_t,
+ sigemptyset(), sigaddset(),
+ SIGCHLD, pthread_sigmask(),
+ SIG_BLOCK, SIG_SETMASK, SA_RESTART,
+ SA_NOCLDSTOP, sigfillset(), kill(),
+ SIGTERM, sigdelset(), SIGKILL,
+ NSIG, sigismember(), SA_ONSTACK,
+ SIG_DFL, SIG_IGN, SIGINT, SIGQUIT,
+ SIGHUP, SIGSTOP, SIG_UNBLOCK */
+#include /* EXIT_SUCCESS, EXIT_FAILURE,
+ malloc(), free(), strtoumax(),
+ realloc(), setenv(), calloc(),
+ mkdtemp(), mkostemp() */
+#include /* not, or, and, xor */
+#include /* error() */
+#include /* EX_USAGE, EX_OSERR, EX_OSFILE */
+#include /* errno, error_t, EACCES,
+ ENAMETOOLONG, ENOENT, ENOTDIR,
+ EEXIST, ECHILD, EPERM, ENOMEM,
+ EAGAIN, EINTR, ENOBUFS, EADDRINUSE,
+ ECONNREFUSED, ECONNRESET,
+ ETOOMANYREFS, EMSGSIZE, EBADF,
+ EINVAL */
+#include /* strdup(), memcpy(),
+ explicit_bzero(), memset(),
+ strcmp(), strlen(), strncpy(),
+ memcmp(), basename() */
+#include /* argz_create(), argz_count(),
+ argz_extract(), argz_next(),
+ argz_add() */
+#include /* epoll_create1(), EPOLL_CLOEXEC,
+ epoll_ctl(), EPOLL_CTL_ADD,
+ struct epoll_event, EPOLLIN,
+ EPOLLRDHUP, EPOLLOUT,
+ epoll_pwait() */
+#include /* struct timespec, clock_gettime(),
+ CLOCK_MONOTONIC */
+#include /* struct argp_option, OPTION_HIDDEN,
+ OPTION_ALIAS, struct argp_state,
+ ARGP_ERR_UNKNOWN, ARGP_KEY_ARGS,
+ struct argp, argp_parse(),
+ ARGP_NO_EXIT */
+#include /* uid_t, gid_t, close(), pipe2(),
+ fork(), _exit(), dup2(),
+ STDOUT_FILENO, setresgid(),
+ setresuid(), execv(), ssize_t,
+ read(), dup3(), getuid(), dup(),
+ STDERR_FILENO, pause(), write(),
+ rmdir(), unlink(), getpid() */
+#include /* munlock(), mlock() */
+#include /* O_CLOEXEC, O_NONBLOCK, fcntl(),
+ F_GETFD, F_GETFL, FD_CLOEXEC,
+ open(), O_WRONLY, O_NOCTTY,
+ O_RDONLY, O_NOFOLLOW */
+#include /* waitpid(), WNOHANG, WIFEXITED(),
+ WEXITSTATUS() */
+#include /* PIPE_BUF, NAME_MAX, INT_MAX */
+#include /* inotify_init1(), IN_NONBLOCK,
+ IN_CLOEXEC, inotify_add_watch(),
+ IN_CLOSE_WRITE, IN_MOVED_TO,
+ IN_MOVED_FROM, IN_DELETE,
+ IN_EXCL_UNLINK, IN_ONLYDIR,
+ struct inotify_event */
+#include /* fnmatch(), FNM_FILE_NAME */
+#include /* asprintf(), FILE, fopen(),
+ getline(), sscanf(), feof(),
+ ferror(), fclose(), stderr,
+ rename(), fdopen(), fprintf(),
+ fscanf() */
+#include /* GKeyFile, g_key_file_free(), g_key_file_new(),
+ GError, g_key_file_load_from_file(),
+ G_KEY_FILE_NONE, TRUE, G_FILE_ERROR_NOENT,
+ g_key_file_get_string(), guint64,
+ g_key_file_get_uint64(),
+ G_KEY_FILE_ERROR_KEY_NOT_FOUND, gconstpointer,
+ g_assert_true(), g_assert_nonnull(),
+ g_assert_null(), g_assert_false(),
+ g_assert_cmpint(), g_assert_cmpuint(),
+ g_test_skip(), g_assert_cmpstr(),
+ g_test_init(), g_test_add(), g_test_run(),
+ GOptionContext, g_option_context_new(),
+ g_option_context_set_help_enabled(), FALSE,
+ g_option_context_set_ignore_unknown_options(),
+ gboolean, GOptionEntry, G_OPTION_ARG_NONE,
+ g_option_context_add_main_entries(),
+ g_option_context_parse(),
+ g_option_context_free(), g_error() */
+#include /* struct sockaddr_un, SUN_LEN */
+#include /* AF_LOCAL, socket(), PF_LOCAL,
+ SOCK_DGRAM, SOCK_NONBLOCK,
+ SOCK_CLOEXEC, connect(),
+ struct sockaddr, socklen_t,
+ shutdown(), SHUT_RD, send(),
+ MSG_NOSIGNAL, bind(), recv(),
+ socketpair() */
+#include /* globfree(), glob_t, glob(),
+ GLOB_ERR, GLOB_NOSORT, GLOB_MARK,
+ GLOB_ABORTED, GLOB_NOMATCH,
+ GLOB_NOSPACE */
+
+/* End of includes */
+
+/* Start of declarations of private types and functions */
+
+/* microseconds of CLOCK_MONOTONIC absolute time; 0 means unset */
+typedef uintmax_t mono_microsecs;
+
+/* "task_queue" - A queue of tasks to be run */
+typedef struct {
+ struct task_struct *tasks; /* Tasks in this queue */
+ size_t length; /* Number of tasks */
+ /* Memory allocated for "tasks", in bytes */
+ size_t allocated;
+ /* Time when this queue should be run, at the latest */
+ mono_microsecs next_run;
+} __attribute__((designated_init)) task_queue;
+
+/* "task_func" - A function type for task functions
+
+ I.e. functions for the code which runs when a task is run, all have
+ this type */
+typedef void (task_func) (const struct task_struct,
+ task_queue *const)
+ __attribute__((nonnull));
+
+/* "buffer" - A data buffer for a growing array of bytes
+
+ Used for the "password" variable */
+typedef struct {
+ char *data;
+ size_t length;
+ size_t allocated;
+} __attribute__((designated_init)) buffer;
+
+/* "string_set" - A set type which can contain strings
+
+ Used by the "cancelled_filenames" variable */
+typedef struct {
+ char *argz; /* Do not access these except in */
+ size_t argz_len; /* the string_set_* functions */
+} __attribute__((designated_init)) string_set;
+
+/* "task_context" - local variables for tasks
+
+ This data structure distinguishes between different tasks which are
+ using the same function. This data structure is passed to every
+ task function when each task is run.
+
+ Note that not every task uses every struct member. */
+typedef struct task_struct {
+ task_func *const func; /* The function run by this task */
+ char *const question_filename; /* The question file */
+ const pid_t pid; /* Mandos client process ID */
+ const int epoll_fd; /* The epoll set file descriptor */
+ bool *const quit_now; /* Set to true on fatal errors */
+ const int fd; /* General purpose file descriptor */
+ bool *const mandos_client_exited; /* Set true when client exits */
+ buffer *const password; /* As read from client process */
+ bool *const password_is_read; /* "password" is done growing */
+ char *filename; /* General purpose file name */
+ /* A set of strings of all the file names of questions which have
+ been cancelled for any reason; tasks pertaining to these question
+ files should not be run */
+ string_set *const cancelled_filenames;
+ const mono_microsecs notafter; /* "NotAfter" from question file */
+ /* Updated before each queue run; is compared with queue.next_run */
+ const mono_microsecs *const current_time;
+} __attribute__((designated_init)) task_context;
+
+/* Declare all our functions here so we can define them in any order
+ below. Note: test functions are *not* declared here, they are
+ declared in the test section. */
+__attribute__((warn_unused_result))
+static bool should_only_run_tests(int *, char **[]);
+__attribute__((warn_unused_result, cold))
+static bool run_tests(int, char *[]);
+static void handle_sigchld(__attribute__((unused)) int sig){}
+__attribute__((warn_unused_result, malloc))
+task_queue *create_queue(void);
+__attribute__((nonnull, warn_unused_result))
+bool add_to_queue(task_queue *const, const task_context);
+__attribute__((nonnull))
+void cleanup_task(const task_context *const);
+__attribute__((nonnull))
+void cleanup_queue(task_queue *const *const);
+__attribute__((pure, nonnull, warn_unused_result))
+bool queue_has_question(const task_queue *const);
+__attribute__((nonnull))
+void cleanup_close(const int *const);
+__attribute__((nonnull))
+void cleanup_string(char *const *const);
+__attribute__((nonnull))
+void cleanup_buffer(buffer *const);
+__attribute__((pure, nonnull, warn_unused_result))
+bool string_set_contains(const string_set, const char *const);
+__attribute__((nonnull, warn_unused_result))
+bool string_set_add(string_set *const, const char *const);
+__attribute__((nonnull))
+void string_set_clear(string_set *);
+void string_set_swap(string_set *const, string_set *const);
+__attribute__((nonnull, warn_unused_result))
+bool start_mandos_client(task_queue *const, const int, bool *const,
+ bool *const, buffer *const, bool *const,
+ const struct sigaction *const,
+ const sigset_t, const char *const,
+ const uid_t, const gid_t,
+ const char *const *const);
+__attribute__((nonnull))
+task_func wait_for_mandos_client_exit;
+__attribute__((nonnull))
+task_func read_mandos_client_output;
+__attribute__((warn_unused_result))
+bool add_inotify_dir_watch(task_queue *const, const int, bool *const,
+ buffer *const, const char *const,
+ string_set *, const mono_microsecs *const,
+ bool *const, bool *const);
+__attribute__((nonnull))
+task_func read_inotify_event;
+__attribute__((nonnull))
+task_func open_and_parse_question;
+__attribute__((nonnull))
+task_func cancel_old_question;
+__attribute__((nonnull))
+task_func connect_question_socket;
+__attribute__((nonnull))
+task_func send_password_to_socket;
+__attribute__((warn_unused_result))
+bool add_existing_questions(task_queue *const, const int,
+ buffer *const, string_set *,
+ const mono_microsecs *const,
+ bool *const, bool *const,
+ const char *const);
+__attribute__((nonnull, warn_unused_result))
+bool wait_for_event(const int, const mono_microsecs,
+ const mono_microsecs);
+bool run_queue(task_queue **const, string_set *const, bool *const);
+bool clear_all_fds_from_epoll_set(const int);
+mono_microsecs get_current_time(void);
+__attribute__((nonnull, warn_unused_result))
+bool setup_signal_handler(struct sigaction *const);
+__attribute__((nonnull))
+bool restore_signal_handler(const struct sigaction *const);
+__attribute__((nonnull, warn_unused_result))
+bool block_sigchld(sigset_t *const);
+__attribute__((nonnull))
+bool restore_sigmask(const sigset_t *const);
+__attribute__((nonnull))
+bool parse_arguments(int, char *[], const bool, char **, char **,
+ uid_t *const , gid_t *const, char **, size_t *);
+
+/* End of declarations of private types and functions */
+
+/* Start of "main" section; this section LACKS TESTS!
+
+ Code here should be as simple as possible. */
+
+/* These are required to be global by Argp */
+const char *argp_program_version = "password-agent " VERSION;
+const char *argp_program_bug_address = "";
+
+int main(int argc, char *argv[]){
+
+ /* If the --test option is passed, skip all normal operations and
+ instead only run the run_tests() function, which also does all
+ its own option parsing, so we don't have to do anything here. */
+ if(should_only_run_tests(&argc, &argv)){
+ if(run_tests(argc, argv)){
+ return EXIT_SUCCESS; /* All tests successful */
+ }
+ return EXIT_FAILURE; /* Some test(s) failed */
+ }
+
+ __attribute__((cleanup(cleanup_string)))
+ char *agent_directory = NULL;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+
+ uid_t user = 0;
+ gid_t group = 0;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ if(not parse_arguments(argc, argv, true, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length)){
+ /* This should never happen, since "true" is passed as the third
+ argument to parse_arguments() above, which should make
+ argp_parse() call exit() if any parsing error occurs. */
+ error(EX_USAGE, errno, "Failed to parse arguments");
+ }
+
+ const char default_agent_directory[] = "/run/systemd/ask-password";
+ const char default_helper_directory[]
+ = "/lib/mandos/plugin-helpers";
+ const char *const default_argv[]
+ = {"/lib/mandos/plugins.d/mandos-client", NULL };
+
+ /* Set variables to default values if unset */
+ if(agent_directory == NULL){
+ agent_directory = strdup(default_agent_directory);
+ if(agent_directory == NULL){
+ error(EX_OSERR, errno, "Failed strdup()");
+ }
+ }
+ if(helper_directory == NULL){
+ helper_directory = strdup(default_helper_directory);
+ if(helper_directory == NULL){
+ error(EX_OSERR, errno, "Failed strdup()");
+ }
+ }
+ if(user == 0){
+ user = 65534; /* nobody */
+ }
+ if(group == 0){
+ group = 65534; /* nogroup */
+ }
+ /* If parse_opt did not create an argz vector, create one with
+ default values */
+ if(mandos_argz == NULL){
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+ /* argz_create() takes a non-const argv for some unknown reason -
+ argz_create() isn't modifying the strings, just copying them.
+ Therefore, this cast to non-const should be safe. */
+#pragma GCC diagnostic ignored "-Wcast-qual"
+#endif
+ errno = argz_create((char *const *)default_argv, &mandos_argz,
+ &mandos_argz_length);
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+ if(errno != 0){
+ error(EX_OSERR, errno, "Failed argz_create()");
+ }
+ }
+ /* Use argz vector to create a normal argv, usable by execv() */
+
+ char **mandos_argv = malloc((argz_count(mandos_argz,
+ mandos_argz_length)
+ + 1) * sizeof(char *));
+ if(mandos_argv == NULL){
+ error_t saved_errno = errno;
+ free(mandos_argz);
+ error(EX_OSERR, saved_errno, "Failed malloc()");
+ }
+ argz_extract(mandos_argz, mandos_argz_length, mandos_argv);
+
+ sigset_t orig_sigmask;
+ if(not block_sigchld(&orig_sigmask)){
+ return EX_OSERR;
+ }
+
+ struct sigaction old_sigchld_action;
+ if(not setup_signal_handler(&old_sigchld_action)){
+ return EX_OSERR;
+ }
+
+ mono_microsecs current_time = 0;
+
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ if(epoll_fd < 0){
+ error(EX_OSERR, errno, "Failed to create epoll set fd");
+ }
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ if(queue == NULL){
+ error(EX_OSERR, errno, "Failed to create task queue");
+ }
+
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ bool password_is_read = false;
+
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+
+ /* Add tasks to queue */
+ if(not start_mandos_client(queue, epoll_fd, &mandos_client_exited,
+ &quit_now, &password, &password_is_read,
+ &old_sigchld_action, orig_sigmask,
+ helper_directory, user, group,
+ (const char *const *)mandos_argv)){
+ return EX_OSERR; /* Error has already been printed */
+ }
+ /* These variables were only for start_mandos_client() and are not
+ needed anymore */
+ free(mandos_argv);
+ free(mandos_argz);
+ mandos_argz = NULL;
+ if(not add_inotify_dir_watch(queue, epoll_fd, &quit_now, &password,
+ agent_directory, &cancelled_filenames,
+ ¤t_time, &mandos_client_exited,
+ &password_is_read)){
+ switch(errno){ /* Error has already been printed */
+ case EACCES:
+ case ENAMETOOLONG:
+ case ENOENT:
+ case ENOTDIR:
+ return EX_OSFILE;
+ default:
+ return EX_OSERR;
+ }
+ }
+ if(not add_existing_questions(queue, epoll_fd, &password,
+ &cancelled_filenames, ¤t_time,
+ &mandos_client_exited,
+ &password_is_read, agent_directory)){
+ return EXIT_FAILURE; /* Error has already been printed */
+ }
+
+ /* Run queue */
+ do {
+ current_time = get_current_time();
+ if(not wait_for_event(epoll_fd, queue->next_run, current_time)){
+ const error_t saved_errno = errno;
+ error(EXIT_FAILURE, saved_errno, "Failure while waiting for"
+ " events");
+ }
+
+ current_time = get_current_time();
+ if(not run_queue(&queue, &cancelled_filenames, &quit_now)){
+ const error_t saved_errno = errno;
+ error(EXIT_FAILURE, saved_errno, "Failure while running queue");
+ }
+
+ /* When no tasks about questions are left in the queue, break out
+ of the loop (and implicitly exit the program) */
+ } while(queue_has_question(queue));
+
+ restore_signal_handler(&old_sigchld_action);
+ restore_sigmask(&orig_sigmask);
+
+ return EXIT_SUCCESS;
+}
+
+__attribute__((warn_unused_result))
+mono_microsecs get_current_time(void){
+ struct timespec currtime;
+ if(clock_gettime(CLOCK_MONOTONIC, &currtime) != 0){
+ error(0, errno, "Failed to get current time");
+ return 0;
+ }
+ return ((mono_microsecs)currtime.tv_sec * 1000000) /* seconds */
+ + ((mono_microsecs)currtime.tv_nsec / 1000); /* nanoseconds */
+}
+
+/* End of "main" section */
+
+/* Start of regular code section; ALL this code has tests */
+
+__attribute__((nonnull))
+bool parse_arguments(int argc, char *argv[], const bool exit_failure,
+ char **agent_directory, char **helper_directory,
+ uid_t *const user, gid_t *const group,
+ char **mandos_argz, size_t *mandos_argz_length){
+
+ const struct argp_option options[] = {
+ { .name="agent-directory",.key='d', .arg="DIRECTORY",
+ .doc="Systemd password agent directory" },
+ { .name="helper-directory",.key=128, .arg="DIRECTORY",
+ .doc="Mandos Client password helper directory" },
+ { .name="plugin-helper-dir", .key=129, /* From plugin-runner */
+ .flags=OPTION_HIDDEN | OPTION_ALIAS },
+ { .name="user", .key='u', .arg="USERID",
+ .doc="User ID the Mandos Client will use as its unprivileged"
+ " user" },
+ { .name="userid", .key=130, /* From plugin--runner */
+ .flags=OPTION_HIDDEN | OPTION_ALIAS },
+ { .name="group", .key='g', .arg="GROUPID",
+ .doc="Group ID the Mandos Client will use as its unprivileged"
+ " group" },
+ { .name="groupid", .key=131, /* From plugin--runner */
+ .flags=OPTION_HIDDEN | OPTION_ALIAS },
+ { .name="test", .key=255, /* See should_only_run_tests() */
+ .doc="Skip normal operation, and only run self-tests. See"
+ " --test --help.", .group=10, },
+ { NULL },
+ };
+
+ __attribute__((nonnull(3)))
+ error_t parse_opt(int key, char *arg, struct argp_state *state){
+ errno = 0;
+ switch(key){
+ case 'd': /* --agent-directory */
+ *agent_directory = strdup(arg);
+ break;
+ case 128: /* --helper-directory */
+ case 129: /* --plugin-helper-dir */
+ *helper_directory = strdup(arg);
+ break;
+ case 'u': /* --user */
+ case 130: /* --userid */
+ {
+ char *tmp;
+ uintmax_t tmp_id = 0;
+ errno = 0;
+ tmp_id = (uid_t)strtoumax(arg, &tmp, 10);
+ if(errno != 0 or tmp == arg or *tmp != '\0'
+ or tmp_id != (uid_t)tmp_id or (uid_t)tmp_id == 0){
+ return ARGP_ERR_UNKNOWN;
+ }
+ *user = (uid_t)tmp_id;
+ errno = 0;
+ break;
+ }
+ case 'g': /* --group */
+ case 131: /* --groupid */
+ {
+ char *tmp;
+ uintmax_t tmp_id = 0;
+ errno = 0;
+ tmp_id = (uid_t)strtoumax(arg, &tmp, 10);
+ if(errno != 0 or tmp == arg or *tmp != '\0'
+ or tmp_id != (gid_t)tmp_id or (gid_t)tmp_id == 0){
+ return ARGP_ERR_UNKNOWN;
+ }
+ *group = (gid_t)tmp_id;
+ errno = 0;
+ break;
+ }
+ case ARGP_KEY_ARGS:
+ /* Copy arguments into argz vector */
+ return argz_create(state->argv + state->next, mandos_argz,
+ mandos_argz_length);
+ default:
+ return ARGP_ERR_UNKNOWN;
+ }
+ return errno;
+ }
+
+ const struct argp argp = {
+ .options=options,
+ .parser=parse_opt,
+ .args_doc="[MANDOS_CLIENT [OPTION...]]\n--test",
+ .doc = "Mandos password agent -- runs Mandos client as a"
+ " systemd password agent",
+ };
+
+ errno = argp_parse(&argp, argc, argv,
+ exit_failure ? 0 : ARGP_NO_EXIT, NULL, NULL);
+
+ return errno == 0;
+}
+
+__attribute__((nonnull, warn_unused_result))
+bool block_sigchld(sigset_t *const orig_sigmask){
+ sigset_t sigchld_sigmask;
+ if(sigemptyset(&sigchld_sigmask) < 0){
+ error(0, errno, "Failed to empty signal set");
+ return false;
+ }
+ if(sigaddset(&sigchld_sigmask, SIGCHLD) < 0){
+ error(0, errno, "Failed to add SIGCHLD to signal set");
+ return false;
+ }
+ if(pthread_sigmask(SIG_BLOCK, &sigchld_sigmask, orig_sigmask) != 0){
+ error(0, errno, "Failed to block SIGCHLD signal");
+ return false;
+ }
+ return true;
+}
+
+__attribute__((nonnull, warn_unused_result, const))
+bool restore_sigmask(const sigset_t *const orig_sigmask){
+ if(pthread_sigmask(SIG_SETMASK, orig_sigmask, NULL) != 0){
+ error(0, errno, "Failed to restore blocked signals");
+ return false;
+ }
+ return true;
+}
+
+__attribute__((nonnull, warn_unused_result))
+bool setup_signal_handler(struct sigaction *const old_sigchld_action){
+ struct sigaction sigchld_action = {
+ .sa_handler=handle_sigchld,
+ .sa_flags=SA_RESTART | SA_NOCLDSTOP,
+ };
+ /* Set all signals in "sa_mask" struct member; this makes all
+ signals automatically blocked during signal handler */
+ if(sigfillset(&sigchld_action.sa_mask) != 0){
+ error(0, errno, "Failed to do sigfillset()");
+ return false;
+ }
+ if(sigaction(SIGCHLD, &sigchld_action, old_sigchld_action) != 0){
+ error(0, errno, "Failed to set SIGCHLD signal handler");
+ return false;
+ }
+ return true;
+}
+
+__attribute__((nonnull, warn_unused_result))
+bool restore_signal_handler(const struct sigaction *const
+ old_sigchld_action){
+ if(sigaction(SIGCHLD, old_sigchld_action, NULL) != 0){
+ error(0, errno, "Failed to restore signal handler");
+ return false;
+ }
+ return true;
+}
+
+__attribute__((warn_unused_result, malloc))
+task_queue *create_queue(void){
+ task_queue *queue = malloc(sizeof(task_queue));
+ if(queue){
+ queue->tasks = NULL;
+ queue->length = 0;
+ queue->allocated = 0;
+ queue->next_run = 0;
+ }
+ return queue;
+}
+
+__attribute__((nonnull, warn_unused_result))
+bool add_to_queue(task_queue *const queue, const task_context task){
+ const size_t needed_size = sizeof(task_context)*(queue->length + 1);
+ if(needed_size > (queue->allocated)){
+ task_context *const new_tasks = realloc(queue->tasks,
+ needed_size);
+ if(new_tasks == NULL){
+ error(0, errno, "Failed to allocate %" PRIuMAX
+ " bytes for queue->tasks", (uintmax_t)needed_size);
+ return false;
+ }
+ queue->tasks = new_tasks;
+ queue->allocated = needed_size;
+ }
+ /* Using memcpy here is necessary because doing */
+ /* queue->tasks[queue->length++] = task; */
+ /* would violate const-ness of task members */
+ memcpy(&(queue->tasks[queue->length++]), &task,
+ sizeof(task_context));
+ return true;
+}
+
+__attribute__((nonnull))
+void cleanup_task(const task_context *const task){
+ const error_t saved_errno = errno;
+ /* free and close all task data */
+ free(task->question_filename);
+ if(task->filename != task->question_filename){
+ free(task->filename);
+ }
+ if(task->pid > 0){
+ kill(task->pid, SIGTERM);
+ }
+ if(task->fd > 0){
+ close(task->fd);
+ }
+ errno = saved_errno;
+}
+
+__attribute__((nonnull))
+void free_queue(task_queue *const queue){
+ free(queue->tasks);
+ free(queue);
+}
+
+__attribute__((nonnull))
+void cleanup_queue(task_queue *const *const queue){
+ if(*queue == NULL){
+ return;
+ }
+ for(size_t i = 0; i < (*queue)->length; i++){
+ const task_context *const task = ((*queue)->tasks)+i;
+ cleanup_task(task);
+ }
+ free_queue(*queue);
+}
+
+__attribute__((pure, nonnull, warn_unused_result))
+bool queue_has_question(const task_queue *const queue){
+ for(size_t i=0; i < queue->length; i++){
+ if(queue->tasks[i].question_filename != NULL){
+ return true;
+ }
+ }
+ return false;
+}
+
+__attribute__((nonnull))
+void cleanup_close(const int *const fd){
+ const error_t saved_errno = errno;
+ close(*fd);
+ errno = saved_errno;
+}
+
+__attribute__((nonnull))
+void cleanup_string(char *const *const ptr){
+ free(*ptr);
+}
+
+__attribute__((nonnull))
+void cleanup_buffer(buffer *buf){
+ if(buf->allocated > 0){
+#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 25)
+ explicit_bzero(buf->data, buf->allocated);
+#else
+ memset(buf->data, '\0', buf->allocated);
+#endif
+ }
+ if(buf->data != NULL){
+ if(munlock(buf->data, buf->allocated) != 0){
+ error(0, errno, "Failed to unlock memory of old buffer");
+ }
+ free(buf->data);
+ buf->data = NULL;
+ }
+ buf->length = 0;
+ buf->allocated = 0;
+}
+
+__attribute__((pure, nonnull, warn_unused_result))
+bool string_set_contains(const string_set set, const char *const str){
+ for(const char *s = set.argz; s != NULL and set.argz_len > 0;
+ s = argz_next(set.argz, set.argz_len, s)){
+ if(strcmp(s, str) == 0){
+ return true;
+ }
+ }
+ return false;
+}
+
+__attribute__((nonnull, warn_unused_result))
+bool string_set_add(string_set *const set, const char *const str){
+ if(string_set_contains(*set, str)){
+ return true;
+ }
+ error_t error = argz_add(&set->argz, &set->argz_len, str);
+ if(error == 0){
+ return true;
+ }
+ errno = error;
+ return false;
+}
+
+__attribute__((nonnull))
+void string_set_clear(string_set *set){
+ free(set->argz);
+ set->argz = NULL;
+ set->argz_len = 0;
+}
+
+__attribute__((nonnull))
+void string_set_swap(string_set *const set1, string_set *const set2){
+ /* Swap contents of two string sets */
+ {
+ char *const tmp_argz = set1->argz;
+ set1->argz = set2->argz;
+ set2->argz = tmp_argz;
+ }
+ {
+ const size_t tmp_argz_len = set1->argz_len;
+ set1->argz_len = set2->argz_len;
+ set2->argz_len = tmp_argz_len;
+ }
+}
+
+__attribute__((nonnull, warn_unused_result))
+bool start_mandos_client(task_queue *const queue,
+ const int epoll_fd,
+ bool *const mandos_client_exited,
+ bool *const quit_now, buffer *const password,
+ bool *const password_is_read,
+ const struct sigaction *const
+ old_sigchld_action, const sigset_t sigmask,
+ const char *const helper_directory,
+ const uid_t user, const gid_t group,
+ const char *const *const argv){
+ int pipefds[2];
+ if(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK) != 0){
+ error(0, errno, "Failed to pipe2(..., O_CLOEXEC | O_NONBLOCK)");
+ return false;
+ }
+
+ const pid_t pid = fork();
+ if(pid == 0){
+ if(not restore_signal_handler(old_sigchld_action)){
+ _exit(EXIT_FAILURE);
+ }
+ if(not restore_sigmask(&sigmask)){
+ _exit(EXIT_FAILURE);
+ }
+ if(close(pipefds[0]) != 0){
+ error(0, errno, "Failed to close() parent pipe fd");
+ _exit(EXIT_FAILURE);
+ }
+ if(dup2(pipefds[1], STDOUT_FILENO) == -1){
+ error(0, errno, "Failed to dup2() pipe fd to stdout");
+ _exit(EXIT_FAILURE);
+ }
+ if(close(pipefds[1]) != 0){
+ error(0, errno, "Failed to close() old child pipe fd");
+ _exit(EXIT_FAILURE);
+ }
+ if(setenv("MANDOSPLUGINHELPERDIR", helper_directory, 1) != 0){
+ error(0, errno, "Failed to setenv(\"MANDOSPLUGINHELPERDIR\","
+ " \"%s\", 1)", helper_directory);
+ _exit(EXIT_FAILURE);
+ }
+ if(group != 0 and setresgid(group, 0, 0) == -1){
+ error(0, errno, "Failed to setresgid(-1, %" PRIuMAX ", %"
+ PRIuMAX")", (uintmax_t)group, (uintmax_t)group);
+ _exit(EXIT_FAILURE);
+ }
+ if(user != 0 and setresuid(user, 0, 0) == -1){
+ error(0, errno, "Failed to setresuid(-1, %" PRIuMAX ", %"
+ PRIuMAX")", (uintmax_t)user, (uintmax_t)user);
+ _exit(EXIT_FAILURE);
+ }
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+ /* For historical reasons, the "argv" argument to execv() is not
+ const, but it is safe to override this. */
+#pragma GCC diagnostic ignored "-Wcast-qual"
+#endif
+ execv(argv[0], (char **)argv);
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+ error(0, errno, "execv(\"%s\", ...) failed", argv[0]);
+ _exit(EXIT_FAILURE);
+ }
+ close(pipefds[1]);
+
+ if(not add_to_queue(queue, (task_context){
+ .func=wait_for_mandos_client_exit,
+ .pid=pid,
+ .mandos_client_exited=mandos_client_exited,
+ .quit_now=quit_now,
+ })){
+ error(0, errno, "Failed to add wait_for_mandos_client to queue");
+ close(pipefds[0]);
+ return false;
+ }
+
+ const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, pipefds[0],
+ &(struct epoll_event)
+ { .events=EPOLLIN | EPOLLRDHUP });
+ if(ret != 0 and errno != EEXIST){
+ error(0, errno, "Failed to add file descriptor to epoll set");
+ close(pipefds[0]);
+ return false;
+ }
+
+ return add_to_queue(queue, (task_context){
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=quit_now,
+ .password=password,
+ .password_is_read=password_is_read,
+ });
+}
+
+__attribute__((nonnull))
+void wait_for_mandos_client_exit(const task_context task,
+ task_queue *const queue){
+ const pid_t pid = task.pid;
+ bool *const mandos_client_exited = task.mandos_client_exited;
+ bool *const quit_now = task.quit_now;
+
+ int status;
+ switch(waitpid(pid, &status, WNOHANG)){
+ case 0: /* Not exited yet */
+ if(not add_to_queue(queue, task)){
+ error(0, errno, "Failed to add myself to queue");
+ *quit_now = true;
+ }
+ break;
+ case -1: /* Error */
+ error(0, errno, "waitpid(%" PRIdMAX ") failed", (intmax_t)pid);
+ if(errno != ECHILD){
+ kill(pid, SIGTERM);
+ }
+ *quit_now = true;
+ break;
+ default: /* Has exited */
+ *mandos_client_exited = true;
+ if((not WIFEXITED(status))
+ or (WEXITSTATUS(status) != EXIT_SUCCESS)){
+ error(0, 0, "Mandos client failed or was killed");
+ *quit_now = true;
+ }
+ }
+}
+
+__attribute__((nonnull))
+void read_mandos_client_output(const task_context task,
+ task_queue *const queue){
+ buffer *const password = task.password;
+ bool *const quit_now = task.quit_now;
+ bool *const password_is_read = task.password_is_read;
+ const int fd = task.fd;
+ const int epoll_fd = task.epoll_fd;
+
+ const size_t new_potential_size = (password->length + PIPE_BUF);
+ if(password->allocated < new_potential_size){
+ char *const new_buffer = calloc(new_potential_size, 1);
+ if(new_buffer == NULL){
+ error(0, errno, "Failed to allocate %" PRIuMAX
+ " bytes for password", (uintmax_t)new_potential_size);
+ *quit_now = true;
+ close(fd);
+ return;
+ }
+ if(mlock(new_buffer, new_potential_size) != 0){
+ /* Warn but do not treat as fatal error */
+ if(errno != EPERM and errno != ENOMEM){
+ error(0, errno, "Failed to lock memory for password");
+ }
+ }
+ if(password->length > 0){
+ memcpy(new_buffer, password->data, password->length);
+#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 25)
+ explicit_bzero(password->data, password->allocated);
+#else
+ memset(password->data, '\0', password->allocated);
+#endif
+ }
+ if(password->data != NULL){
+ if(munlock(password->data, password->allocated) != 0){
+ error(0, errno, "Failed to unlock memory of old buffer");
+ }
+ free(password->data);
+ }
+ password->data = new_buffer;
+ password->allocated = new_potential_size;
+ }
+
+ const ssize_t read_length = read(fd, password->data
+ + password->length, PIPE_BUF);
+
+ if(read_length == 0){ /* EOF */
+ *password_is_read = true;
+ close(fd);
+ return;
+ }
+ if(read_length < 0 and errno != EAGAIN){ /* Actual error */
+ error(0, errno, "Failed to read password from Mandos client");
+ *quit_now = true;
+ close(fd);
+ return;
+ }
+ if(read_length > 0){ /* Data has been read */
+ password->length += (size_t)read_length;
+ }
+
+ /* Either data was read, or EAGAIN was indicated, meaning no data
+ available yet */
+
+ /* Re-add the fd to the epoll set */
+ const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd,
+ &(struct epoll_event)
+ { .events=EPOLLIN | EPOLLRDHUP });
+ if(ret != 0 and errno != EEXIST){
+ error(0, errno, "Failed to re-add file descriptor to epoll set");
+ *quit_now = true;
+ close(fd);
+ return;
+ }
+
+ /* Re-add myself to the queue */
+ if(not add_to_queue(queue, task)){
+ error(0, errno, "Failed to add myself to queue");
+ *quit_now = true;
+ close(fd);
+ }
+}
+
+__attribute__((nonnull, warn_unused_result))
+bool add_inotify_dir_watch(task_queue *const queue,
+ const int epoll_fd, bool *const quit_now,
+ buffer *const password,
+ const char *const dir,
+ string_set *cancelled_filenames,
+ const mono_microsecs *const current_time,
+ bool *const mandos_client_exited,
+ bool *const password_is_read){
+ const int fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
+ if(fd == -1){
+ error(0, errno, "Failed to create inotify instance");
+ return false;
+ }
+
+ if(inotify_add_watch(fd, dir, IN_CLOSE_WRITE | IN_MOVED_TO
+ | IN_MOVED_FROM| IN_DELETE | IN_EXCL_UNLINK
+ | IN_ONLYDIR)
+ == -1){
+ error(0, errno, "Failed to create inotify watch on %s", dir);
+ return false;
+ }
+
+ /* Add the inotify fd to the epoll set */
+ const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd,
+ &(struct epoll_event)
+ { .events=EPOLLIN | EPOLLRDHUP });
+ if(ret != 0 and errno != EEXIST){
+ error(0, errno, "Failed to add file descriptor to epoll set");
+ close(fd);
+ return false;
+ }
+
+ const task_context read_inotify_event_task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .quit_now=quit_now,
+ .password=password,
+ .fd=fd,
+ .filename=strdup(dir),
+ .cancelled_filenames=cancelled_filenames,
+ .current_time=current_time,
+ .mandos_client_exited=mandos_client_exited,
+ .password_is_read=password_is_read,
+ };
+ if(read_inotify_event_task.filename == NULL){
+ error(0, errno, "Failed to strdup(\"%s\")", dir);
+ close(fd);
+ return false;
+ }
+
+ return add_to_queue(queue, read_inotify_event_task);
+}
+
+__attribute__((nonnull))
+void read_inotify_event(const task_context task,
+ task_queue *const queue){
+ const int fd = task.fd;
+ const int epoll_fd = task.epoll_fd;
+ char *const filename = task.filename;
+ bool *quit_now = task.quit_now;
+ buffer *const password = task.password;
+ string_set *const cancelled_filenames = task.cancelled_filenames;
+ const mono_microsecs *const current_time = task.current_time;
+ bool *const mandos_client_exited = task.mandos_client_exited;
+ bool *const password_is_read = task.password_is_read;
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ struct {
+ struct inotify_event event;
+ char name_buffer[NAME_MAX + 1];
+ } ievent_buffer;
+ struct inotify_event *const ievent = &ievent_buffer.event;
+
+ const ssize_t read_length = read(fd, ievent, ievent_size);
+ if(read_length == 0){ /* EOF */
+ error(0, 0, "Got EOF from inotify fd for directory %s", filename);
+ *quit_now = true;
+ cleanup_task(&task);
+ return;
+ }
+ if(read_length < 0 and errno != EAGAIN){ /* Actual error */
+ error(0, errno, "Failed to read from inotify fd for directory %s",
+ filename);
+ *quit_now = true;
+ cleanup_task(&task);
+ return;
+ }
+ if(read_length > 0 /* Data has been read */
+ and fnmatch("ask.*", ievent->name, FNM_FILE_NAME) == 0){
+ char *question_filename = NULL;
+ const ssize_t question_filename_length
+ = asprintf(&question_filename, "%s/%s", filename, ievent->name);
+ if(question_filename_length < 0){
+ error(0, errno, "Failed to create file name from directory name"
+ " %s and file name %s", filename, ievent->name);
+ } else {
+ if(ievent->mask & (IN_CLOSE_WRITE | IN_MOVED_TO)){
+ if(not add_to_queue(queue, (task_context){
+ .func=open_and_parse_question,
+ .epoll_fd=epoll_fd,
+ .question_filename=question_filename,
+ .filename=question_filename,
+ .password=password,
+ .cancelled_filenames=cancelled_filenames,
+ .current_time=current_time,
+ .mandos_client_exited=mandos_client_exited,
+ .password_is_read=password_is_read,
+ })){
+ error(0, errno, "Failed to add open_and_parse_question task"
+ " for file name %s to queue", filename);
+ } else {
+ /* Force the added task (open_and_parse_question) to run
+ immediately */
+ queue->next_run = 1;
+ }
+ } else if(ievent->mask & (IN_MOVED_FROM | IN_DELETE)){
+ if(not string_set_add(cancelled_filenames,
+ question_filename)){
+ error(0, errno, "Could not add question %s to"
+ " cancelled_questions", question_filename);
+ *quit_now = true;
+ free(question_filename);
+ cleanup_task(&task);
+ return;
+ }
+ free(question_filename);
+ }
+ }
+ }
+
+ /* Either data was read, or EAGAIN was indicated, meaning no data
+ available yet */
+
+ /* Re-add myself to the queue */
+ if(not add_to_queue(queue, task)){
+ error(0, errno, "Failed to re-add read_inotify_event(%s) to"
+ " queue", filename);
+ *quit_now = true;
+ cleanup_task(&task);
+ return;
+ }
+
+ /* Re-add the fd to the epoll set */
+ const int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd,
+ &(struct epoll_event)
+ { .events=EPOLLIN | EPOLLRDHUP });
+ if(ret != 0 and errno != EEXIST){
+ error(0, errno, "Failed to re-add inotify file descriptor %d for"
+ " directory %s to epoll set", fd, filename);
+ /* Force the added task (read_inotify_event) to run again, at most
+ one second from now */
+ if((queue->next_run == 0)
+ or (queue->next_run > (*current_time + 1000000))){
+ queue->next_run = *current_time + 1000000;
+ }
+ }
+}
+
+__attribute__((nonnull))
+void open_and_parse_question(const task_context task,
+ task_queue *const queue){
+ __attribute__((cleanup(cleanup_string)))
+ char *question_filename = task.question_filename;
+ const int epoll_fd = task.epoll_fd;
+ buffer *const password = task.password;
+ string_set *const cancelled_filenames = task.cancelled_filenames;
+ const mono_microsecs *const current_time = task.current_time;
+ bool *const mandos_client_exited = task.mandos_client_exited;
+ bool *const password_is_read = task.password_is_read;
+
+ /* We use the GLib "Key-value file parser" functions to parse the
+ question file. See for specification of contents */
+ __attribute__((nonnull))
+ void cleanup_g_key_file(GKeyFile **key_file){
+ if(*key_file != NULL){
+ g_key_file_free(*key_file);
+ }
+ }
+
+ __attribute__((cleanup(cleanup_g_key_file)))
+ GKeyFile *key_file = g_key_file_new();
+ if(key_file == NULL){
+ error(0, errno, "Failed g_key_file_new() for \"%s\"",
+ question_filename);
+ return;
+ }
+ GError *glib_error = NULL;
+ if(g_key_file_load_from_file(key_file, question_filename,
+ G_KEY_FILE_NONE, &glib_error) != TRUE){
+ /* If a file was removed, we should ignore it, so */
+ /* only show error message if file actually existed */
+ if(glib_error->code != G_FILE_ERROR_NOENT){
+ error(0, 0, "Failed to load question data from file \"%s\": %s",
+ question_filename, glib_error->message);
+ }
+ return;
+ }
+
+ __attribute__((cleanup(cleanup_string)))
+ char *socket_name = g_key_file_get_string(key_file, "Ask",
+ "Socket",
+ &glib_error);
+ if(socket_name == NULL){
+ error(0, 0, "Question file \"%s\" did not contain \"Socket\": %s",
+ question_filename, glib_error->message);
+ return;
+ }
+
+ if(strlen(socket_name) == 0){
+ error(0, 0, "Question file \"%s\" had empty \"Socket\" value",
+ question_filename);
+ return;
+ }
+
+ const guint64 pid = g_key_file_get_uint64(key_file, "Ask", "PID",
+ &glib_error);
+ if(glib_error != NULL){
+ error(0, 0, "Question file \"%s\" contained bad \"PID\": %s",
+ question_filename, glib_error->message);
+ return;
+ }
+
+ if((pid != (guint64)((pid_t)pid))
+ or (kill((pid_t)pid, 0) != 0)){
+ error(0, 0, "PID %" PRIuMAX " in question file \"%s\" is bad or"
+ " does not exist", (uintmax_t)pid, question_filename);
+ return;
+ }
+
+ guint64 notafter = g_key_file_get_uint64(key_file, "Ask",
+ "NotAfter", &glib_error);
+ if(glib_error != NULL){
+ if(glib_error->code != G_KEY_FILE_ERROR_KEY_NOT_FOUND){
+ error(0, 0, "Question file \"%s\" contained bad \"NotAfter\":"
+ " %s", question_filename, glib_error->message);
+ }
+ notafter = 0;
+ }
+ if(notafter != 0){
+ if(queue->next_run == 0 or (queue->next_run > notafter)){
+ queue->next_run = notafter;
+ }
+ if(*current_time >= notafter){
+ return;
+ }
+ }
+
+ const task_context connect_question_socket_task = {
+ .func=connect_question_socket,
+ .question_filename=strdup(question_filename),
+ .epoll_fd=epoll_fd,
+ .password=password,
+ .filename=strdup(socket_name),
+ .cancelled_filenames=task.cancelled_filenames,
+ .mandos_client_exited=mandos_client_exited,
+ .password_is_read=password_is_read,
+ .current_time=current_time,
+ };
+ if(connect_question_socket_task.question_filename == NULL
+ or connect_question_socket_task.filename == NULL
+ or not add_to_queue(queue, connect_question_socket_task)){
+ error(0, errno, "Failed to add connect_question_socket for socket"
+ " %s (from \"%s\") to queue", socket_name,
+ question_filename);
+ cleanup_task(&connect_question_socket_task);
+ return;
+ }
+ /* Force the added task (connect_question_socket) to run
+ immediately */
+ queue->next_run = 1;
+
+ if(notafter > 0){
+ char *const dup_filename = strdup(question_filename);
+ const task_context cancel_old_question_task = {
+ .func=cancel_old_question,
+ .question_filename=dup_filename,
+ .notafter=notafter,
+ .filename=dup_filename,
+ .cancelled_filenames=cancelled_filenames,
+ .current_time=current_time,
+ };
+ if(cancel_old_question_task.question_filename == NULL
+ or not add_to_queue(queue, cancel_old_question_task)){
+ error(0, errno, "Failed to add cancel_old_question for file "
+ "\"%s\" to queue", question_filename);
+ cleanup_task(&cancel_old_question_task);
+ return;
+ }
+ }
+}
+
+__attribute__((nonnull))
+void cancel_old_question(const task_context task,
+ task_queue *const queue){
+ char *const question_filename = task.question_filename;
+ string_set *const cancelled_filenames = task.cancelled_filenames;
+ const mono_microsecs notafter = task.notafter;
+ const mono_microsecs *const current_time = task.current_time;
+
+ if(*current_time >= notafter){
+ if(not string_set_add(cancelled_filenames, question_filename)){
+ error(0, errno, "Failed to cancel question for file %s",
+ question_filename);
+ }
+ cleanup_task(&task);
+ return;
+ }
+
+ if(not add_to_queue(queue, task)){
+ error(0, errno, "Failed to add cancel_old_question for file "
+ "%s to queue", question_filename);
+ cleanup_task(&task);
+ return;
+ }
+
+ if((queue->next_run == 0) or (queue->next_run > notafter)){
+ queue->next_run = notafter;
+ }
+}
+
+__attribute__((nonnull))
+void connect_question_socket(const task_context task,
+ task_queue *const queue){
+ char *const question_filename = task.question_filename;
+ char *const filename = task.filename;
+ const int epoll_fd = task.epoll_fd;
+ buffer *const password = task.password;
+ string_set *const cancelled_filenames = task.cancelled_filenames;
+ bool *const mandos_client_exited = task.mandos_client_exited;
+ bool *const password_is_read = task.password_is_read;
+ const mono_microsecs *const current_time = task.current_time;
+
+ struct sockaddr_un sock_name = { .sun_family=AF_LOCAL };
+
+ if(sizeof(sock_name.sun_path) <= strlen(filename)){
+ error(0, 0, "Socket filename is larger than"
+ " sizeof(sockaddr_un.sun_path); %" PRIuMAX ": \"%s\"",
+ (uintmax_t)sizeof(sock_name.sun_path), filename);
+ if(not string_set_add(cancelled_filenames, question_filename)){
+ error(0, errno, "Failed to cancel question for file %s",
+ question_filename);
+ }
+ cleanup_task(&task);
+ return;
+ }
+
+ const int fd = socket(PF_LOCAL, SOCK_DGRAM
+ | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
+ if(fd < 0){
+ error(0, errno,
+ "Failed to create socket(PF_LOCAL, SOCK_DGRAM, 0)");
+ if(not add_to_queue(queue, task)){
+ error(0, errno, "Failed to add connect_question_socket for file"
+ " \"%s\" and socket \"%s\" to queue", question_filename,
+ filename);
+ cleanup_task(&task);
+ } else {
+ /* Force the added task (connect_question_socket) to run
+ immediately */
+ queue->next_run = 1;
+ }
+ return;
+ }
+
+ strncpy(sock_name.sun_path, filename, sizeof(sock_name.sun_path));
+ if(connect(fd, (struct sockaddr *)&sock_name,
+ (socklen_t)SUN_LEN(&sock_name)) != 0){
+ error(0, errno, "Failed to connect socket to \"%s\"", filename);
+ if(not add_to_queue(queue, task)){
+ error(0, errno, "Failed to add connect_question_socket for file"
+ " \"%s\" and socket \"%s\" to queue", question_filename,
+ filename);
+ cleanup_task(&task);
+ } else {
+ /* Force the added task (connect_question_socket) to run again,
+ at most one second from now */
+ if((queue->next_run == 0)
+ or (queue->next_run > (*current_time + 1000000))){
+ queue->next_run = *current_time + 1000000;
+ }
+ }
+ return;
+ }
+
+ /* Not necessary, but we can try, and merely warn on failure */
+ if(shutdown(fd, SHUT_RD) != 0){
+ error(0, errno, "Failed to shutdown reading from socket \"%s\"",
+ filename);
+ }
+
+ /* Add the fd to the epoll set */
+ if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd,
+ &(struct epoll_event){ .events=EPOLLOUT })
+ != 0){
+ error(0, errno, "Failed to add inotify file descriptor %d for"
+ " socket %s to epoll set", fd, filename);
+ if(not add_to_queue(queue, task)){
+ error(0, errno, "Failed to add connect_question_socket for file"
+ " \"%s\" and socket \"%s\" to queue", question_filename,
+ filename);
+ cleanup_task(&task);
+ } else {
+ /* Force the added task (connect_question_socket) to run again,
+ at most one second from now */
+ if((queue->next_run == 0)
+ or (queue->next_run > (*current_time + 1000000))){
+ queue->next_run = *current_time + 1000000;
+ }
+ }
+ return;
+ }
+
+ /* add task send_password_to_socket to queue */
+ const task_context send_password_to_socket_task = {
+ .func=send_password_to_socket,
+ .question_filename=question_filename,
+ .filename=filename,
+ .epoll_fd=epoll_fd,
+ .fd=fd,
+ .password=password,
+ .cancelled_filenames=cancelled_filenames,
+ .mandos_client_exited=mandos_client_exited,
+ .password_is_read=password_is_read,
+ .current_time=current_time,
+ };
+
+ if(not add_to_queue(queue, send_password_to_socket_task)){
+ error(0, errno, "Failed to add send_password_to_socket for"
+ " file \"%s\" and socket \"%s\" to queue",
+ question_filename, filename);
+ cleanup_task(&send_password_to_socket_task);
+ }
+}
+
+__attribute__((nonnull))
+void send_password_to_socket(const task_context task,
+ task_queue *const queue){
+ char *const question_filename=task.question_filename;
+ char *const filename=task.filename;
+ const int epoll_fd=task.epoll_fd;
+ const int fd=task.fd;
+ buffer *const password=task.password;
+ string_set *const cancelled_filenames=task.cancelled_filenames;
+ bool *const mandos_client_exited = task.mandos_client_exited;
+ bool *const password_is_read = task.password_is_read;
+ const mono_microsecs *const current_time = task.current_time;
+
+ if(*mandos_client_exited and *password_is_read){
+
+ const size_t send_buffer_length = password->length + 2;
+ char *send_buffer = malloc(send_buffer_length);
+ if(send_buffer == NULL){
+ error(0, errno, "Failed to allocate send_buffer");
+ } else {
+ if(mlock(send_buffer, send_buffer_length) != 0){
+ /* Warn but do not treat as fatal error */
+ if(errno != EPERM and errno != ENOMEM){
+ error(0, errno, "Failed to lock memory for password"
+ " buffer");
+ }
+ }
+ /* “[…] send a single datagram to the socket consisting of the
+ password string either prefixed with "+" or with "-"
+ depending on whether the password entry was successful or
+ not. You may but don't have to include a final NUL byte in
+ your message.
+
+ — (Wed 08 Oct 2014 02:14:28 AM UTC)
+ */
+ send_buffer[0] = '+'; /* Prefix with "+" */
+ /* Always add an extra NUL */
+ send_buffer[password->length + 1] = '\0';
+ if(password->length > 0){
+ memcpy(send_buffer + 1, password->data, password->length);
+ }
+ errno = 0;
+ ssize_t ssret = send(fd, send_buffer, send_buffer_length,
+ MSG_NOSIGNAL);
+ const error_t saved_errno = errno;
+#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 25)
+ explicit_bzero(send_buffer, send_buffer_length);
+#else
+ memset(send_buffer, '\0', send_buffer_length);
+#endif
+ if(munlock(send_buffer, send_buffer_length) != 0){
+ error(0, errno, "Failed to unlock memory of send buffer");
+ }
+ free(send_buffer);
+ if(ssret < 0 or ssret < (ssize_t)send_buffer_length){
+ switch(saved_errno){
+ case EINTR:
+ case ENOBUFS:
+ case ENOMEM:
+ case EADDRINUSE:
+ case ECONNREFUSED:
+ case ECONNRESET:
+ case ENOENT:
+ case ETOOMANYREFS:
+ case EAGAIN:
+ /* Retry, below */
+ break;
+ case EMSGSIZE:
+ error(0, 0, "Password of size %" PRIuMAX " is too big",
+ (uintmax_t)password->length);
+#if __GNUC__ < 7
+ /* FALLTHROUGH */
+#else
+ __attribute__((fallthrough));
+#endif
+ case 0:
+ if(ssret >= 0 and ssret < (ssize_t)send_buffer_length){
+ error(0, 0, "Password only partially sent to socket");
+ }
+#if __GNUC__ < 7
+ /* FALLTHROUGH */
+#else
+ __attribute__((fallthrough));
+#endif
+ default:
+ error(0, saved_errno, "Failed to send() to socket %s",
+ filename);
+ if(not string_set_add(cancelled_filenames,
+ question_filename)){
+ error(0, errno, "Failed to cancel question for file %s",
+ question_filename);
+ }
+ cleanup_task(&task);
+ return;
+ }
+ } else {
+ /* Success */
+ cleanup_task(&task);
+ return;
+ }
+ }
+ }
+
+ /* We failed or are not ready yet; retry later */
+
+ if(not add_to_queue(queue, task)){
+ error(0, errno, "Failed to add send_password_to_socket for"
+ " file %s and socket %s to queue", question_filename,
+ filename);
+ cleanup_task(&task);
+ }
+
+ /* Add the fd to the epoll set */
+ if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd,
+ &(struct epoll_event){ .events=EPOLLOUT })
+ != 0){
+ error(0, errno, "Failed to add socket file descriptor %d for"
+ " socket %s to epoll set", fd, filename);
+ /* Force the added task (send_password_to_socket) to run again, at
+ most one second from now */
+ if((queue->next_run == 0)
+ or (queue->next_run > (*current_time + 1000000))){
+ queue->next_run = *current_time + 1000000;
+ }
+ }
+}
+
+__attribute__((warn_unused_result))
+bool add_existing_questions(task_queue *const queue,
+ const int epoll_fd,
+ buffer *const password,
+ string_set *cancelled_filenames,
+ const mono_microsecs *const current_time,
+ bool *const mandos_client_exited,
+ bool *const password_is_read,
+ const char *const dirname){
+ __attribute__((cleanup(cleanup_string)))
+ char *dir_pattern = NULL;
+ const int ret = asprintf(&dir_pattern, "%s/ask.*", dirname);
+ if(ret < 0 or dir_pattern == NULL){
+ error(0, errno, "Could not create glob pattern for directory %s",
+ dirname);
+ return false;
+ }
+ __attribute__((cleanup(globfree)))
+ glob_t question_filenames = {};
+ switch(glob(dir_pattern, GLOB_ERR | GLOB_NOSORT | GLOB_MARK,
+ NULL, &question_filenames)){
+ case GLOB_ABORTED:
+ default:
+ error(0, errno, "Failed to open directory %s", dirname);
+ return false;
+ case GLOB_NOMATCH:
+ error(0, errno, "There are no question files in %s", dirname);
+ return false;
+ case GLOB_NOSPACE:
+ error(0, errno, "Could not allocate memory for question file"
+ " names in %s", dirname);
+#if __GNUC__ < 7
+ /* FALLTHROUGH */
+#else
+ __attribute__((fallthrough));
+#endif
+ case 0:
+ for(size_t i = 0; i < question_filenames.gl_pathc; i++){
+ char *const question_filename = strdup(question_filenames
+ .gl_pathv[i]);
+ const task_context task = {
+ .func=open_and_parse_question,
+ .epoll_fd=epoll_fd,
+ .question_filename=question_filename,
+ .filename=question_filename,
+ .password=password,
+ .cancelled_filenames=cancelled_filenames,
+ .current_time=current_time,
+ .mandos_client_exited=mandos_client_exited,
+ .password_is_read=password_is_read,
+ };
+
+ if(question_filename == NULL
+ or not add_to_queue(queue, task)){
+ error(0, errno, "Failed to add open_and_parse_question for"
+ " file %s to queue",
+ question_filenames.gl_pathv[i]);
+ free(question_filename);
+ } else {
+ queue->next_run = 1;
+ }
+ }
+ return true;
+ }
+}
+
+__attribute__((nonnull, warn_unused_result))
+bool wait_for_event(const int epoll_fd,
+ const mono_microsecs queue_next_run,
+ const mono_microsecs current_time){
+ __attribute__((const))
+ int milliseconds_to_wait(const mono_microsecs currtime,
+ const mono_microsecs nextrun){
+ if(currtime >= nextrun){
+ return 0;
+ }
+ const uintmax_t wait_time_ms = (nextrun - currtime) / 1000;
+ if(wait_time_ms > (uintmax_t)INT_MAX){
+ return INT_MAX;
+ }
+ return (int)wait_time_ms;
+ }
+
+ const int wait_time_ms = milliseconds_to_wait(current_time,
+ queue_next_run);
+
+ /* Prepare unblocking of SIGCHLD during epoll_pwait */
+ sigset_t temporary_unblocked_sigmask;
+ /* Get current signal mask */
+ if(pthread_sigmask(-1, NULL, &temporary_unblocked_sigmask) != 0){
+ return false;
+ }
+ /* Remove SIGCHLD from the signal mask */
+ if(sigdelset(&temporary_unblocked_sigmask, SIGCHLD) != 0){
+ return false;
+ }
+ struct epoll_event events[8]; /* Ignored */
+ int ret = epoll_pwait(epoll_fd, events,
+ sizeof(events) / sizeof(struct epoll_event),
+ queue_next_run == 0 ? -1 : (int)wait_time_ms,
+ &temporary_unblocked_sigmask);
+ if(ret < 0 and errno != EINTR){
+ error(0, errno, "Failed epoll_pwait(epfd=%d, ..., timeout=%d,"
+ " ...", epoll_fd,
+ queue_next_run == 0 ? -1 : (int)wait_time_ms);
+ return false;
+ }
+ return clear_all_fds_from_epoll_set(epoll_fd);
+}
+
+bool clear_all_fds_from_epoll_set(const int epoll_fd){
+ /* Create a new empty epoll set */
+ __attribute__((cleanup(cleanup_close)))
+ const int new_epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ if(new_epoll_fd < 0){
+ return false;
+ }
+ /* dup3() the new epoll set fd over the old one, replacing it */
+ if(dup3(new_epoll_fd, epoll_fd, O_CLOEXEC) < 0){
+ return false;
+ }
+ return true;
+}
+
+__attribute__((nonnull, warn_unused_result))
+bool run_queue(task_queue **const queue,
+ string_set *const cancelled_filenames,
+ bool *const quit_now){
+
+ task_queue *new_queue = create_queue();
+ if(new_queue == NULL){
+ return false;
+ }
+
+ __attribute__((cleanup(string_set_clear)))
+ string_set old_cancelled_filenames = {};
+ string_set_swap(cancelled_filenames, &old_cancelled_filenames);
+
+ /* Declare i outside the for loop, since we might need i after the
+ loop in case we aborted in the middle */
+ size_t i;
+ for(i=0; i < (*queue)->length and not *quit_now; i++){
+ task_context *const task = &((*queue)->tasks[i]);
+ const char *const question_filename = task->question_filename;
+ /* Skip any task referencing a cancelled question filename */
+ if(question_filename != NULL
+ and string_set_contains(old_cancelled_filenames,
+ question_filename)){
+ cleanup_task(task);
+ continue;
+ }
+ task->func(*task, new_queue);
+ }
+
+ if(*quit_now){
+ /* we might be in the middle of the queue, so clean up any
+ remaining tasks in the current queue */
+ for(; i < (*queue)->length; i++){
+ cleanup_task(&((*queue)->tasks[i]));
+ }
+ free_queue(*queue);
+ *queue = new_queue;
+ new_queue = NULL;
+ return false;
+ }
+ free_queue(*queue);
+ *queue = new_queue;
+ new_queue = NULL;
+
+ return true;
+}
+
+/* End of regular code section */
+
+/* Start of tests section; here are the tests for the above code */
+
+/* This "fixture" data structure is used by the test setup and
+ teardown functions */
+typedef struct {
+ struct sigaction orig_sigaction;
+ sigset_t orig_sigmask;
+} test_fixture;
+
+static void test_setup(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ g_assert_true(setup_signal_handler(&fixture->orig_sigaction));
+ g_assert_true(block_sigchld(&fixture->orig_sigmask));
+}
+
+static void test_teardown(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ g_assert_true(restore_signal_handler(&fixture->orig_sigaction));
+ g_assert_true(restore_sigmask(&fixture->orig_sigmask));
+}
+
+/* Utility function used by tests to search queue for matching task */
+__attribute__((pure, nonnull, warn_unused_result))
+static task_context *find_matching_task(const task_queue *const queue,
+ const task_context task){
+ /* The argument "task" structure is a pattern to match; 0 in any
+ member means any value matches, otherwise the value must match.
+ The filename strings are compared by strcmp(), not by pointer. */
+ for(size_t i = 0; i < queue->length; i++){
+ task_context *const current_task = queue->tasks+i;
+ /* Check all members of task_context, if set to a non-zero value.
+ If a member does not match, continue to next task in queue */
+
+ /* task_func *const func */
+ if(task.func != NULL and current_task->func != task.func){
+ continue;
+ }
+ /* char *const question_filename; */
+ if(task.question_filename != NULL
+ and (current_task->question_filename == NULL
+ or strcmp(current_task->question_filename,
+ task.question_filename) != 0)){
+ continue;
+ }
+ /* const pid_t pid; */
+ if(task.pid != 0 and current_task->pid != task.pid){
+ continue;
+ }
+ /* const int epoll_fd; */
+ if(task.epoll_fd != 0
+ and current_task->epoll_fd != task.epoll_fd){
+ continue;
+ }
+ /* bool *const quit_now; */
+ if(task.quit_now != NULL
+ and current_task->quit_now != task.quit_now){
+ continue;
+ }
+ /* const int fd; */
+ if(task.fd != 0 and current_task->fd != task.fd){
+ continue;
+ }
+ /* bool *const mandos_client_exited; */
+ if(task.mandos_client_exited != NULL
+ and current_task->mandos_client_exited
+ != task.mandos_client_exited){
+ continue;
+ }
+ /* buffer *const password; */
+ if(task.password != NULL
+ and current_task->password != task.password){
+ continue;
+ }
+ /* bool *const password_is_read; */
+ if(task.password_is_read != NULL
+ and current_task->password_is_read != task.password_is_read){
+ continue;
+ }
+ /* char *filename; */
+ if(task.filename != NULL
+ and (current_task->filename == NULL
+ or strcmp(current_task->filename, task.filename) != 0)){
+ continue;
+ }
+ /* string_set *const cancelled_filenames; */
+ if(task.cancelled_filenames != NULL
+ and current_task->cancelled_filenames
+ != task.cancelled_filenames){
+ continue;
+ }
+ /* const mono_microsecs notafter; */
+ if(task.notafter != 0
+ and current_task->notafter != task.notafter){
+ continue;
+ }
+ /* const mono_microsecs *const current_time; */
+ if(task.current_time != NULL
+ and current_task->current_time != task.current_time){
+ continue;
+ }
+ /* Current task matches all members; return it */
+ return current_task;
+ }
+ /* No task in queue matches passed pattern task */
+ return NULL;
+}
+
+static void test_create_queue(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *const queue = create_queue();
+ g_assert_nonnull(queue);
+ g_assert_null(queue->tasks);
+ g_assert_true(queue->length == 0);
+ g_assert_true(queue->next_run == 0);
+}
+
+static task_func dummy_func;
+
+static void test_add_to_queue(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ g_assert_true(add_to_queue(queue,
+ (task_context){ .func=dummy_func }));
+ g_assert_true(queue->length == 1);
+ g_assert_nonnull(queue->tasks);
+ g_assert_true(queue->tasks[0].func == dummy_func);
+}
+
+static void dummy_func(__attribute__((unused))
+ const task_context task,
+ __attribute__((unused))
+ task_queue *const queue){
+}
+
+static void test_queue_has_question_empty(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ g_assert_false(queue_has_question(queue));
+}
+
+static void test_queue_has_question_false(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ g_assert_true(add_to_queue(queue,
+ (task_context){ .func=dummy_func }));
+ g_assert_false(queue_has_question(queue));
+}
+
+static void test_queue_has_question_true(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ char *const question_filename
+ = strdup("/nonexistent/question_filename");
+ g_assert_nonnull(question_filename);
+ task_context task = {
+ .func=dummy_func,
+ .question_filename=question_filename,
+ };
+ g_assert_true(add_to_queue(queue, task));
+ g_assert_true(queue_has_question(queue));
+}
+
+static void test_queue_has_question_false2(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ task_context task = { .func=dummy_func };
+ g_assert_true(add_to_queue(queue, task));
+ g_assert_true(add_to_queue(queue, task));
+ g_assert_cmpint((int)queue->length, ==, 2);
+ g_assert_false(queue_has_question(queue));
+}
+
+static void test_queue_has_question_true2(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ task_context task1 = { .func=dummy_func };
+ g_assert_true(add_to_queue(queue, task1));
+ char *const question_filename
+ = strdup("/nonexistent/question_filename");
+ g_assert_nonnull(question_filename);
+ task_context task2 = {
+ .func=dummy_func,
+ .question_filename=question_filename,
+ };
+ g_assert_true(add_to_queue(queue, task2));
+ g_assert_cmpint((int)queue->length, ==, 2);
+ g_assert_true(queue_has_question(queue));
+}
+
+static void test_cleanup_buffer(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ buffer buf = {};
+
+ const size_t buffersize = 10;
+
+ buf.data = malloc(buffersize);
+ g_assert_nonnull(buf.data);
+ if(mlock(buf.data, buffersize) != 0){
+ g_assert_true(errno == EPERM or errno == ENOMEM);
+ }
+
+ cleanup_buffer(&buf);
+ g_assert_null(buf.data);
+}
+
+static
+void test_string_set_new_set_contains_nothing(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(string_set_clear)))
+ string_set set = {};
+ g_assert_false(string_set_contains(set, "")); /* Empty string */
+ g_assert_false(string_set_contains(set, "test_string"));
+}
+
+static void
+test_string_set_with_added_string_contains_it(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(string_set_clear)))
+ string_set set = {};
+ g_assert_true(string_set_add(&set, "test_string"));
+ g_assert_true(string_set_contains(set, "test_string"));
+}
+
+static void
+test_string_set_cleared_does_not_contain_str(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(string_set_clear)))
+ string_set set = {};
+ g_assert_true(string_set_add(&set, "test_string"));
+ string_set_clear(&set);
+ g_assert_false(string_set_contains(set, "test_string"));
+}
+
+static
+void test_string_set_swap_one_with_empty(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(string_set_clear)))
+ string_set set1 = {};
+ __attribute__((cleanup(string_set_clear)))
+ string_set set2 = {};
+ g_assert_true(string_set_add(&set1, "test_string1"));
+ string_set_swap(&set1, &set2);
+ g_assert_false(string_set_contains(set1, "test_string1"));
+ g_assert_true(string_set_contains(set2, "test_string1"));
+}
+
+static
+void test_string_set_swap_empty_with_one(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(string_set_clear)))
+ string_set set1 = {};
+ __attribute__((cleanup(string_set_clear)))
+ string_set set2 = {};
+ g_assert_true(string_set_add(&set2, "test_string2"));
+ string_set_swap(&set1, &set2);
+ g_assert_true(string_set_contains(set1, "test_string2"));
+ g_assert_false(string_set_contains(set2, "test_string2"));
+}
+
+static void test_string_set_swap_one_with_one(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(string_set_clear)))
+ string_set set1 = {};
+ __attribute__((cleanup(string_set_clear)))
+ string_set set2 = {};
+ g_assert_true(string_set_add(&set1, "test_string1"));
+ g_assert_true(string_set_add(&set2, "test_string2"));
+ string_set_swap(&set1, &set2);
+ g_assert_false(string_set_contains(set1, "test_string1"));
+ g_assert_true(string_set_contains(set1, "test_string2"));
+ g_assert_false(string_set_contains(set2, "test_string2"));
+ g_assert_true(string_set_contains(set2, "test_string1"));
+}
+
+static bool fd_has_cloexec_and_nonblock(const int);
+
+static bool epoll_set_contains(int, int, uint32_t);
+
+static void test_start_mandos_client(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ buffer password = {};
+ bool password_is_read = false;
+ const char helper_directory[] = "/nonexistent";
+ const char *const argv[] = { "/bin/true", NULL };
+
+ g_assert_true(start_mandos_client(queue, epoll_fd,
+ &mandos_client_exited, &quit_now,
+ &password, &password_is_read,
+ &fixture->orig_sigaction,
+ fixture->orig_sigmask,
+ helper_directory, 0, 0, argv));
+
+ g_assert_cmpuint((unsigned int)queue->length, >=, 2);
+
+ const task_context *const added_wait_task
+ = find_matching_task(queue, (task_context){
+ .func=wait_for_mandos_client_exit,
+ .mandos_client_exited=&mandos_client_exited,
+ .quit_now=&quit_now,
+ });
+ g_assert_nonnull(added_wait_task);
+ g_assert_cmpint(added_wait_task->pid, >, 0);
+ g_assert_cmpint(kill(added_wait_task->pid, SIGKILL), ==, 0);
+ waitpid(added_wait_task->pid, NULL, 0);
+
+ const task_context *const added_read_task
+ = find_matching_task(queue, (task_context){
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ });
+ g_assert_nonnull(added_read_task);
+ g_assert_cmpint(added_read_task->fd, >, 2);
+ g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd));
+ g_assert_true(epoll_set_contains(epoll_fd, added_read_task->fd,
+ EPOLLIN | EPOLLRDHUP));
+}
+
+static bool fd_has_cloexec_and_nonblock(const int fd){
+ const int socket_fd_flags = fcntl(fd, F_GETFD, 0);
+ const int socket_file_flags = fcntl(fd, F_GETFL, 0);
+ return ((socket_fd_flags >= 0)
+ and (socket_fd_flags & FD_CLOEXEC)
+ and (socket_file_flags >= 0)
+ and (socket_file_flags & O_NONBLOCK));
+}
+
+__attribute__((const))
+bool is_privileged(void){
+ uid_t user = getuid() + 1;
+ if(user == 0){ /* Overflow check */
+ user++;
+ }
+ gid_t group = getuid() + 1;
+ if(group == 0){ /* Overflow check */
+ group++;
+ }
+ const pid_t pid = fork();
+ if(pid == 0){ /* Child */
+ if(setresgid((uid_t)-1, group, group) == -1){
+ if(errno != EPERM){
+ error(EXIT_FAILURE, errno, "Failed to setresgid(-1, %" PRIuMAX
+ ", %" PRIuMAX")", (uintmax_t)group, (uintmax_t)group);
+ }
+ exit(EXIT_FAILURE);
+ }
+ if(setresuid((uid_t)-1, user, user) == -1){
+ if(errno != EPERM){
+ error(EXIT_FAILURE, errno, "Failed to setresuid(-1, %" PRIuMAX
+ ", %" PRIuMAX")", (uintmax_t)user, (uintmax_t)user);
+ }
+ exit(EXIT_FAILURE);
+ }
+ exit(EXIT_SUCCESS);
+ }
+ int status;
+ waitpid(pid, &status, 0);
+ if(WIFEXITED(status) and (WEXITSTATUS(status) == EXIT_SUCCESS)){
+ return true;
+ }
+ return false;
+}
+
+static bool epoll_set_contains(int epoll_fd, int fd, uint32_t events){
+ /* Only scan for events in this eventmask */
+ const uint32_t eventmask = EPOLLIN | EPOLLOUT | EPOLLRDHUP;
+ __attribute__((cleanup(cleanup_string)))
+ char *fdinfo_name = NULL;
+ int ret = asprintf(&fdinfo_name, "/proc/self/fdinfo/%d", epoll_fd);
+ g_assert_cmpint(ret, >, 0);
+ g_assert_nonnull(fdinfo_name);
+
+ FILE *fdinfo = fopen(fdinfo_name, "r");
+ g_assert_nonnull(fdinfo);
+ uint32_t reported_events;
+ buffer line = {};
+ int found_fd = -1;
+
+ do {
+ if(getline(&line.data, &line.allocated, fdinfo) < 0){
+ break;
+ }
+ /* See proc(5) for format of /proc/PID/fdinfo/FD for epoll fd's */
+ if(sscanf(line.data, "tfd: %d events: %" SCNx32 " ",
+ &found_fd, &reported_events) == 2){
+ if(found_fd == fd){
+ break;
+ }
+ }
+ } while(not feof(fdinfo) and not ferror(fdinfo));
+ g_assert_cmpint(fclose(fdinfo), ==, 0);
+ free(line.data);
+ if(found_fd != fd){
+ return false;
+ }
+
+ if(events == 0){
+ /* Don't check events if none are given */
+ return true;
+ }
+ return (reported_events & eventmask) == (events & eventmask);
+}
+
+static void test_start_mandos_client_execv(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ const char helper_directory[] = "/nonexistent";
+ /* Can't execv("/", ...), so this should fail */
+ const char *const argv[] = { "/", NULL };
+
+ {
+ __attribute__((cleanup(cleanup_close)))
+ const int devnull_fd = open("/dev/null",
+ O_WRONLY | O_CLOEXEC | O_NOCTTY);
+ g_assert_cmpint(devnull_fd, >=, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int real_stderr_fd = dup(STDERR_FILENO);
+ g_assert_cmpint(real_stderr_fd, >=, 0);
+ dup2(devnull_fd, STDERR_FILENO);
+
+ const bool success = start_mandos_client(queue, epoll_fd,
+ &mandos_client_exited,
+ &quit_now,
+ &password,
+ (bool[]){false},
+ &fixture->orig_sigaction,
+ fixture->orig_sigmask,
+ helper_directory, 0, 0,
+ argv);
+ dup2(real_stderr_fd, STDERR_FILENO);
+ g_assert_true(success);
+ }
+ g_assert_cmpuint((unsigned int)queue->length, ==, 2);
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ do {
+ queue->next_run = 0;
+ string_set cancelled_filenames = {};
+
+ {
+ __attribute__((cleanup(cleanup_close)))
+ const int devnull_fd = open("/dev/null",
+ O_WRONLY | O_CLOEXEC | O_NOCTTY);
+ g_assert_cmpint(devnull_fd, >=, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int real_stderr_fd = dup(STDERR_FILENO);
+ g_assert_cmpint(real_stderr_fd, >=, 0);
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ dup2(devnull_fd, STDERR_FILENO);
+ const bool success = run_queue(&queue, &cancelled_filenames,
+ &quit_now);
+ dup2(real_stderr_fd, STDERR_FILENO);
+ if(not success){
+ break;
+ }
+ }
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while(((queue->length) > 0)
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+
+ g_assert_true(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ g_assert_true(mandos_client_exited);
+}
+
+static void test_start_mandos_client_suid_euid(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ if(not is_privileged()){
+ g_test_skip("Not privileged");
+ return;
+ }
+
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ bool password_is_read = false;
+ const char helper_directory[] = "/nonexistent";
+ const char *const argv[] = { "/usr/bin/id", "--user", NULL };
+ uid_t user = 1000;
+ gid_t group = 1001;
+
+ const bool success = start_mandos_client(queue, epoll_fd,
+ &mandos_client_exited,
+ &quit_now, &password,
+ &password_is_read,
+ &fixture->orig_sigaction,
+ fixture->orig_sigmask,
+ helper_directory, user,
+ group, argv);
+ g_assert_true(success);
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ do {
+ queue->next_run = 0;
+ string_set cancelled_filenames = {};
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while(((queue->length) > 0)
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ g_assert_true(mandos_client_exited);
+
+ g_assert_true(password_is_read);
+ g_assert_nonnull(password.data);
+
+ uintmax_t id;
+ g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id),
+ ==, 1);
+ g_assert_true((uid_t)id == id);
+
+ g_assert_cmpuint((unsigned int)id, ==, 0);
+}
+
+static void test_start_mandos_client_suid_egid(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ if(not is_privileged()){
+ g_test_skip("Not privileged");
+ return;
+ }
+
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ bool password_is_read = false;
+ const char helper_directory[] = "/nonexistent";
+ const char *const argv[] = { "/usr/bin/id", "--group", NULL };
+ uid_t user = 1000;
+ gid_t group = 1001;
+
+ const bool success = start_mandos_client(queue, epoll_fd,
+ &mandos_client_exited,
+ &quit_now, &password,
+ &password_is_read,
+ &fixture->orig_sigaction,
+ fixture->orig_sigmask,
+ helper_directory, user,
+ group, argv);
+ g_assert_true(success);
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ do {
+ queue->next_run = 0;
+ string_set cancelled_filenames = {};
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while(((queue->length) > 0)
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ g_assert_true(mandos_client_exited);
+
+ g_assert_true(password_is_read);
+ g_assert_nonnull(password.data);
+
+ uintmax_t id;
+ g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id),
+ ==, 1);
+ g_assert_true((gid_t)id == id);
+
+ g_assert_cmpuint((unsigned int)id, ==, 0);
+}
+
+static void test_start_mandos_client_suid_ruid(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ if(not is_privileged()){
+ g_test_skip("Not privileged");
+ return;
+ }
+
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ bool password_is_read = false;
+ const char helper_directory[] = "/nonexistent";
+ const char *const argv[] = { "/usr/bin/id", "--user", "--real",
+ NULL };
+ uid_t user = 1000;
+ gid_t group = 1001;
+
+ const bool success = start_mandos_client(queue, epoll_fd,
+ &mandos_client_exited,
+ &quit_now, &password,
+ &password_is_read,
+ &fixture->orig_sigaction,
+ fixture->orig_sigmask,
+ helper_directory, user,
+ group, argv);
+ g_assert_true(success);
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ do {
+ queue->next_run = 0;
+ string_set cancelled_filenames = {};
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while(((queue->length) > 0)
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ g_assert_true(mandos_client_exited);
+
+ g_assert_true(password_is_read);
+ g_assert_nonnull(password.data);
+
+ uintmax_t id;
+ g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id),
+ ==, 1);
+ g_assert_true((uid_t)id == id);
+
+ g_assert_cmpuint((unsigned int)id, ==, user);
+}
+
+static void test_start_mandos_client_suid_rgid(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ if(not is_privileged()){
+ g_test_skip("Not privileged");
+ return;
+ }
+
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ bool password_is_read = false;
+ const char helper_directory[] = "/nonexistent";
+ const char *const argv[] = { "/usr/bin/id", "--group", "--real",
+ NULL };
+ uid_t user = 1000;
+ gid_t group = 1001;
+
+ const bool success = start_mandos_client(queue, epoll_fd,
+ &mandos_client_exited,
+ &quit_now, &password,
+ &password_is_read,
+ &fixture->orig_sigaction,
+ fixture->orig_sigmask,
+ helper_directory, user,
+ group, argv);
+ g_assert_true(success);
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ do {
+ queue->next_run = 0;
+ string_set cancelled_filenames = {};
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while(((queue->length) > 0)
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ g_assert_true(mandos_client_exited);
+
+ g_assert_true(password_is_read);
+ g_assert_nonnull(password.data);
+
+ uintmax_t id;
+ g_assert_cmpint(sscanf(password.data, "%" SCNuMAX "\n", &id),
+ ==, 1);
+ g_assert_true((gid_t)id == id);
+
+ g_assert_cmpuint((unsigned int)id, ==, group);
+}
+
+static void test_start_mandos_client_read(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ bool password_is_read = false;
+ const char dummy_test_password[] = "dummy test password";
+ const char helper_directory[] = "/nonexistent";
+ const char *const argv[] = { "/bin/echo", "-n", dummy_test_password,
+ NULL };
+
+ const bool success = start_mandos_client(queue, epoll_fd,
+ &mandos_client_exited,
+ &quit_now, &password,
+ &password_is_read,
+ &fixture->orig_sigaction,
+ fixture->orig_sigmask,
+ helper_directory, 0, 0,
+ argv);
+ g_assert_true(success);
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ do {
+ queue->next_run = 0;
+ string_set cancelled_filenames = {};
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while(((queue->length) > 0)
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ g_assert_true(mandos_client_exited);
+
+ g_assert_true(password_is_read);
+ g_assert_cmpint((int)password.length, ==,
+ sizeof(dummy_test_password)-1);
+ g_assert_nonnull(password.data);
+ g_assert_cmpint(memcmp(dummy_test_password, password.data,
+ sizeof(dummy_test_password)-1), ==, 0);
+}
+
+static
+void test_start_mandos_client_helper_directory(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ bool password_is_read = false;
+ const char helper_directory[] = "/nonexistent";
+ const char *const argv[] = { "/bin/sh", "-c",
+ "echo -n ${MANDOSPLUGINHELPERDIR}", NULL };
+
+ const bool success = start_mandos_client(queue, epoll_fd,
+ &mandos_client_exited,
+ &quit_now, &password,
+ &password_is_read,
+ &fixture->orig_sigaction,
+ fixture->orig_sigmask,
+ helper_directory, 0, 0,
+ argv);
+ g_assert_true(success);
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ do {
+ queue->next_run = 0;
+ string_set cancelled_filenames = {};
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while(((queue->length) > 0)
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ g_assert_true(mandos_client_exited);
+
+ g_assert_true(password_is_read);
+ g_assert_cmpint((int)password.length, ==,
+ sizeof(helper_directory)-1);
+ g_assert_nonnull(password.data);
+ g_assert_cmpint(memcmp(helper_directory, password.data,
+ sizeof(helper_directory)-1), ==, 0);
+}
+
+__attribute__((nonnull, warn_unused_result))
+static bool proc_status_sigblk_to_sigset(const char *const,
+ sigset_t *const);
+
+static void test_start_mandos_client_sigmask(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ bool password_is_read = false;
+ const char helper_directory[] = "/nonexistent";
+ /* see proc(5) for format of /proc/self/status */
+ const char *const argv[] = { "/usr/bin/awk",
+ "$1==\"SigBlk:\"{ print $2 }", "/proc/self/status", NULL };
+
+ g_assert_true(start_mandos_client(queue, epoll_fd,
+ &mandos_client_exited, &quit_now,
+ &password, &password_is_read,
+ &fixture->orig_sigaction,
+ fixture->orig_sigmask,
+ helper_directory, 0, 0, argv));
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ do {
+ queue->next_run = 0;
+ string_set cancelled_filenames = {};
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while((not (mandos_client_exited and password_is_read))
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+ g_assert_true(mandos_client_exited);
+ g_assert_true(password_is_read);
+
+ sigset_t parsed_sigmask;
+ g_assert_true(proc_status_sigblk_to_sigset(password.data,
+ &parsed_sigmask));
+
+ for(int signum = 1; signum < NSIG; signum++){
+ const bool has_signal = sigismember(&parsed_sigmask, signum);
+ if(sigismember(&fixture->orig_sigmask, signum)){
+ g_assert_true(has_signal);
+ } else {
+ g_assert_false(has_signal);
+ }
+ }
+}
+
+__attribute__((nonnull, warn_unused_result))
+static bool proc_status_sigblk_to_sigset(const char *const sigblk,
+ sigset_t *const sigmask){
+ /* parse /proc/PID/status SigBlk value and convert to a sigset_t */
+ uintmax_t scanned_sigmask;
+ if(sscanf(sigblk, "%" SCNxMAX " ", &scanned_sigmask) != 1){
+ return false;
+ }
+ if(sigemptyset(sigmask) != 0){
+ return false;
+ }
+ for(int signum = 1; signum < NSIG; signum++){
+ if(scanned_sigmask & ((uintmax_t)1 << (signum-1))){
+ if(sigaddset(sigmask, signum) != 0){
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+static void run_task_with_stderr_to_dev_null(const task_context task,
+ task_queue *const queue);
+
+static
+void test_wait_for_mandos_client_exit_badpid(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ const task_context task = {
+ .func=wait_for_mandos_client_exit,
+ .pid=1,
+ .mandos_client_exited=&mandos_client_exited,
+ .quit_now=&quit_now,
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+
+ g_assert_false(mandos_client_exited);
+ g_assert_true(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+}
+
+static void run_task_with_stderr_to_dev_null(const task_context task,
+ task_queue *const queue){
+ FILE *real_stderr = stderr;
+ FILE *devnull = fopen("/dev/null", "we");
+ g_assert_nonnull(devnull);
+
+ stderr = devnull;
+ task.func(task, queue);
+ stderr = real_stderr;
+
+ g_assert_cmpint(fclose(devnull), ==, 0);
+}
+
+static
+void test_wait_for_mandos_client_exit_noexit(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+
+ pid_t create_eternal_process(void){
+ const pid_t pid = fork();
+ if(pid == 0){ /* Child */
+ if(not restore_signal_handler(&fixture->orig_sigaction)){
+ _exit(EXIT_FAILURE);
+ }
+ if(not restore_sigmask(&fixture->orig_sigmask)){
+ _exit(EXIT_FAILURE);
+ }
+ while(true){
+ pause();
+ }
+ }
+ return pid;
+ }
+ pid_t pid = create_eternal_process();
+ g_assert_true(pid != -1);
+
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ const task_context task = {
+ .func=wait_for_mandos_client_exit,
+ .pid=pid,
+ .mandos_client_exited=&mandos_client_exited,
+ .quit_now=&quit_now,
+ };
+ task.func(task, queue);
+
+ g_assert_false(mandos_client_exited);
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=wait_for_mandos_client_exit,
+ .pid=task.pid,
+ .mandos_client_exited=&mandos_client_exited,
+ .quit_now=&quit_now,
+ }));
+}
+
+static
+void test_wait_for_mandos_client_exit_success(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+
+ pid_t create_successful_process(void){
+ const pid_t pid = fork();
+ if(pid == 0){ /* Child */
+ if(not restore_signal_handler(&fixture->orig_sigaction)){
+ _exit(EXIT_FAILURE);
+ }
+ if(not restore_sigmask(&fixture->orig_sigmask)){
+ _exit(EXIT_FAILURE);
+ }
+ exit(EXIT_SUCCESS);
+ }
+ return pid;
+ }
+ const pid_t pid = create_successful_process();
+ g_assert_true(pid != -1);
+
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ const task_context initial_task = {
+ .func=wait_for_mandos_client_exit,
+ .pid=pid,
+ .mandos_client_exited=&mandos_client_exited,
+ .quit_now=&quit_now,
+ };
+ g_assert_true(add_to_queue(queue, initial_task));
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ do {
+ queue->next_run = 0;
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ g_assert_true(run_queue(&queue, (string_set[]){{}}, &quit_now));
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while((not mandos_client_exited)
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+
+ g_assert_true(mandos_client_exited);
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+}
+
+static
+void test_wait_for_mandos_client_exit_failure(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+
+ pid_t create_failing_process(void){
+ const pid_t pid = fork();
+ if(pid == 0){ /* Child */
+ if(not restore_signal_handler(&fixture->orig_sigaction)){
+ _exit(EXIT_FAILURE);
+ }
+ if(not restore_sigmask(&fixture->orig_sigmask)){
+ _exit(EXIT_FAILURE);
+ }
+ exit(EXIT_FAILURE);
+ }
+ return pid;
+ }
+ const pid_t pid = create_failing_process();
+ g_assert_true(pid != -1);
+
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ g_assert_true(add_to_queue(queue, (task_context){
+ .func=wait_for_mandos_client_exit,
+ .pid=pid,
+ .mandos_client_exited=&mandos_client_exited,
+ .quit_now=&quit_now,
+ }));
+
+ g_assert_true(sigismember(&fixture->orig_sigmask, SIGCHLD) == 0);
+
+ __attribute__((cleanup(cleanup_close)))
+ const int devnull_fd = open("/dev/null",
+ O_WRONLY | O_CLOEXEC | O_NOCTTY);
+ g_assert_cmpint(devnull_fd, >=, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int real_stderr_fd = dup(STDERR_FILENO);
+ g_assert_cmpint(real_stderr_fd, >=, 0);
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ do {
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ dup2(devnull_fd, STDERR_FILENO);
+ const bool success = run_queue(&queue, &cancelled_filenames,
+ &quit_now);
+ dup2(real_stderr_fd, STDERR_FILENO);
+ if(not success){
+ break;
+ }
+
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while((not mandos_client_exited)
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+
+ g_assert_true(quit_now);
+ g_assert_true(mandos_client_exited);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+}
+
+static
+void test_wait_for_mandos_client_exit_killed(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ bool mandos_client_exited = false;
+ bool quit_now = false;
+
+ pid_t create_killed_process(void){
+ const pid_t pid = fork();
+ if(pid == 0){ /* Child */
+ if(not restore_signal_handler(&fixture->orig_sigaction)){
+ _exit(EXIT_FAILURE);
+ }
+ if(not restore_sigmask(&fixture->orig_sigmask)){
+ _exit(EXIT_FAILURE);
+ }
+ while(true){
+ pause();
+ }
+ }
+ kill(pid, SIGKILL);
+ return pid;
+ }
+ const pid_t pid = create_killed_process();
+ g_assert_true(pid != -1);
+
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ g_assert_true(add_to_queue(queue, (task_context){
+ .func=wait_for_mandos_client_exit,
+ .pid=pid,
+ .mandos_client_exited=&mandos_client_exited,
+ .quit_now=&quit_now,
+ }));
+
+ __attribute__((cleanup(cleanup_close)))
+ const int devnull_fd = open("/dev/null",
+ O_WRONLY | O_CLOEXEC, O_NOCTTY);
+ g_assert_cmpint(devnull_fd, >=, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int real_stderr_fd = dup(STDERR_FILENO);
+ g_assert_cmpint(real_stderr_fd, >=, 0);
+
+ struct timespec starttime, currtime;
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &starttime) == 0);
+ do {
+ g_assert_true(wait_for_event(epoll_fd, queue->next_run, 0));
+ dup2(devnull_fd, STDERR_FILENO);
+ const bool success = run_queue(&queue, &cancelled_filenames,
+ &quit_now);
+ dup2(real_stderr_fd, STDERR_FILENO);
+ if(not success){
+ break;
+ }
+
+ g_assert_true(clock_gettime(CLOCK_MONOTONIC, &currtime) == 0);
+ } while((not mandos_client_exited)
+ and (not quit_now)
+ and ((currtime.tv_sec - starttime.tv_sec) < 10));
+
+ g_assert_true(mandos_client_exited);
+ g_assert_true(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+}
+
+static bool epoll_set_does_not_contain(int, int);
+
+static
+void test_read_mandos_client_output_readerror(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+
+ /* Reading /proc/self/mem from offset 0 will always give EIO */
+ const int fd = open("/proc/self/mem",
+ O_RDONLY | O_CLOEXEC | O_NOCTTY);
+
+ bool password_is_read = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=fd,
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+ g_assert_false(password_is_read);
+ g_assert_cmpint((int)password.length, ==, 0);
+ g_assert_true(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_true(epoll_set_does_not_contain(epoll_fd, fd));
+
+ g_assert_cmpint(close(fd), ==, -1);
+}
+
+static bool epoll_set_does_not_contain(int epoll_fd, int fd){
+ return not epoll_set_contains(epoll_fd, fd, 0);
+}
+
+static
+void test_read_mandos_client_output_nodata(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+
+ bool password_is_read = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ };
+ task.func(task, queue);
+ g_assert_false(password_is_read);
+ g_assert_cmpint((int)password.length, ==, 0);
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+}
+
+static void test_read_mandos_client_output_eof(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+
+ bool password_is_read = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ };
+ task.func(task, queue);
+ g_assert_true(password_is_read);
+ g_assert_cmpint((int)password.length, ==, 0);
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_true(epoll_set_does_not_contain(epoll_fd, pipefds[0]));
+
+ g_assert_cmpint(close(pipefds[0]), ==, -1);
+}
+
+static
+void test_read_mandos_client_output_once(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ const char dummy_test_password[] = "dummy test password";
+ /* Start with a pre-allocated buffer */
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {
+ .data=malloc(sizeof(dummy_test_password)),
+ .length=0,
+ .allocated=sizeof(dummy_test_password),
+ };
+ g_assert_nonnull(password.data);
+ if(mlock(password.data, password.allocated) != 0){
+ g_assert_true(errno == EPERM or errno == ENOMEM);
+ }
+
+ bool password_is_read = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ g_assert_true(sizeof(dummy_test_password) <= PIPE_BUF);
+ g_assert_cmpint((int)write(pipefds[1], dummy_test_password,
+ sizeof(dummy_test_password)),
+ ==, (int)sizeof(dummy_test_password));
+
+ task_context task = {
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ };
+ task.func(task, queue);
+
+ g_assert_false(password_is_read);
+ g_assert_cmpint((int)password.length, ==,
+ (int)sizeof(dummy_test_password));
+ g_assert_nonnull(password.data);
+ g_assert_cmpint(memcmp(password.data, dummy_test_password,
+ sizeof(dummy_test_password)), ==, 0);
+
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+}
+
+static
+void test_read_mandos_client_output_malloc(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ const char dummy_test_password[] = "dummy test password";
+ /* Start with an empty buffer */
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+
+ bool password_is_read = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ g_assert_true(sizeof(dummy_test_password) <= PIPE_BUF);
+ g_assert_cmpint((int)write(pipefds[1], dummy_test_password,
+ sizeof(dummy_test_password)),
+ ==, (int)sizeof(dummy_test_password));
+
+ task_context task = {
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ };
+ task.func(task, queue);
+
+ g_assert_false(password_is_read);
+ g_assert_cmpint((int)password.length, ==,
+ (int)sizeof(dummy_test_password));
+ g_assert_nonnull(password.data);
+ g_assert_cmpint(memcmp(password.data, dummy_test_password,
+ sizeof(dummy_test_password)), ==, 0);
+
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+}
+
+static
+void test_read_mandos_client_output_append(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ const char dummy_test_password[] = "dummy test password";
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {
+ .data=malloc(PIPE_BUF),
+ .length=PIPE_BUF,
+ .allocated=PIPE_BUF,
+ };
+ g_assert_nonnull(password.data);
+ if(mlock(password.data, password.allocated) != 0){
+ g_assert_true(errno == EPERM or errno == ENOMEM);
+ }
+
+ memset(password.data, 'x', PIPE_BUF);
+ char password_expected[PIPE_BUF];
+ memcpy(password_expected, password.data, PIPE_BUF);
+
+ bool password_is_read = false;
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ g_assert_true(sizeof(dummy_test_password) <= PIPE_BUF);
+ g_assert_cmpint((int)write(pipefds[1], dummy_test_password,
+ sizeof(dummy_test_password)),
+ ==, (int)sizeof(dummy_test_password));
+
+ task_context task = {
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ };
+ task.func(task, queue);
+
+ g_assert_false(password_is_read);
+ g_assert_cmpint((int)password.length, ==,
+ PIPE_BUF + sizeof(dummy_test_password));
+ g_assert_nonnull(password.data);
+ g_assert_cmpint(memcmp(password_expected, password.data, PIPE_BUF),
+ ==, 0);
+ g_assert_cmpint(memcmp(password.data + PIPE_BUF,
+ dummy_test_password,
+ sizeof(dummy_test_password)), ==, 0);
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_mandos_client_output,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .password=&password,
+ .password_is_read=&password_is_read,
+ .quit_now=&quit_now,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+}
+
+static char *make_temporary_directory(void);
+
+static void test_add_inotify_dir_watch(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+
+ g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now,
+ &password, tempdir,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read));
+
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ const task_context *const added_read_task
+ = find_matching_task(queue, (task_context){
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=tempdir,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ });
+ g_assert_nonnull(added_read_task);
+
+ g_assert_cmpint(added_read_task->fd, >, 2);
+ g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd));
+ g_assert_true(epoll_set_contains(added_read_task->epoll_fd,
+ added_read_task->fd,
+ EPOLLIN | EPOLLRDHUP));
+
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static char *make_temporary_directory(void){
+ char *name = strdup("/tmp/mandosXXXXXX");
+ g_assert_nonnull(name);
+ char *result = mkdtemp(name);
+ if(result == NULL){
+ free(name);
+ }
+ return result;
+}
+
+static void test_add_inotify_dir_watch_fail(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+
+ const char nonexistent_dir[] = "/nonexistent";
+
+ FILE *real_stderr = stderr;
+ FILE *devnull = fopen("/dev/null", "we");
+ g_assert_nonnull(devnull);
+ stderr = devnull;
+ g_assert_false(add_inotify_dir_watch(queue, epoll_fd, &quit_now,
+ &password, nonexistent_dir,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read));
+ stderr = real_stderr;
+ g_assert_cmpint(fclose(devnull), ==, 0);
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+}
+
+static void test_add_inotify_dir_watch_nondir(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+
+ const char not_a_directory[] = "/dev/tty";
+
+ FILE *real_stderr = stderr;
+ FILE *devnull = fopen("/dev/null", "we");
+ g_assert_nonnull(devnull);
+ stderr = devnull;
+ g_assert_false(add_inotify_dir_watch(queue, epoll_fd, &quit_now,
+ &password, not_a_directory,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read));
+ stderr = real_stderr;
+ g_assert_cmpint(fclose(devnull), ==, 0);
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+}
+
+static void test_add_inotify_dir_watch_EAGAIN(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+
+ g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now,
+ &password, tempdir,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read));
+
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ const task_context *const added_read_task
+ = find_matching_task(queue,
+ (task_context){ .func=read_inotify_event });
+ g_assert_nonnull(added_read_task);
+
+ g_assert_cmpint(added_read_task->fd, >, 2);
+ g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd));
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ struct inotify_event *ievent = malloc(ievent_size);
+ g_assert_nonnull(ievent);
+
+ g_assert_cmpint(read(added_read_task->fd, ievent, ievent_size), ==,
+ -1);
+ g_assert_cmpint(errno, ==, EAGAIN);
+
+ free(ievent);
+
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static char *make_temporary_file_in_directory(const char
+ *const dir);
+
+static
+void test_add_inotify_dir_watch_IN_CLOSE_WRITE(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+
+ g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now,
+ &password, tempdir,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read));
+
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ const task_context *const added_read_task
+ = find_matching_task(queue,
+ (task_context){ .func=read_inotify_event });
+ g_assert_nonnull(added_read_task);
+
+ g_assert_cmpint(added_read_task->fd, >, 2);
+ g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd));
+
+ __attribute__((cleanup(cleanup_string)))
+ char *filename = make_temporary_file_in_directory(tempdir);
+ g_assert_nonnull(filename);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ struct inotify_event *ievent = malloc(ievent_size);
+ g_assert_nonnull(ievent);
+
+ ssize_t read_size = 0;
+ read_size = read(added_read_task->fd, ievent, ievent_size);
+
+ g_assert_cmpint((int)read_size, >, 0);
+ g_assert_true(ievent->mask & IN_CLOSE_WRITE);
+ g_assert_cmpstr(ievent->name, ==, basename(filename));
+
+ free(ievent);
+
+ g_assert_cmpint(unlink(filename), ==, 0);
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static char *make_temporary_prefixed_file_in_directory(const char
+ *const prefix,
+ const char
+ *const dir){
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%sXXXXXX", dir, prefix),
+ >, 0);
+ g_assert_nonnull(filename);
+ const int fd = mkostemp(filename, O_CLOEXEC);
+ g_assert_cmpint(fd, >=, 0);
+ g_assert_cmpint(close(fd), ==, 0);
+ return filename;
+}
+
+static char *make_temporary_file_in_directory(const char
+ *const dir){
+ return make_temporary_prefixed_file_in_directory("temp", dir);
+}
+
+static
+void test_add_inotify_dir_watch_IN_MOVED_TO(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *watchdir = make_temporary_directory();
+ g_assert_nonnull(watchdir);
+
+ g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now,
+ &password, watchdir,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read));
+
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ const task_context *const added_read_task
+ = find_matching_task(queue,
+ (task_context){ .func=read_inotify_event });
+ g_assert_nonnull(added_read_task);
+
+ g_assert_cmpint(added_read_task->fd, >, 2);
+ g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd));
+
+ char *sourcedir = make_temporary_directory();
+ g_assert_nonnull(sourcedir);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *filename = make_temporary_file_in_directory(sourcedir);
+ g_assert_nonnull(filename);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *targetfilename = NULL;
+ g_assert_cmpint(asprintf(&targetfilename, "%s/%s", watchdir,
+ basename(filename)), >, 0);
+ g_assert_nonnull(targetfilename);
+
+ g_assert_cmpint(rename(filename, targetfilename), ==, 0);
+ g_assert_cmpint(rmdir(sourcedir), ==, 0);
+ free(sourcedir);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ struct inotify_event *ievent = malloc(ievent_size);
+ g_assert_nonnull(ievent);
+
+ ssize_t read_size = read(added_read_task->fd, ievent, ievent_size);
+
+ g_assert_cmpint((int)read_size, >, 0);
+ g_assert_true(ievent->mask & IN_MOVED_TO);
+ g_assert_cmpstr(ievent->name, ==, basename(targetfilename));
+
+ free(ievent);
+
+ g_assert_cmpint(unlink(targetfilename), ==, 0);
+ g_assert_cmpint(rmdir(watchdir), ==, 0);
+}
+
+static
+void test_add_inotify_dir_watch_IN_MOVED_FROM(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename = make_temporary_file_in_directory(tempdir);
+ g_assert_nonnull(tempfilename);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *targetdir = make_temporary_directory();
+ g_assert_nonnull(targetdir);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *targetfilename = NULL;
+ g_assert_cmpint(asprintf(&targetfilename, "%s/%s", targetdir,
+ basename(tempfilename)), >, 0);
+ g_assert_nonnull(targetfilename);
+
+ g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now,
+ &password, tempdir,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read));
+
+ g_assert_cmpint(rename(tempfilename, targetfilename), ==, 0);
+
+ const task_context *const added_read_task
+ = find_matching_task(queue,
+ (task_context){ .func=read_inotify_event });
+ g_assert_nonnull(added_read_task);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ struct inotify_event *ievent = malloc(ievent_size);
+ g_assert_nonnull(ievent);
+
+ ssize_t read_size = read(added_read_task->fd, ievent, ievent_size);
+
+ g_assert_cmpint((int)read_size, >, 0);
+ g_assert_true(ievent->mask & IN_MOVED_FROM);
+ g_assert_cmpstr(ievent->name, ==, basename(tempfilename));
+
+ free(ievent);
+
+ g_assert_cmpint(unlink(targetfilename), ==, 0);
+ g_assert_cmpint(rmdir(targetdir), ==, 0);
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static
+void test_add_inotify_dir_watch_IN_DELETE(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfile = make_temporary_file_in_directory(tempdir);
+ g_assert_nonnull(tempfile);
+
+ g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now,
+ &password, tempdir,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read));
+ g_assert_cmpint(unlink(tempfile), ==, 0);
+
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ const task_context *const added_read_task
+ = find_matching_task(queue,
+ (task_context){ .func=read_inotify_event });
+ g_assert_nonnull(added_read_task);
+
+ g_assert_cmpint(added_read_task->fd, >, 2);
+ g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd));
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ struct inotify_event *ievent = malloc(ievent_size);
+ g_assert_nonnull(ievent);
+
+ ssize_t read_size = 0;
+ read_size = read(added_read_task->fd, ievent, ievent_size);
+
+ g_assert_cmpint((int)read_size, >, 0);
+ g_assert_true(ievent->mask & IN_DELETE);
+ g_assert_cmpstr(ievent->name, ==, basename(tempfile));
+
+ free(ievent);
+
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static
+void test_add_inotify_dir_watch_IN_EXCL_UNLINK(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfile = make_temporary_file_in_directory(tempdir);
+ g_assert_nonnull(tempfile);
+ int tempfile_fd = open(tempfile, O_WRONLY | O_CLOEXEC | O_NOCTTY
+ | O_NOFOLLOW);
+ g_assert_cmpint(tempfile_fd, >, 2);
+
+ g_assert_true(add_inotify_dir_watch(queue, epoll_fd, &quit_now,
+ &password, tempdir,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read));
+ g_assert_cmpint(unlink(tempfile), ==, 0);
+
+ g_assert_cmpuint((unsigned int)queue->length, >, 0);
+
+ const task_context *const added_read_task
+ = find_matching_task(queue,
+ (task_context){ .func=read_inotify_event });
+ g_assert_nonnull(added_read_task);
+
+ g_assert_cmpint(added_read_task->fd, >, 2);
+ g_assert_true(fd_has_cloexec_and_nonblock(added_read_task->fd));
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ struct inotify_event *ievent = malloc(ievent_size);
+ g_assert_nonnull(ievent);
+
+ ssize_t read_size = 0;
+ read_size = read(added_read_task->fd, ievent, ievent_size);
+
+ g_assert_cmpint((int)read_size, >, 0);
+ g_assert_true(ievent->mask & IN_DELETE);
+ g_assert_cmpstr(ievent->name, ==, basename(tempfile));
+
+ g_assert_cmpint(close(tempfile_fd), ==, 0);
+
+ /* IN_EXCL_UNLINK should make the closing of the previously unlinked
+ file not appear as an ievent, so we should not see it now. */
+ read_size = read(added_read_task->fd, ievent, ievent_size);
+ g_assert_cmpint((int)read_size, ==, -1);
+ g_assert_true(errno == EAGAIN);
+
+ free(ievent);
+
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static void test_read_inotify_event_readerror(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ const mono_microsecs current_time = 0;
+
+ /* Reading /proc/self/mem from offset 0 will always result in EIO */
+ const int fd = open("/proc/self/mem",
+ O_RDONLY | O_CLOEXEC | O_NOCTTY);
+
+ bool quit_now = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=fd,
+ .quit_now=&quit_now,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames = &(string_set){},
+ .notafter=0,
+ .current_time=¤t_time,
+ };
+ g_assert_nonnull(task.filename);
+ run_task_with_stderr_to_dev_null(task, queue);
+ g_assert_true(quit_now);
+ g_assert_true(queue->next_run == 0);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_true(epoll_set_does_not_contain(epoll_fd, fd));
+
+ g_assert_cmpint(close(fd), ==, -1);
+}
+
+static void test_read_inotify_event_bad_epoll(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ const mono_microsecs current_time = 17;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+ const int epoll_fd = pipefds[0]; /* This will obviously fail */
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames = &(string_set){},
+ .notafter=0,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ g_assert_nonnull(task.filename);
+ run_task_with_stderr_to_dev_null(task, queue);
+
+ g_assert_nonnull(find_matching_task(queue, task));
+ g_assert_true(queue->next_run == 1000000 + current_time);
+
+ g_assert_cmpint(close(pipefds[0]), ==, 0);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+}
+
+static void test_read_inotify_event_nodata(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ const mono_microsecs current_time = 0;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames = &(string_set){},
+ .notafter=0,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ g_assert_nonnull(task.filename);
+ task.func(task, queue);
+ g_assert_false(quit_now);
+ g_assert_true(queue->next_run == 0);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=task.filename,
+ .cancelled_filenames=task.cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+}
+
+static void test_read_inotify_event_eof(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ const mono_microsecs current_time = 0;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+
+ bool quit_now = false;
+ buffer password = {};
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames = &(string_set){},
+ .notafter=0,
+ .current_time=¤t_time,
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+ g_assert_true(quit_now);
+ g_assert_true(queue->next_run == 0);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_true(epoll_set_does_not_contain(epoll_fd, pipefds[0]));
+
+ g_assert_cmpint(close(pipefds[0]), ==, -1);
+}
+
+static
+void test_read_inotify_event_IN_CLOSE_WRITE(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ const mono_microsecs current_time = 0;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_max_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ g_assert_cmpint(ievent_max_size, <=, PIPE_BUF);
+ struct {
+ struct inotify_event event;
+ char name_buffer[NAME_MAX + 1];
+ } ievent_buffer;
+ struct inotify_event *const ievent = &ievent_buffer.event;
+
+ const char dummy_file_name[] = "ask.dummy_file_name";
+ ievent->mask = IN_CLOSE_WRITE;
+ ievent->len = sizeof(dummy_file_name);
+ memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name));
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + sizeof(dummy_file_name));
+ g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
+ ==, ievent_size);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames = &(string_set){},
+ .notafter=0,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+ g_assert_false(quit_now);
+ g_assert_true(queue->next_run != 0);
+ g_assert_cmpuint((unsigned int)queue->length, >=, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=task.filename,
+ .cancelled_filenames=task.cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+
+ g_assert_cmpuint((unsigned int)queue->length, >=, 2);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename,
+ dummy_file_name), >, 0);
+ g_assert_nonnull(filename);
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=open_and_parse_question,
+ .epoll_fd=epoll_fd,
+ .filename=filename,
+ .question_filename=filename,
+ .password=&password,
+ .cancelled_filenames=task.cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+}
+
+static
+void test_read_inotify_event_IN_MOVED_TO(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ const mono_microsecs current_time = 0;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_max_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ g_assert_cmpint(ievent_max_size, <=, PIPE_BUF);
+ struct {
+ struct inotify_event event;
+ char name_buffer[NAME_MAX + 1];
+ } ievent_buffer;
+ struct inotify_event *const ievent = &ievent_buffer.event;
+
+ const char dummy_file_name[] = "ask.dummy_file_name";
+ ievent->mask = IN_MOVED_TO;
+ ievent->len = sizeof(dummy_file_name);
+ memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name));
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + sizeof(dummy_file_name));
+ g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
+ ==, ievent_size);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames = &(string_set){},
+ .notafter=0,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+ g_assert_false(quit_now);
+ g_assert_true(queue->next_run != 0);
+ g_assert_cmpuint((unsigned int)queue->length, >=, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=task.filename,
+ .cancelled_filenames=task.cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+
+ g_assert_cmpuint((unsigned int)queue->length, >=, 2);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename,
+ dummy_file_name), >, 0);
+ g_assert_nonnull(filename);
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=open_and_parse_question,
+ .epoll_fd=epoll_fd,
+ .filename=filename,
+ .question_filename=filename,
+ .password=&password,
+ .cancelled_filenames=task.cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+}
+
+static
+void test_read_inotify_event_IN_MOVED_FROM(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_max_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ g_assert_cmpint(ievent_max_size, <=, PIPE_BUF);
+ struct {
+ struct inotify_event event;
+ char name_buffer[NAME_MAX + 1];
+ } ievent_buffer;
+ struct inotify_event *const ievent = &ievent_buffer.event;
+
+ const char dummy_file_name[] = "ask.dummy_file_name";
+ ievent->mask = IN_MOVED_FROM;
+ ievent->len = sizeof(dummy_file_name);
+ memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name));
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + sizeof(dummy_file_name));
+ g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
+ ==, ievent_size);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+ g_assert_false(quit_now);
+ g_assert_true(queue->next_run == 0);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=task.filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+
+ __attribute__((cleanup(cleanup_string)))
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename,
+ dummy_file_name), >, 0);
+ g_assert_nonnull(filename);
+ g_assert_true(string_set_contains(*task.cancelled_filenames,
+ filename));
+}
+
+static void test_read_inotify_event_IN_DELETE(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_max_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ g_assert_cmpint(ievent_max_size, <=, PIPE_BUF);
+ struct {
+ struct inotify_event event;
+ char name_buffer[NAME_MAX + 1];
+ } ievent_buffer;
+ struct inotify_event *const ievent = &ievent_buffer.event;
+
+ const char dummy_file_name[] = "ask.dummy_file_name";
+ ievent->mask = IN_DELETE;
+ ievent->len = sizeof(dummy_file_name);
+ memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name));
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + sizeof(dummy_file_name));
+ g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
+ ==, ievent_size);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+ g_assert_false(quit_now);
+ g_assert_true(queue->next_run == 0);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=task.filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+
+ __attribute__((cleanup(cleanup_string)))
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename,
+ dummy_file_name), >, 0);
+ g_assert_nonnull(filename);
+ g_assert_true(string_set_contains(*task.cancelled_filenames,
+ filename));
+}
+
+static void
+test_read_inotify_event_IN_CLOSE_WRITE_badname(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ const mono_microsecs current_time = 0;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_max_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ g_assert_cmpint(ievent_max_size, <=, PIPE_BUF);
+ struct {
+ struct inotify_event event;
+ char name_buffer[NAME_MAX + 1];
+ } ievent_buffer;
+ struct inotify_event *const ievent = &ievent_buffer.event;
+
+ const char dummy_file_name[] = "ignored.dummy_file_name";
+ ievent->mask = IN_CLOSE_WRITE;
+ ievent->len = sizeof(dummy_file_name);
+ memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name));
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + sizeof(dummy_file_name));
+ g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
+ ==, ievent_size);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames = &(string_set){},
+ .notafter=0,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+ g_assert_false(quit_now);
+ g_assert_true(queue->next_run == 0);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=task.filename,
+ .cancelled_filenames=task.cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+}
+
+static void
+test_read_inotify_event_IN_MOVED_TO_badname(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ const mono_microsecs current_time = 0;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_max_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ g_assert_cmpint(ievent_max_size, <=, PIPE_BUF);
+ struct {
+ struct inotify_event event;
+ char name_buffer[NAME_MAX + 1];
+ } ievent_buffer;
+ struct inotify_event *const ievent = &ievent_buffer.event;
+
+ const char dummy_file_name[] = "ignored.dummy_file_name";
+ ievent->mask = IN_MOVED_TO;
+ ievent->len = sizeof(dummy_file_name);
+ memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name));
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + sizeof(dummy_file_name));
+ g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
+ ==, ievent_size);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames = &(string_set){},
+ .notafter=0,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+ g_assert_false(quit_now);
+ g_assert_true(queue->next_run == 0);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=task.filename,
+ .cancelled_filenames=task.cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+}
+
+static void
+test_read_inotify_event_IN_MOVED_FROM_badname(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_max_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ g_assert_cmpint(ievent_max_size, <=, PIPE_BUF);
+ struct {
+ struct inotify_event event;
+ char name_buffer[NAME_MAX + 1];
+ } ievent_buffer;
+ struct inotify_event *const ievent = &ievent_buffer.event;
+
+ const char dummy_file_name[] = "ignored.dummy_file_name";
+ ievent->mask = IN_MOVED_FROM;
+ ievent->len = sizeof(dummy_file_name);
+ memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name));
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + sizeof(dummy_file_name));
+ g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
+ ==, ievent_size);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+ g_assert_false(quit_now);
+ g_assert_true(queue->next_run == 0);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=task.filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+
+ __attribute__((cleanup(cleanup_string)))
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename,
+ dummy_file_name), >, 0);
+ g_assert_nonnull(filename);
+ g_assert_false(string_set_contains(cancelled_filenames, filename));
+}
+
+static
+void test_read_inotify_event_IN_DELETE_badname(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+
+ /* "sufficient to read at least one event." - inotify(7) */
+ const size_t ievent_max_size = (sizeof(struct inotify_event)
+ + NAME_MAX + 1);
+ g_assert_cmpint(ievent_max_size, <=, PIPE_BUF);
+ struct {
+ struct inotify_event event;
+ char name_buffer[NAME_MAX + 1];
+ } ievent_buffer;
+ struct inotify_event *const ievent = &ievent_buffer.event;
+
+ const char dummy_file_name[] = "ignored.dummy_file_name";
+ ievent->mask = IN_DELETE;
+ ievent->len = sizeof(dummy_file_name);
+ memcpy(ievent->name, dummy_file_name, sizeof(dummy_file_name));
+ const size_t ievent_size = (sizeof(struct inotify_event)
+ + sizeof(dummy_file_name));
+ g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
+ ==, ievent_size);
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+
+ bool quit_now = false;
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ task_context task = {
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=strdup("/nonexistent"),
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+ g_assert_false(quit_now);
+ g_assert_true(queue->next_run == 0);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=read_inotify_event,
+ .epoll_fd=epoll_fd,
+ .fd=pipefds[0],
+ .quit_now=&quit_now,
+ .password=&password,
+ .filename=task.filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(epoll_set_contains(epoll_fd, pipefds[0],
+ EPOLLIN | EPOLLRDHUP));
+
+ __attribute__((cleanup(cleanup_string)))
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%s", task.filename,
+ dummy_file_name), >, 0);
+ g_assert_nonnull(filename);
+ g_assert_false(string_set_contains(cancelled_filenames, filename));
+}
+
+static
+void test_open_and_parse_question_ENOENT(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ char *const filename = strdup("/nonexistent");
+ g_assert_nonnull(filename);
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=(buffer[]){{}},
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=(mono_microsecs[]){0},
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+}
+
+static void test_open_and_parse_question_EIO(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ const mono_microsecs current_time = 0;
+
+ char *filename = strdup("/proc/self/mem");
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+}
+
+static void
+test_open_and_parse_question_parse_error(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename = strdup("/tmp/mandosXXXXXX");
+ g_assert_nonnull(tempfilename);
+ int tempfile = mkostemp(tempfilename, O_CLOEXEC);
+ g_assert_cmpint(tempfile, >, 0);
+ const char bad_data[] = "this is bad syntax\n";
+ g_assert_cmpint(write(tempfile, bad_data, sizeof(bad_data)),
+ ==, sizeof(bad_data));
+ g_assert_cmpint(close(tempfile), ==, 0);
+
+ char *const filename = strdup(tempfilename);
+ g_assert_nonnull(filename);
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=(buffer[]){{}},
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=(mono_microsecs[]){0},
+ .mandos_client_exited=(bool[]){false},
+ .password_is_read=(bool[]){false},
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_cmpint(unlink(tempfilename), ==, 0);
+}
+
+static
+void test_open_and_parse_question_nosocket(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename = strdup("/tmp/mandosXXXXXX");
+ g_assert_nonnull(tempfilename);
+ int questionfile = mkostemp(tempfilename, O_CLOEXEC);
+ g_assert_cmpint(questionfile, >, 0);
+ FILE *qf = fdopen(questionfile, "w");
+ g_assert_cmpint(fprintf(qf, "[Ask]\nPID=1\n"), >, 0);
+ g_assert_cmpint(fclose(qf), ==, 0);
+
+ char *const filename = strdup(tempfilename);
+ g_assert_nonnull(filename);
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=(buffer[]){{}},
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=(mono_microsecs[]){0},
+ .mandos_client_exited=(bool[]){false},
+ .password_is_read=(bool[]){false},
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_cmpint(unlink(tempfilename), ==, 0);
+}
+
+static
+void test_open_and_parse_question_badsocket(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename = strdup("/tmp/mandosXXXXXX");
+ g_assert_nonnull(tempfilename);
+ int questionfile = mkostemp(tempfilename, O_CLOEXEC);
+ g_assert_cmpint(questionfile, >, 0);
+ FILE *qf = fdopen(questionfile, "w");
+ g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=\nPID=1\n"), >, 0);
+ g_assert_cmpint(fclose(qf), ==, 0);
+
+ char *const filename = strdup(tempfilename);
+ g_assert_nonnull(filename);
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=(buffer[]){{}},
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=(mono_microsecs[]){0},
+ .mandos_client_exited=(bool[]){false},
+ .password_is_read=(bool[]){false},
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_cmpint(unlink(tempfilename), ==, 0);
+}
+
+static
+void test_open_and_parse_question_nopid(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename = strdup("/tmp/mandosXXXXXX");
+ g_assert_nonnull(tempfilename);
+ int questionfile = mkostemp(tempfilename, O_CLOEXEC);
+ g_assert_cmpint(questionfile, >, 0);
+ FILE *qf = fdopen(questionfile, "w");
+ g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\n"), >, 0);
+ g_assert_cmpint(fclose(qf), ==, 0);
+
+ char *const filename = strdup(tempfilename);
+ g_assert_nonnull(filename);
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=(buffer[]){{}},
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=(mono_microsecs[]){0},
+ .mandos_client_exited=(bool[]){false},
+ .password_is_read=(bool[]){false},
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_cmpint(unlink(tempfilename), ==, 0);
+}
+
+static
+void test_open_and_parse_question_badpid(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename = strdup("/tmp/mandosXXXXXX");
+ g_assert_nonnull(tempfilename);
+ int questionfile = mkostemp(tempfilename, O_CLOEXEC);
+ g_assert_cmpint(questionfile, >, 0);
+ FILE *qf = fdopen(questionfile, "w");
+ g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=\n"),
+ >, 0);
+ g_assert_cmpint(fclose(qf), ==, 0);
+
+ char *const filename = strdup(tempfilename);
+ g_assert_nonnull(filename);
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=(buffer[]){{}},
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=(mono_microsecs[]){0},
+ .mandos_client_exited=(bool[]){false},
+ .password_is_read=(bool[]){false},
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_cmpint(unlink(tempfilename), ==, 0);
+}
+
+static void
+test_open_and_parse_question_noexist_pid(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ const mono_microsecs current_time = 0;
+
+ /* Find value of sysctl kernel.pid_max */
+ uintmax_t pid_max = 0;
+ FILE *sysctl_pid_max = fopen("/proc/sys/kernel/pid_max", "r");
+ g_assert_nonnull(sysctl_pid_max);
+ g_assert_cmpint(fscanf(sysctl_pid_max, "%" PRIuMAX, &pid_max),
+ ==, 1);
+ g_assert_cmpint(fclose(sysctl_pid_max), ==, 0);
+
+ pid_t nonexisting_pid = ((pid_t)pid_max)+1;
+ g_assert_true(nonexisting_pid > 0); /* Overflow check */
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename = strdup("/tmp/mandosXXXXXX");
+ g_assert_nonnull(tempfilename);
+ int questionfile = mkostemp(tempfilename, O_CLOEXEC);
+ g_assert_cmpint(questionfile, >, 0);
+ FILE *qf = fdopen(questionfile, "w");
+ g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%"
+ PRIuMAX"\n", (uintmax_t)nonexisting_pid),
+ >, 0);
+ g_assert_cmpint(fclose(qf), ==, 0);
+
+ char *const question_filename = strdup(tempfilename);
+ g_assert_nonnull(question_filename);
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=question_filename,
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .filename=question_filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_cmpint(unlink(tempfilename), ==, 0);
+}
+
+static void
+test_open_and_parse_question_no_notafter(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ const mono_microsecs current_time = 0;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename = strdup("/tmp/mandosXXXXXX");
+ g_assert_nonnull(tempfilename);
+ int questionfile = mkostemp(tempfilename, O_CLOEXEC);
+ g_assert_cmpint(questionfile, >, 0);
+ FILE *qf = fdopen(questionfile, "w");
+ g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%"
+ PRIuMAX "\n", (uintmax_t)getpid()), >, 0);
+ g_assert_cmpint(fclose(qf), ==, 0);
+
+ char *const filename = strdup(tempfilename);
+ g_assert_nonnull(filename);
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *socket_filename = strdup("/nonexistent");
+ g_assert_nonnull(socket_filename);
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=connect_question_socket,
+ .question_filename=tempfilename,
+ .filename=socket_filename,
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(queue->next_run != 0);
+
+ g_assert_cmpint(unlink(tempfilename), ==, 0);
+}
+
+static void
+test_open_and_parse_question_bad_notafter(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ const mono_microsecs current_time = 0;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename = strdup("/tmp/mandosXXXXXX");
+ g_assert_nonnull(tempfilename);
+ int questionfile = mkostemp(tempfilename, O_CLOEXEC);
+ g_assert_cmpint(questionfile, >, 0);
+ FILE *qf = fdopen(questionfile, "w");
+ g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%"
+ PRIuMAX "\nNotAfter=\n",
+ (uintmax_t)getpid()), >, 0);
+ g_assert_cmpint(fclose(qf), ==, 0);
+
+ char *const filename = strdup(tempfilename);
+ g_assert_nonnull(filename);
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ run_task_with_stderr_to_dev_null(task, queue);
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ __attribute__((cleanup(cleanup_string)))
+ char *socket_filename = strdup("/nonexistent");
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=connect_question_socket,
+ .question_filename=tempfilename,
+ .filename=socket_filename,
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+ g_assert_true(queue->next_run != 0);
+
+ g_assert_cmpint(unlink(tempfilename), ==, 0);
+}
+
+static
+void assert_open_and_parse_question_with_notafter(const mono_microsecs
+ current_time,
+ const mono_microsecs
+ notafter,
+ const mono_microsecs
+ next_queue_run){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ queue->next_run = next_queue_run;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename = strdup("/tmp/mandosXXXXXX");
+ g_assert_nonnull(tempfilename);
+ int questionfile = mkostemp(tempfilename, O_CLOEXEC);
+ g_assert_cmpint(questionfile, >, 0);
+ FILE *qf = fdopen(questionfile, "w");
+ g_assert_cmpint(fprintf(qf, "[Ask]\nSocket=/nonexistent\nPID=%"
+ PRIuMAX "\nNotAfter=%" PRIuMAX "\n",
+ (uintmax_t)getpid(), notafter), >, 0);
+ g_assert_cmpint(fclose(qf), ==, 0);
+
+ char *const filename = strdup(tempfilename);
+ g_assert_nonnull(filename);
+ task_context task = {
+ .func=open_and_parse_question,
+ .question_filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ };
+ task.func(task, queue);
+
+ if(queue->length >= 1){
+ __attribute__((cleanup(cleanup_string)))
+ char *socket_filename = strdup("/nonexistent");
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=connect_question_socket,
+ .filename=socket_filename,
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .current_time=¤t_time,
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+ g_assert_true(queue->next_run != 0);
+ }
+
+ if(notafter == 0){
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+ } else if(current_time >= notafter) {
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ } else {
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=cancel_old_question,
+ .question_filename=tempfilename,
+ .filename=tempfilename,
+ .notafter=notafter,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ }));
+ }
+ g_assert_true(queue->next_run == 1);
+
+ g_assert_cmpint(unlink(tempfilename), ==, 0);
+}
+
+static void
+test_open_and_parse_question_notafter_0(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* current_time, notafter, next_queue_run */
+ assert_open_and_parse_question_with_notafter(0, 0, 0);
+}
+
+static void
+test_open_and_parse_question_notafter_1(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* current_time, notafter, next_queue_run */
+ assert_open_and_parse_question_with_notafter(0, 1, 0);
+}
+
+static void
+test_open_and_parse_question_notafter_1_1(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* current_time, notafter, next_queue_run */
+ assert_open_and_parse_question_with_notafter(0, 1, 1);
+}
+
+static void
+test_open_and_parse_question_notafter_1_2(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* current_time, notafter, next_queue_run */
+ assert_open_and_parse_question_with_notafter(0, 1, 2);
+}
+
+static void
+test_open_and_parse_question_equal_notafter(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* current_time, notafter, next_queue_run */
+ assert_open_and_parse_question_with_notafter(1, 1, 0);
+}
+
+static void
+test_open_and_parse_question_late_notafter(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* current_time, notafter, next_queue_run */
+ assert_open_and_parse_question_with_notafter(2, 1, 0);
+}
+
+static void assert_cancel_old_question_param(const mono_microsecs
+ next_queue_run,
+ const mono_microsecs
+ notafter,
+ const mono_microsecs
+ current_time,
+ const mono_microsecs
+ next_set_to){
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ queue->next_run = next_queue_run;
+
+ char *const question_filename = strdup("/nonexistent");
+ g_assert_nonnull(question_filename);
+ task_context task = {
+ .func=cancel_old_question,
+ .question_filename=question_filename,
+ .filename=question_filename,
+ .notafter=notafter,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ };
+ task.func(task, queue);
+
+ if(current_time >= notafter){
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ g_assert_true(string_set_contains(cancelled_filenames,
+ "/nonexistent"));
+ } else {
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=cancel_old_question,
+ .question_filename=question_filename,
+ .filename=question_filename,
+ .notafter=notafter,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ }));
+
+ g_assert_false(string_set_contains(cancelled_filenames,
+ question_filename));
+ }
+ g_assert_cmpuint((unsigned int)queue->next_run, ==,
+ (unsigned int)next_set_to);
+}
+
+static void test_cancel_old_question_0_1_2(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* next_queue_run unset,
+ cancellation should happen because time has come,
+ next_queue_run should be unchanged */
+ /* next_queue_run, notafter, current_time, next_set_to */
+ assert_cancel_old_question_param(0, 1, 2, 0);
+}
+
+static void test_cancel_old_question_0_2_1(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* If next_queue_run is 0, meaning unset, and notafter is 2,
+ and current_time is not yet notafter or greater,
+ update value of next_queue_run to value of notafter */
+ /* next_queue_run, notafter, current_time, next_set_to */
+ assert_cancel_old_question_param(0, 2, 1, 2);
+}
+
+static void test_cancel_old_question_1_2_3(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* next_queue_run 1,
+ cancellation should happen because time has come,
+ next_queue_run should be unchanged */
+ /* next_queue_run, notafter, current_time, next_set_to */
+ assert_cancel_old_question_param(1, 2, 3, 1);
+}
+
+static void test_cancel_old_question_1_3_2(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* If next_queue_run is set,
+ and current_time is not yet notafter or greater,
+ and notafter is larger than next_queue_run
+ next_queue_run should be unchanged */
+ /* next_queue_run, notafter, current_time, next_set_to */
+ assert_cancel_old_question_param(1, 3, 2, 1);
+}
+
+static void test_cancel_old_question_2_1_3(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* next_queue_run 2,
+ cancellation should happen because time has come,
+ next_queue_run should be unchanged */
+ /* next_queue_run, notafter, current_time, next_set_to */
+ assert_cancel_old_question_param(2, 1, 3, 2);
+}
+
+static void test_cancel_old_question_2_3_1(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* If next_queue_run is set,
+ and current_time is not yet notafter or greater,
+ and notafter is larger than next_queue_run
+ next_queue_run should be unchanged */
+ /* next_queue_run, notafter, current_time, next_set_to */
+ assert_cancel_old_question_param(2, 3, 1, 2);
+}
+
+static void test_cancel_old_question_3_1_2(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* next_queue_run 3,
+ cancellation should happen because time has come,
+ next_queue_run should be unchanged */
+ /* next_queue_run, notafter, current_time, next_set_to */
+ assert_cancel_old_question_param(3, 1, 2, 3);
+}
+
+static void test_cancel_old_question_3_2_1(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* If next_queue_run is set,
+ and current_time is not yet notafter or greater,
+ and notafter is smaller than next_queue_run
+ update value of next_queue_run to value of notafter */
+ /* next_queue_run, notafter, current_time, next_set_to */
+ assert_cancel_old_question_param(3, 2, 1, 2);
+}
+
+static void
+test_connect_question_socket_name_too_long(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ const char question_filename[] = "/nonexistent/question";
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+ struct sockaddr_un unix_socket = { .sun_family=AF_LOCAL };
+ char socket_name[sizeof(unix_socket.sun_path)];
+ memset(socket_name, 'x', sizeof(socket_name));
+ socket_name[sizeof(socket_name)-1] = '\0';
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name),
+ >, 0);
+ g_assert_nonnull(filename);
+
+ task_context task = {
+ .func=connect_question_socket,
+ .question_filename=strdup(question_filename),
+ .epoll_fd=epoll_fd,
+ .password=(buffer[]){{}},
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=(bool[]){false},
+ .password_is_read=(bool[]){false},
+ .current_time=(mono_microsecs[]){0},
+ };
+ g_assert_nonnull(task.question_filename);
+ run_task_with_stderr_to_dev_null(task, queue);
+
+ g_assert_true(string_set_contains(cancelled_filenames,
+ question_filename));
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ g_assert_true(queue->next_run == 0);
+
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static
+void test_connect_question_socket_connect_fail(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ const char question_filename[] = "/nonexistent/question";
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 3;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+ char socket_name[] = "nonexistent";
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name),
+ >, 0);
+ g_assert_nonnull(filename);
+
+ task_context task = {
+ .func=connect_question_socket,
+ .question_filename=strdup(question_filename),
+ .epoll_fd=epoll_fd,
+ .password=(buffer[]){{}},
+ .filename=filename,
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=(bool[]){false},
+ .password_is_read=(bool[]){false},
+ .current_time=¤t_time,
+ };
+ g_assert_nonnull(task.question_filename);
+ run_task_with_stderr_to_dev_null(task, queue);
+
+ g_assert_nonnull(find_matching_task(queue, task));
+
+ g_assert_false(string_set_contains(cancelled_filenames,
+ question_filename));
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+ g_assert_true(queue->next_run == 1000000 + current_time);
+
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static
+void test_connect_question_socket_bad_epoll(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = open("/dev/null",
+ O_WRONLY | O_CLOEXEC | O_NOCTTY);
+ __attribute__((cleanup(cleanup_string)))
+ char *const question_filename = strdup("/nonexistent/question");
+ g_assert_nonnull(question_filename);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 5;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+ __attribute__((cleanup(cleanup_close)))
+ const int sock_fd = socket(PF_LOCAL, SOCK_DGRAM
+ | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
+ g_assert_cmpint(sock_fd, >=, 0);
+ struct sockaddr_un sock_name = { .sun_family=AF_LOCAL };
+ const char socket_name[] = "socket_name";
+ __attribute__((cleanup(cleanup_string)))
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name),
+ >, 0);
+ g_assert_nonnull(filename);
+ g_assert_cmpint((int)strlen(filename), <,
+ (int)sizeof(sock_name.sun_path));
+ strncpy(sock_name.sun_path, filename, sizeof(sock_name.sun_path));
+ sock_name.sun_path[sizeof(sock_name.sun_path)-1] = '\0';
+ g_assert_cmpint((int)bind(sock_fd, (struct sockaddr *)&sock_name,
+ (socklen_t)SUN_LEN(&sock_name)), >=, 0);
+ task_context task = {
+ .func=connect_question_socket,
+ .question_filename=strdup(question_filename),
+ .epoll_fd=epoll_fd,
+ .password=(buffer[]){{}},
+ .filename=strdup(filename),
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=(bool[]){false},
+ .password_is_read=(bool[]){false},
+ .current_time=¤t_time,
+ };
+ g_assert_nonnull(task.question_filename);
+ run_task_with_stderr_to_dev_null(task, queue);
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+ const task_context *const added_task
+ = find_matching_task(queue, task);
+ g_assert_nonnull(added_task);
+ g_assert_true(queue->next_run == 1000000 + current_time);
+
+ g_assert_cmpint(unlink(filename), ==, 0);
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static
+void test_connect_question_socket_usable(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_string)))
+ char *const question_filename = strdup("/nonexistent/question");
+ g_assert_nonnull(question_filename);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ buffer password = {};
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ const mono_microsecs current_time = 0;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+ __attribute__((cleanup(cleanup_close)))
+ const int sock_fd = socket(PF_LOCAL, SOCK_DGRAM
+ | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
+ g_assert_cmpint(sock_fd, >=, 0);
+ struct sockaddr_un sock_name = { .sun_family=AF_LOCAL };
+ const char socket_name[] = "socket_name";
+ __attribute__((cleanup(cleanup_string)))
+ char *filename = NULL;
+ g_assert_cmpint(asprintf(&filename, "%s/%s", tempdir, socket_name),
+ >, 0);
+ g_assert_nonnull(filename);
+ g_assert_cmpint((int)strlen(filename), <,
+ (int)sizeof(sock_name.sun_path));
+ strncpy(sock_name.sun_path, filename, sizeof(sock_name.sun_path));
+ sock_name.sun_path[sizeof(sock_name.sun_path)-1] = '\0';
+ g_assert_cmpint((int)bind(sock_fd, (struct sockaddr *)&sock_name,
+ (socklen_t)SUN_LEN(&sock_name)), >=, 0);
+ task_context task = {
+ .func=connect_question_socket,
+ .question_filename=strdup(question_filename),
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .filename=strdup(filename),
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ .current_time=¤t_time,
+ };
+ g_assert_nonnull(task.question_filename);
+ task.func(task, queue);
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+ const task_context *const added_task
+ = find_matching_task(queue, (task_context){
+ .func=send_password_to_socket,
+ .question_filename=question_filename,
+ .filename=filename,
+ .epoll_fd=epoll_fd,
+ .password=&password,
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ .current_time=¤t_time,
+ });
+ g_assert_nonnull(added_task);
+ g_assert_cmpint(added_task->fd, >, 0);
+
+ g_assert_true(epoll_set_contains(epoll_fd, added_task->fd,
+ EPOLLOUT));
+
+ const int fd = added_task->fd;
+ g_assert_cmpint(fd, >, 0);
+ g_assert_true(fd_has_cloexec_and_nonblock(fd));
+
+ /* write to fd */
+ char write_data[PIPE_BUF];
+ {
+ /* Construct test password buffer */
+ /* Start with + since that is what the real procotol uses */
+ write_data[0] = '+';
+ /* Set a special character at string end just to mark the end */
+ write_data[sizeof(write_data)-2] = 'y';
+ /* Set NUL at buffer end, as suggested by the protocol */
+ write_data[sizeof(write_data)-1] = '\0';
+ /* Fill rest of password with 'x' */
+ memset(write_data+1, 'x', sizeof(write_data)-3);
+ g_assert_cmpint((int)send(fd, write_data, sizeof(write_data),
+ MSG_NOSIGNAL), ==, sizeof(write_data));
+ }
+
+ /* read from sock_fd */
+ char read_data[sizeof(write_data)];
+ g_assert_cmpint((int)read(sock_fd, read_data, sizeof(read_data)),
+ ==, sizeof(read_data));
+
+ g_assert_true(memcmp(write_data, read_data, sizeof(write_data))
+ == 0);
+
+ /* writing to sock_fd should fail */
+ g_assert_cmpint(send(sock_fd, write_data, sizeof(write_data),
+ MSG_NOSIGNAL), <, 0);
+
+ /* reading from fd should fail */
+ g_assert_cmpint((int)recv(fd, read_data, sizeof(read_data),
+ MSG_NOSIGNAL), <, 0);
+
+ g_assert_cmpint(unlink(filename), ==, 0);
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static void
+test_send_password_to_socket_client_not_exited(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_string)))
+ char *const question_filename = strdup("/nonexistent/question");
+ g_assert_nonnull(question_filename);
+ __attribute__((cleanup(cleanup_string)))
+ char *const filename = strdup("/nonexistent/socket");
+ g_assert_nonnull(filename);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ buffer password = {};
+ bool password_is_read = true;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ int socketfds[2];
+ g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM
+ | SOCK_NONBLOCK | SOCK_CLOEXEC, 0,
+ socketfds), ==, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int read_socket = socketfds[0];
+ const int write_socket = socketfds[1];
+ task_context task = {
+ .func=send_password_to_socket,
+ .question_filename=strdup(question_filename),
+ .filename=strdup(filename),
+ .epoll_fd=epoll_fd,
+ .fd=write_socket,
+ .password=&password,
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=(bool[]){false},
+ .password_is_read=&password_is_read,
+ .current_time=(mono_microsecs[]){0},
+ };
+ g_assert_nonnull(task.question_filename);
+
+ task.func(task, queue);
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ const task_context *const added_task
+ = find_matching_task(queue, task);
+ g_assert_nonnull(added_task);
+ g_assert_cmpuint((unsigned int)password.length, ==, 0);
+ g_assert_true(password_is_read);
+
+ g_assert_cmpint(added_task->fd, >, 0);
+ g_assert_true(epoll_set_contains(epoll_fd, added_task->fd,
+ EPOLLOUT));
+}
+
+static void
+test_send_password_to_socket_password_not_read(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_string)))
+ char *const question_filename = strdup("/nonexistent/question");
+ g_assert_nonnull(question_filename);
+ __attribute__((cleanup(cleanup_string)))
+ char *const filename = strdup("/nonexistent/socket");
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ buffer password = {};
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ int socketfds[2];
+ g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM
+ | SOCK_NONBLOCK | SOCK_CLOEXEC, 0,
+ socketfds), ==, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int read_socket = socketfds[0];
+ const int write_socket = socketfds[1];
+ task_context task = {
+ .func=send_password_to_socket,
+ .question_filename=strdup(question_filename),
+ .filename=strdup(filename),
+ .epoll_fd=epoll_fd,
+ .fd=write_socket,
+ .password=&password,
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=(bool[]){false},
+ .password_is_read=(bool[]){false},
+ .current_time=(mono_microsecs[]){0},
+ };
+ g_assert_nonnull(task.question_filename);
+
+ task.func(task, queue);
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ const task_context *const added_task = find_matching_task(queue,
+ task);
+ g_assert_nonnull(added_task);
+ g_assert_cmpuint((unsigned int)password.length, ==, 0);
+ g_assert_true(queue->next_run == 0);
+
+ g_assert_cmpint(added_task->fd, >, 0);
+ g_assert_true(epoll_set_contains(epoll_fd, added_task->fd,
+ EPOLLOUT));
+}
+
+static
+void test_send_password_to_socket_EMSGSIZE(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ const char question_filename[] = "/nonexistent/question";
+ char *const filename = strdup("/nonexistent/socket");
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const size_t oversized = 1024*1024; /* Limit seems to be 212960 */
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {
+ .data=malloc(oversized),
+ .length=oversized,
+ .allocated=oversized,
+ };
+ g_assert_nonnull(password.data);
+ if(mlock(password.data, password.allocated) != 0){
+ g_assert_true(errno == EPERM or errno == ENOMEM);
+ }
+ /* Construct test password buffer */
+ /* Start with + since that is what the real procotol uses */
+ password.data[0] = '+';
+ /* Set a special character at string end just to mark the end */
+ password.data[oversized-3] = 'y';
+ /* Set NUL at buffer end, as suggested by the protocol */
+ password.data[oversized-2] = '\0';
+ /* Fill rest of password with 'x' */
+ memset(password.data+1, 'x', oversized-3);
+
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ int socketfds[2];
+ g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM
+ | SOCK_NONBLOCK | SOCK_CLOEXEC, 0,
+ socketfds), ==, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int read_socket = socketfds[0];
+ __attribute__((cleanup(cleanup_close)))
+ const int write_socket = socketfds[1];
+ task_context task = {
+ .func=send_password_to_socket,
+ .question_filename=strdup(question_filename),
+ .filename=filename,
+ .epoll_fd=epoll_fd,
+ .fd=write_socket,
+ .password=&password,
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=(bool[]){true},
+ .password_is_read=(bool[]){true},
+ .current_time=(mono_microsecs[]){0},
+ };
+ g_assert_nonnull(task.question_filename);
+
+ run_task_with_stderr_to_dev_null(task, queue);
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+ g_assert_true(string_set_contains(cancelled_filenames,
+ question_filename));
+}
+
+static void test_send_password_to_socket_retry(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_string)))
+ char *const question_filename = strdup("/nonexistent/question");
+ g_assert_nonnull(question_filename);
+ __attribute__((cleanup(cleanup_string)))
+ char *const filename = strdup("/nonexistent/socket");
+ g_assert_nonnull(filename);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ int socketfds[2];
+ g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM
+ | SOCK_NONBLOCK | SOCK_CLOEXEC, 0,
+ socketfds), ==, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int read_socket = socketfds[0];
+ const int write_socket = socketfds[1];
+ /* Close the server side socket to force ECONNRESET on client */
+ g_assert_cmpint(close(read_socket), ==, 0);
+ task_context task = {
+ .func=send_password_to_socket,
+ .question_filename=strdup(question_filename),
+ .filename=strdup(filename),
+ .epoll_fd=epoll_fd,
+ .fd=write_socket,
+ .password=&password,
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=(bool[]){true},
+ .password_is_read=(bool[]){true},
+ .current_time=(mono_microsecs[]){0},
+ };
+ g_assert_nonnull(task.question_filename);
+
+ task.func(task, queue);
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ const task_context *const added_task = find_matching_task(queue,
+ task);
+ g_assert_nonnull(added_task);
+ g_assert_cmpuint((unsigned int)password.length, ==, 0);
+
+ g_assert_true(epoll_set_contains(epoll_fd, added_task->fd,
+ EPOLLOUT));
+}
+
+static
+void test_send_password_to_socket_bad_epoll(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = open("/dev/null",
+ O_WRONLY | O_CLOEXEC | O_NOCTTY);
+ __attribute__((cleanup(cleanup_string)))
+ char *const question_filename = strdup("/nonexistent/question");
+ g_assert_nonnull(question_filename);
+ __attribute__((cleanup(cleanup_string)))
+ char *const filename = strdup("/nonexistent/socket");
+ g_assert_nonnull(filename);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+
+ const mono_microsecs current_time = 11;
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ int socketfds[2];
+ g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM
+ | SOCK_NONBLOCK | SOCK_CLOEXEC, 0,
+ socketfds), ==, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int read_socket = socketfds[0];
+ const int write_socket = socketfds[1];
+ /* Close the server side socket to force ECONNRESET on client */
+ g_assert_cmpint(close(read_socket), ==, 0);
+ task_context task = {
+ .func=send_password_to_socket,
+ .question_filename=strdup(question_filename),
+ .filename=strdup(filename),
+ .epoll_fd=epoll_fd,
+ .fd=write_socket,
+ .password=&password,
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=(bool[]){true},
+ .password_is_read=(bool[]){true},
+ .current_time=¤t_time,
+ };
+ g_assert_nonnull(task.question_filename);
+
+ run_task_with_stderr_to_dev_null(task, queue);
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ const task_context *const added_task = find_matching_task(queue,
+ task);
+ g_assert_nonnull(added_task);
+ g_assert_true(queue->next_run == current_time + 1000000);
+ g_assert_cmpuint((unsigned int)password.length, ==, 0);
+}
+
+static void assert_send_password_to_socket_password(buffer password){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ char *const question_filename = strdup("/nonexistent/question");
+ g_assert_nonnull(question_filename);
+ char *const filename = strdup("/nonexistent/socket");
+ g_assert_nonnull(filename);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ int socketfds[2];
+ g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM
+ | SOCK_NONBLOCK | SOCK_CLOEXEC, 0,
+ socketfds), ==, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int read_socket = socketfds[0];
+ const int write_socket = socketfds[1];
+ task_context task = {
+ .func=send_password_to_socket,
+ .question_filename=question_filename,
+ .filename=filename,
+ .epoll_fd=epoll_fd,
+ .fd=write_socket,
+ .password=&password,
+ .cancelled_filenames=&cancelled_filenames,
+ .mandos_client_exited=(bool[]){true},
+ .password_is_read=(bool[]){true},
+ .current_time=(mono_microsecs[]){0},
+ };
+
+ char *expected_written_data = malloc(password.length + 2);
+ g_assert_nonnull(expected_written_data);
+ expected_written_data[0] = '+';
+ expected_written_data[password.length + 1] = '\0';
+ if(password.length > 0){
+ g_assert_nonnull(password.data);
+ memcpy(expected_written_data + 1, password.data, password.length);
+ }
+
+ task.func(task, queue);
+
+ char buf[PIPE_BUF];
+ g_assert_cmpint((int)read(read_socket, buf, PIPE_BUF), ==,
+ (int)(password.length + 2));
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_true(memcmp(expected_written_data, buf,
+ password.length + 2) == 0);
+
+ g_assert_true(epoll_set_does_not_contain(epoll_fd, write_socket));
+
+ free(expected_written_data);
+}
+
+static void
+test_send_password_to_socket_null_password(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ assert_send_password_to_socket_password(password);
+}
+
+static void
+test_send_password_to_socket_empty_password(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {
+ .data=malloc(1), /* because malloc(0) may return NULL */
+ .length=0,
+ .allocated=0, /* deliberate lie */
+ };
+ g_assert_nonnull(password.data);
+ assert_send_password_to_socket_password(password);
+}
+
+static void
+test_send_password_to_socket_empty_str_pass(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {
+ .data=strdup(""),
+ .length=0,
+ .allocated=1,
+ };
+ if(mlock(password.data, password.allocated) != 0){
+ g_assert_true(errno == EPERM or errno == ENOMEM);
+ }
+ assert_send_password_to_socket_password(password);
+}
+
+static void
+test_send_password_to_socket_text_password(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ const char dummy_test_password[] = "dummy test password";
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {
+ .data = strdup(dummy_test_password),
+ .length = strlen(dummy_test_password),
+ .allocated = sizeof(dummy_test_password),
+ };
+ if(mlock(password.data, password.allocated) != 0){
+ g_assert_true(errno == EPERM or errno == ENOMEM);
+ }
+ assert_send_password_to_socket_password(password);
+}
+
+static void
+test_send_password_to_socket_binary_password(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {
+ .data=malloc(255),
+ .length=255,
+ .allocated=255,
+ };
+ g_assert_nonnull(password.data);
+ if(mlock(password.data, password.allocated) != 0){
+ g_assert_true(errno == EPERM or errno == ENOMEM);
+ }
+ char c = 1; /* Start at 1, avoiding NUL */
+ for(int i=0; i < 255; i++){
+ password.data[i] = c++;
+ }
+ assert_send_password_to_socket_password(password);
+}
+
+static void
+test_send_password_to_socket_nuls_in_password(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ char test_password[] = {'\0', 'a', '\0', 'b', '\0', 'c', '\0'};
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {
+ .data=malloc(sizeof(test_password)),
+ .length=sizeof(test_password),
+ .allocated=sizeof(test_password),
+ };
+ g_assert_nonnull(password.data);
+ if(mlock(password.data, password.allocated) !=0){
+ g_assert_true(errno == EPERM or errno == ENOMEM);
+ }
+ memcpy(password.data, test_password, password.allocated);
+ assert_send_password_to_socket_password(password);
+}
+
+static bool assert_add_existing_questions_to_devnull(task_queue
+ *const,
+ const int,
+ buffer *const,
+ string_set *,
+ const
+ mono_microsecs
+ *const,
+ bool *const,
+ bool *const,
+ const char
+ *const);
+
+static void test_add_existing_questions_ENOENT(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+
+ g_assert_false(assert_add_existing_questions_to_devnull
+ (queue,
+ epoll_fd,
+ (buffer[]){{}}, /* password */
+ &cancelled_filenames,
+ (mono_microsecs[]){0}, /* current_time */
+ (bool[]){false}, /* mandos_client_exited */
+ (bool[]){false}, /* password_is_read */
+ "/nonexistent")); /* dirname */
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+}
+
+static
+bool assert_add_existing_questions_to_devnull(task_queue
+ *const queue,
+ const int
+ epoll_fd,
+ buffer *const
+ password,
+ string_set
+ *cancelled_filenames,
+ const mono_microsecs
+ *const current_time,
+ bool *const
+ mandos_client_exited,
+ bool *const
+ password_is_read,
+ const char *const
+ dirname){
+ __attribute__((cleanup(cleanup_close)))
+ const int devnull_fd = open("/dev/null",
+ O_WRONLY | O_CLOEXEC | O_NOCTTY);
+ g_assert_cmpint(devnull_fd, >=, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int real_stderr_fd = dup(STDERR_FILENO);
+ g_assert_cmpint(real_stderr_fd, >=, 0);
+ dup2(devnull_fd, STDERR_FILENO);
+ const bool ret = add_existing_questions(queue, epoll_fd, password,
+ cancelled_filenames,
+ current_time,
+ mandos_client_exited,
+ password_is_read, dirname);
+ dup2(real_stderr_fd, STDERR_FILENO);
+ return ret;
+}
+
+static
+void test_add_existing_questions_no_questions(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+
+ g_assert_false(assert_add_existing_questions_to_devnull
+ (queue,
+ epoll_fd,
+ (buffer[]){{}}, /* password */
+ &cancelled_filenames,
+ (mono_microsecs[]){0}, /* current_time */
+ (bool[]){false}, /* mandos_client_exited */
+ (bool[]){false}, /* password_is_read */
+ tempdir));
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static char *make_question_file_in_directory(const char *const);
+
+static
+void test_add_existing_questions_one_question(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+ __attribute__((cleanup(cleanup_string)))
+ char *question_filename
+ = make_question_file_in_directory(tempdir);
+ g_assert_nonnull(question_filename);
+
+ g_assert_true(assert_add_existing_questions_to_devnull
+ (queue,
+ epoll_fd,
+ &password,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read,
+ tempdir));
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=open_and_parse_question,
+ .epoll_fd=epoll_fd,
+ .filename=question_filename,
+ .question_filename=question_filename,
+ .password=&password,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(queue->next_run == 1);
+
+ g_assert_cmpint(unlink(question_filename), ==, 0);
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static char *make_question_file_in_directory(const char
+ *const dir){
+ return make_temporary_prefixed_file_in_directory("ask.", dir);
+}
+
+static
+void test_add_existing_questions_two_questions(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+ __attribute__((cleanup(cleanup_string)))
+ char *question_filename1
+ = make_question_file_in_directory(tempdir);
+ g_assert_nonnull(question_filename1);
+ __attribute__((cleanup(cleanup_string)))
+ char *question_filename2
+ = make_question_file_in_directory(tempdir);
+ g_assert_nonnull(question_filename2);
+
+ g_assert_true(assert_add_existing_questions_to_devnull
+ (queue,
+ epoll_fd,
+ &password,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read,
+ tempdir));
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 2);
+
+ g_assert_true(queue->next_run == 1);
+
+ __attribute__((cleanup(string_set_clear)))
+ string_set seen_questions = {};
+
+ bool queue_contains_question_opener(char *const question_filename){
+ return(find_matching_task(queue, (task_context){
+ .func=open_and_parse_question,
+ .epoll_fd=epoll_fd,
+ .question_filename=question_filename,
+ .password=&password,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }) != NULL);
+ }
+
+ g_assert_true(queue_contains_question_opener(question_filename1));
+ g_assert_true(queue_contains_question_opener(question_filename2));
+
+ g_assert_true(queue->next_run == 1);
+
+ g_assert_cmpint(unlink(question_filename1), ==, 0);
+ g_assert_cmpint(unlink(question_filename2), ==, 0);
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static void
+test_add_existing_questions_non_questions(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+ __attribute__((cleanup(cleanup_string)))
+ char *question_filename1
+ = make_temporary_file_in_directory(tempdir);
+ g_assert_nonnull(question_filename1);
+ __attribute__((cleanup(cleanup_string)))
+ char *question_filename2
+ = make_temporary_file_in_directory(tempdir);
+ g_assert_nonnull(question_filename2);
+
+ g_assert_false(assert_add_existing_questions_to_devnull
+ (queue,
+ epoll_fd,
+ (buffer[]){{}}, /* password */
+ &cancelled_filenames,
+ (mono_microsecs[]){0}, /* current_time */
+ (bool[]){false}, /* mandos_client_exited */
+ (bool[]){false}, /* password_is_read */
+ tempdir));
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 0);
+
+ g_assert_cmpint(unlink(question_filename1), ==, 0);
+ g_assert_cmpint(unlink(question_filename2), ==, 0);
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static void
+test_add_existing_questions_both_types(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ __attribute__((cleanup(cleanup_buffer)))
+ buffer password = {};
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ const mono_microsecs current_time = 0;
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+ __attribute__((cleanup(cleanup_string)))
+ char *tempdir = make_temporary_directory();
+ g_assert_nonnull(tempdir);
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename1 = make_temporary_file_in_directory(tempdir);
+ g_assert_nonnull(tempfilename1);
+ __attribute__((cleanup(cleanup_string)))
+ char *tempfilename2 = make_temporary_file_in_directory(tempdir);
+ g_assert_nonnull(tempfilename2);
+ __attribute__((cleanup(cleanup_string)))
+ char *question_filename
+ = make_question_file_in_directory(tempdir);
+ g_assert_nonnull(question_filename);
+
+ g_assert_true(assert_add_existing_questions_to_devnull
+ (queue,
+ epoll_fd,
+ &password,
+ &cancelled_filenames,
+ ¤t_time,
+ &mandos_client_exited,
+ &password_is_read,
+ tempdir));
+
+ g_assert_cmpuint((unsigned int)queue->length, ==, 1);
+
+ g_assert_nonnull(find_matching_task(queue, (task_context){
+ .func=open_and_parse_question,
+ .epoll_fd=epoll_fd,
+ .filename=question_filename,
+ .question_filename=question_filename,
+ .password=&password,
+ .cancelled_filenames=&cancelled_filenames,
+ .current_time=¤t_time,
+ .mandos_client_exited=&mandos_client_exited,
+ .password_is_read=&password_is_read,
+ }));
+
+ g_assert_true(queue->next_run == 1);
+
+ g_assert_cmpint(unlink(tempfilename1), ==, 0);
+ g_assert_cmpint(unlink(tempfilename2), ==, 0);
+ g_assert_cmpint(unlink(question_filename), ==, 0);
+ g_assert_cmpint(rmdir(tempdir), ==, 0);
+}
+
+static void test_wait_for_event_timeout(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+
+ g_assert_true(wait_for_event(epoll_fd, 1, 0));
+}
+
+static void test_wait_for_event_event(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int read_pipe = pipefds[0];
+ __attribute__((cleanup(cleanup_close)))
+ const int write_pipe = pipefds[1];
+ g_assert_cmpint(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, read_pipe,
+ &(struct epoll_event)
+ { .events=EPOLLIN | EPOLLRDHUP }), ==, 0);
+ g_assert_cmpint((int)write(write_pipe, "x", 1), ==, 1);
+
+ g_assert_true(wait_for_event(epoll_fd, 0, 0));
+}
+
+static void test_wait_for_event_sigchld(test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ const pid_t pid = fork();
+ if(pid == 0){ /* Child */
+ if(not restore_signal_handler(&fixture->orig_sigaction)){
+ _exit(EXIT_FAILURE);
+ }
+ if(not restore_sigmask(&fixture->orig_sigmask)){
+ _exit(EXIT_FAILURE);
+ }
+ exit(EXIT_SUCCESS);
+ }
+ g_assert_true(pid != -1);
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ g_assert_cmpint(epoll_fd, >=, 0);
+
+ g_assert_true(wait_for_event(epoll_fd, 0, 0));
+
+ int status;
+ g_assert_true(waitpid(pid, &status, 0) == pid);
+ g_assert_true(WIFEXITED(status));
+ g_assert_cmpint(WEXITSTATUS(status), ==, EXIT_SUCCESS);
+}
+
+static void test_run_queue_zeroes_next_run(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ queue->next_run = 1;
+ __attribute__((cleanup(cleanup_close)))
+ const int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ bool quit_now = false;
+
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)queue->next_run, ==, 0);
+}
+
+static
+void test_run_queue_clears_cancelled_filenames(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ bool quit_now = false;
+ const char question_filename[] = "/nonexistent/question_filename";
+ g_assert_true(string_set_add(&cancelled_filenames,
+ question_filename));
+
+ g_assert_true(add_to_queue(queue,
+ (task_context){ .func=dummy_func }));
+
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)(queue->length), ==, 0);
+ g_assert_false(string_set_contains(cancelled_filenames,
+ question_filename));
+}
+
+static
+void test_run_queue_skips_cancelled_filenames(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ bool quit_now = false;
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int read_pipe = pipefds[0];
+ g_assert_cmpint(close(pipefds[1]), ==, 0);
+ const char question_filename[] = "/nonexistent/question_filename";
+ g_assert_true(string_set_add(&cancelled_filenames,
+ question_filename));
+ __attribute__((nonnull))
+ void quit_func(const task_context task,
+ __attribute__((unused)) task_queue *const q){
+ g_assert_nonnull(task.quit_now);
+ *task.quit_now = true;
+ }
+ task_context task = {
+ .func=quit_func,
+ .question_filename=strdup(question_filename),
+ .quit_now=&quit_now,
+ .fd=read_pipe,
+ };
+ g_assert_nonnull(task.question_filename);
+
+ g_assert_true(add_to_queue(queue, task));
+
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_false(quit_now);
+
+ /* read_pipe should be closed already */
+ errno = 0;
+ bool read_pipe_closed = (close(read_pipe) == -1);
+ read_pipe_closed &= (errno == EBADF);
+ g_assert_true(read_pipe_closed);
+}
+
+static void test_run_queue_one_task(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ bool quit_now = false;
+
+ __attribute__((nonnull))
+ void next_run_func(__attribute__((unused))
+ const task_context task,
+ task_queue *const q){
+ q->next_run = 1;
+ }
+
+ task_context task = {
+ .func=next_run_func,
+ };
+ g_assert_true(add_to_queue(queue, task));
+
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_cmpuint((unsigned int)(queue->next_run), ==, 1);
+ g_assert_cmpuint((unsigned int)(queue->length), ==, 0);
+}
+
+static void test_run_queue_two_tasks(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ queue->next_run = 1;
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ bool quit_now = false;
+ bool mandos_client_exited = false;
+
+ __attribute__((nonnull))
+ void next_run_func(__attribute__((unused))
+ const task_context task,
+ task_queue *const q){
+ q->next_run = 1;
+ }
+
+ __attribute__((nonnull))
+ void exited_func(const task_context task,
+ __attribute__((unused)) task_queue *const q){
+ *task.mandos_client_exited = true;
+ }
+
+ task_context task1 = {
+ .func=next_run_func,
+ };
+ g_assert_true(add_to_queue(queue, task1));
+
+ task_context task2 = {
+ .func=exited_func,
+ .mandos_client_exited=&mandos_client_exited,
+ };
+ g_assert_true(add_to_queue(queue, task2));
+
+ g_assert_true(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_false(quit_now);
+ g_assert_cmpuint((unsigned int)(queue->next_run), ==, 1);
+ g_assert_true(mandos_client_exited);
+ g_assert_cmpuint((unsigned int)(queue->length), ==, 0);
+}
+
+static void test_run_queue_two_tasks_quit(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ bool quit_now = false;
+ bool mandos_client_exited = false;
+ bool password_is_read = false;
+
+ __attribute__((nonnull))
+ void set_exited_func(const task_context task,
+ __attribute__((unused)) task_queue *const q){
+ *task.mandos_client_exited = true;
+ *task.quit_now = true;
+ }
+ task_context task1 = {
+ .func=set_exited_func,
+ .quit_now=&quit_now,
+ .mandos_client_exited=&mandos_client_exited,
+ };
+ g_assert_true(add_to_queue(queue, task1));
+
+ __attribute__((nonnull))
+ void set_read_func(const task_context task,
+ __attribute__((unused)) task_queue *const q){
+ *task.quit_now = true;
+ *task.password_is_read = true;
+ }
+ task_context task2 = {
+ .func=set_read_func,
+ .quit_now=&quit_now,
+ .password_is_read=&password_is_read,
+ };
+ g_assert_true(add_to_queue(queue, task2));
+
+ g_assert_false(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_true(quit_now);
+ g_assert_true(mandos_client_exited xor password_is_read);
+ g_assert_cmpuint((unsigned int)(queue->length), ==, 0);
+}
+
+static void test_run_queue_two_tasks_cleanup(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ __attribute__((cleanup(cleanup_queue)))
+ task_queue *queue = create_queue();
+ g_assert_nonnull(queue);
+ __attribute__((cleanup(string_set_clear)))
+ string_set cancelled_filenames = {};
+ int pipefds[2];
+ g_assert_cmpint(pipe2(pipefds, O_CLOEXEC | O_NONBLOCK), ==, 0);
+ __attribute__((cleanup(cleanup_close)))
+ const int read_pipe = pipefds[0];
+ __attribute__((cleanup(cleanup_close)))
+ const int write_pipe = pipefds[1];
+ bool quit_now = false;
+
+ __attribute__((nonnull))
+ void read_func(const task_context task,
+ __attribute__((unused)) task_queue *const q){
+ *task.quit_now = true;
+ }
+ task_context task1 = {
+ .func=read_func,
+ .quit_now=&quit_now,
+ .fd=read_pipe,
+ };
+ g_assert_true(add_to_queue(queue, task1));
+
+ __attribute__((nonnull))
+ void write_func(const task_context task,
+ __attribute__((unused)) task_queue *const q){
+ *task.quit_now = true;
+ }
+ task_context task2 = {
+ .func=write_func,
+ .quit_now=&quit_now,
+ .fd=write_pipe,
+ };
+ g_assert_true(add_to_queue(queue, task2));
+
+ g_assert_false(run_queue(&queue, &cancelled_filenames, &quit_now));
+ g_assert_true(quit_now);
+
+ /* Either read_pipe or write_pipe should be closed already */
+ errno = 0;
+ bool close_read_pipe = (close(read_pipe) == -1);
+ close_read_pipe &= (errno == EBADF);
+ errno = 0;
+ bool close_write_pipe = (close(write_pipe) == -1);
+ close_write_pipe &= (errno == EBADF);
+ g_assert_true(close_read_pipe xor close_write_pipe);
+ g_assert_cmpuint((unsigned int)(queue->length), ==, 0);
+}
+
+static void test_setup_signal_handler(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* Save current SIGCHLD action, whatever it is */
+ struct sigaction expected_sigchld_action;
+ g_assert_cmpint(sigaction(SIGCHLD, NULL, &expected_sigchld_action),
+ ==, 0);
+
+ /* Act; i.e. run the setup_signal_handler() function */
+ struct sigaction actual_old_sigchld_action;
+ g_assert_true(setup_signal_handler(&actual_old_sigchld_action));
+
+ /* Check that the function correctly set "actual_old_sigchld_action"
+ to the same values as the previously saved
+ "expected_sigchld_action" */
+ /* Check member sa_handler */
+ g_assert_true(actual_old_sigchld_action.sa_handler
+ == expected_sigchld_action.sa_handler);
+ /* Check member sa_mask */
+ for(int signum = 1; signum < NSIG; signum++){
+ const int expected_old_block_state
+ = sigismember(&expected_sigchld_action.sa_mask, signum);
+ g_assert_cmpint(expected_old_block_state, >=, 0);
+ const int actual_old_block_state
+ = sigismember(&actual_old_sigchld_action.sa_mask, signum);
+ g_assert_cmpint(actual_old_block_state, >=, 0);
+ g_assert_cmpint(actual_old_block_state,
+ ==, expected_old_block_state);
+ }
+ /* Check member sa_flags */
+ g_assert_true((actual_old_sigchld_action.sa_flags
+ & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART))
+ == (expected_sigchld_action.sa_flags
+ & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART)));
+
+ /* Retrieve the current signal handler for SIGCHLD as set by
+ setup_signal_handler() */
+ struct sigaction actual_new_sigchld_action;
+ g_assert_cmpint(sigaction(SIGCHLD, NULL,
+ &actual_new_sigchld_action), ==, 0);
+ /* Check that the signal handler (member sa_handler) is correctly
+ set to the "handle_sigchld" function */
+ g_assert_true(actual_new_sigchld_action.sa_handler != SIG_DFL);
+ g_assert_true(actual_new_sigchld_action.sa_handler != SIG_IGN);
+ g_assert_true(actual_new_sigchld_action.sa_handler
+ == handle_sigchld);
+ /* Check (in member sa_mask) that at least a handful of signals are
+ actually blocked during the signal handler */
+ for(int signum = 1; signum < NSIG; signum++){
+ int actual_new_block_state;
+ switch(signum){
+ case SIGTERM:
+ case SIGINT:
+ case SIGQUIT:
+ case SIGHUP:
+ actual_new_block_state
+ = sigismember(&actual_new_sigchld_action.sa_mask, signum);
+ g_assert_cmpint(actual_new_block_state, ==, 1);
+ continue;
+ case SIGKILL: /* non-blockable */
+ case SIGSTOP: /* non-blockable */
+ case SIGCHLD: /* always blocked */
+ default:
+ continue;
+ }
+ }
+ /* Check member sa_flags */
+ g_assert_true((actual_new_sigchld_action.sa_flags
+ & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART))
+ == (SA_NOCLDSTOP | SA_RESTART));
+
+ /* Restore signal handler */
+ g_assert_cmpint(sigaction(SIGCHLD, &expected_sigchld_action, NULL),
+ ==, 0);
+}
+
+static void test_restore_signal_handler(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* Save current SIGCHLD action, whatever it is */
+ struct sigaction expected_sigchld_action;
+ g_assert_cmpint(sigaction(SIGCHLD, NULL, &expected_sigchld_action),
+ ==, 0);
+ /* Since we haven't established a signal handler yet, there should
+ not be one established. But another test may have relied on
+ restore_signal_handler() to restore the signal handler, and if
+ restore_signal_handler() is buggy (which we should be prepared
+ for in this test) the signal handler may not have been restored
+ properly; check for this: */
+ g_assert_true(expected_sigchld_action.sa_handler != handle_sigchld);
+
+ /* Establish a signal handler */
+ struct sigaction sigchld_action = {
+ .sa_handler=handle_sigchld,
+ .sa_flags=SA_RESTART | SA_NOCLDSTOP,
+ };
+ g_assert_cmpint(sigfillset(&sigchld_action.sa_mask), ==, 0);
+ g_assert_cmpint(sigaction(SIGCHLD, &sigchld_action, NULL), ==, 0);
+
+ /* Act; i.e. run the restore_signal_handler() function */
+ g_assert_true(restore_signal_handler(&expected_sigchld_action));
+
+ /* Retrieve the restored signal handler data */
+ struct sigaction actual_restored_sigchld_action;
+ g_assert_cmpint(sigaction(SIGCHLD, NULL,
+ &actual_restored_sigchld_action), ==, 0);
+
+ /* Check that the function correctly restored the signal action, as
+ saved in "actual_restored_sigchld_action", to the same values as
+ the previously saved "expected_sigchld_action" */
+ /* Check member sa_handler */
+ g_assert_true(actual_restored_sigchld_action.sa_handler
+ == expected_sigchld_action.sa_handler);
+ /* Check member sa_mask */
+ for(int signum = 1; signum < NSIG; signum++){
+ const int expected_old_block_state
+ = sigismember(&expected_sigchld_action.sa_mask, signum);
+ g_assert_cmpint(expected_old_block_state, >=, 0);
+ const int actual_restored_block_state
+ = sigismember(&actual_restored_sigchld_action.sa_mask, signum);
+ g_assert_cmpint(actual_restored_block_state, >=, 0);
+ g_assert_cmpint(actual_restored_block_state,
+ ==, expected_old_block_state);
+ }
+ /* Check member sa_flags */
+ g_assert_true((actual_restored_sigchld_action.sa_flags
+ & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART))
+ == (expected_sigchld_action.sa_flags
+ & (SA_NOCLDSTOP | SA_ONSTACK | SA_RESTART)));
+}
+
+static void test_block_sigchld(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* Save original signal mask */
+ sigset_t expected_sigmask;
+ g_assert_cmpint(pthread_sigmask(-1, NULL, &expected_sigmask),
+ ==, 0);
+
+ /* Make sure SIGCHLD is unblocked for this test */
+ sigset_t sigchld_sigmask;
+ g_assert_cmpint(sigemptyset(&sigchld_sigmask), ==, 0);
+ g_assert_cmpint(sigaddset(&sigchld_sigmask, SIGCHLD), ==, 0);
+ g_assert_cmpint(pthread_sigmask(SIG_UNBLOCK, &sigchld_sigmask,
+ NULL), ==, 0);
+
+ /* Act; i.e. run the block_sigchld() function */
+ sigset_t actual_old_sigmask;
+ g_assert_true(block_sigchld(&actual_old_sigmask));
+
+ /* Check the actual_old_sigmask; it should be the same as the
+ previously saved signal mask "expected_sigmask". */
+ for(int signum = 1; signum < NSIG; signum++){
+ const int expected_old_block_state
+ = sigismember(&expected_sigmask, signum);
+ g_assert_cmpint(expected_old_block_state, >=, 0);
+ const int actual_old_block_state
+ = sigismember(&actual_old_sigmask, signum);
+ g_assert_cmpint(actual_old_block_state, >=, 0);
+ g_assert_cmpint(actual_old_block_state,
+ ==, expected_old_block_state);
+ }
+
+ /* Retrieve the newly set signal mask */
+ sigset_t actual_sigmask;
+ g_assert_cmpint(pthread_sigmask(-1, NULL, &actual_sigmask), ==, 0);
+
+ /* SIGCHLD should be blocked */
+ g_assert_cmpint(sigismember(&actual_sigmask, SIGCHLD), ==, 1);
+
+ /* Restore signal mask */
+ g_assert_cmpint(pthread_sigmask(SIG_SETMASK, &expected_sigmask,
+ NULL), ==, 0);
+}
+
+static void test_restore_sigmask(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ /* Save original signal mask */
+ sigset_t orig_sigmask;
+ g_assert_cmpint(pthread_sigmask(-1, NULL, &orig_sigmask), ==, 0);
+
+ /* Make sure SIGCHLD is blocked for this test */
+ sigset_t sigchld_sigmask;
+ g_assert_cmpint(sigemptyset(&sigchld_sigmask), ==, 0);
+ g_assert_cmpint(sigaddset(&sigchld_sigmask, SIGCHLD), ==, 0);
+ g_assert_cmpint(pthread_sigmask(SIG_BLOCK, &sigchld_sigmask,
+ NULL), ==, 0);
+
+ /* Act; i.e. run the restore_sigmask() function */
+ g_assert_true(restore_sigmask(&orig_sigmask));
+
+ /* Retrieve the newly restored signal mask */
+ sigset_t restored_sigmask;
+ g_assert_cmpint(pthread_sigmask(-1, NULL, &restored_sigmask),
+ ==, 0);
+
+ /* Check the restored_sigmask; it should be the same as the
+ previously saved signal mask "orig_sigmask". */
+ for(int signum = 1; signum < NSIG; signum++){
+ const int orig_block_state = sigismember(&orig_sigmask, signum);
+ g_assert_cmpint(orig_block_state, >=, 0);
+ const int restored_block_state = sigismember(&restored_sigmask,
+ signum);
+ g_assert_cmpint(restored_block_state, >=, 0);
+ g_assert_cmpint(restored_block_state, ==, orig_block_state);
+ }
+
+ /* Restore signal mask */
+ g_assert_cmpint(pthread_sigmask(SIG_SETMASK, &orig_sigmask,
+ NULL), ==, 0);
+}
+
+static void test_parse_arguments_noargs(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ char *agent_directory = NULL;
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+ g_assert_null(agent_directory);
+ g_assert_null(helper_directory);
+ g_assert_true(user == 0);
+ g_assert_true(group == 0);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+__attribute__((nonnull))
+static bool parse_arguments_devnull(int argc, char *argv[],
+ const bool exit_failure,
+ char **agent_directory,
+ char **helper_directory,
+ uid_t *const user,
+ gid_t *const group,
+ char **mandos_argz,
+ size_t *mandos_argz_length){
+
+ FILE *real_stderr = stderr;
+ FILE *devnull = fopen("/dev/null", "we");
+ g_assert_nonnull(devnull);
+ stderr = devnull;
+
+ const bool ret = parse_arguments(argc, argv, exit_failure,
+ agent_directory,
+ helper_directory, user, group,
+ mandos_argz, mandos_argz_length);
+ const error_t saved_errno = errno;
+
+ stderr = real_stderr;
+ g_assert_cmpint(fclose(devnull), ==, 0);
+
+ errno = saved_errno;
+
+ return ret;
+}
+
+static void test_parse_arguments_invalid(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--invalid"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ char *agent_directory = NULL;
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_false(parse_arguments_devnull(argc, argv, false,
+ &agent_directory,
+ &helper_directory, &user,
+ &group, &mandos_argz,
+ &mandos_argz_length));
+
+ g_assert_true(errno == EINVAL);
+ g_assert_null(agent_directory);
+ g_assert_null(helper_directory);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static void test_parse_arguments_long_dir(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--agent-directory"),
+ strdup("/tmp"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *agent_directory = NULL;
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+
+ g_assert_cmpstr(agent_directory, ==, "/tmp");
+ g_assert_null(helper_directory);
+ g_assert_true(user == 0);
+ g_assert_true(group == 0);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static void test_parse_arguments_short_dir(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("-d"),
+ strdup("/tmp"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *agent_directory = NULL;
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+
+ g_assert_cmpstr(agent_directory, ==, "/tmp");
+ g_assert_null(helper_directory);
+ g_assert_true(user == 0);
+ g_assert_true(group == 0);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static
+void test_parse_arguments_helper_directory(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--helper-directory"),
+ strdup("/tmp"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+
+ g_assert_cmpstr(helper_directory, ==, "/tmp");
+ g_assert_null(agent_directory);
+ g_assert_true(user == 0);
+ g_assert_true(group == 0);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static
+void test_parse_arguments_plugin_helper_dir(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--plugin-helper-dir"),
+ strdup("/tmp"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+
+ g_assert_cmpstr(helper_directory, ==, "/tmp");
+ g_assert_null(agent_directory);
+ g_assert_true(user == 0);
+ g_assert_true(group == 0);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static void test_parse_arguments_user(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--user"),
+ strdup("1000"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+
+ g_assert_null(helper_directory);
+ g_assert_null(agent_directory);
+ g_assert_cmpuint((unsigned int)user, ==, 1000);
+ g_assert_true(group == 0);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static void test_parse_arguments_user_invalid(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--user"),
+ strdup("invalid"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_false(parse_arguments_devnull(argc, argv, false,
+ &agent_directory,
+ &helper_directory, &user,
+ &group, &mandos_argz,
+ &mandos_argz_length));
+
+ g_assert_null(helper_directory);
+ g_assert_null(agent_directory);
+ g_assert_cmpuint((unsigned int)user, ==, 0);
+ g_assert_true(group == 0);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static
+void test_parse_arguments_user_zero_invalid(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--user"),
+ strdup("0"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_false(parse_arguments_devnull(argc, argv, false,
+ &agent_directory,
+ &helper_directory, &user,
+ &group, &mandos_argz,
+ &mandos_argz_length));
+
+ g_assert_null(helper_directory);
+ g_assert_null(agent_directory);
+ g_assert_cmpuint((unsigned int)user, ==, 0);
+ g_assert_true(group == 0);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static void test_parse_arguments_group(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--group"),
+ strdup("1000"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+
+ g_assert_null(helper_directory);
+ g_assert_null(agent_directory);
+ g_assert_true(user == 0);
+ g_assert_cmpuint((unsigned int)group, ==, 1000);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static void test_parse_arguments_group_invalid(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--group"),
+ strdup("invalid"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_false(parse_arguments_devnull(argc, argv, false,
+ &agent_directory,
+ &helper_directory, &user,
+ &group, &mandos_argz,
+ &mandos_argz_length));
+
+ g_assert_null(helper_directory);
+ g_assert_null(agent_directory);
+ g_assert_true(user == 0);
+ g_assert_true(group == 0);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static
+void test_parse_arguments_group_zero_invalid(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--group"),
+ strdup("0"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_false(parse_arguments_devnull(argc, argv, false,
+ &agent_directory,
+ &helper_directory, &user,
+ &group, &mandos_argz,
+ &mandos_argz_length));
+
+ g_assert_null(helper_directory);
+ g_assert_null(agent_directory);
+ g_assert_cmpuint((unsigned int)group, ==, 0);
+ g_assert_true(group == 0);
+ g_assert_null(mandos_argz);
+ g_assert_true(mandos_argz_length == 0);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static void test_parse_arguments_mandos_noargs(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer
+ user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("mandos-client"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+
+ g_assert_null(agent_directory);
+ g_assert_null(helper_directory);
+ g_assert_true(user == 0);
+ g_assert_true(group == 0);
+ g_assert_cmpstr(mandos_argz, ==, "mandos-client");
+ g_assert_cmpuint((unsigned int)argz_count(mandos_argz,
+ mandos_argz_length),
+ ==, 1);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static void test_parse_arguments_mandos_args(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("mandos-client"),
+ strdup("one"),
+ strdup("two"),
+ strdup("three"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+
+ g_assert_null(agent_directory);
+ g_assert_null(helper_directory);
+ g_assert_true(user == 0);
+ g_assert_true(group == 0);
+ char *marg = mandos_argz;
+ g_assert_cmpstr(marg, ==, "mandos-client");
+ marg = argz_next(mandos_argz, mandos_argz_length, marg);
+ g_assert_cmpstr(marg, ==, "one");
+ marg = argz_next(mandos_argz, mandos_argz_length, marg);
+ g_assert_cmpstr(marg, ==, "two");
+ marg = argz_next(mandos_argz, mandos_argz_length, marg);
+ g_assert_cmpstr(marg, ==, "three");
+ g_assert_cmpuint((unsigned int)argz_count(mandos_argz,
+ mandos_argz_length),
+ ==, 4);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static void test_parse_arguments_all_args(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("--agent-directory"),
+ strdup("/tmp"),
+ strdup("--helper-directory"),
+ strdup("/var/tmp"),
+ strdup("--user"),
+ strdup("1"),
+ strdup("--group"),
+ strdup("2"),
+ strdup("mandos-client"),
+ strdup("one"),
+ strdup("two"),
+ strdup("three"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+
+ g_assert_cmpstr(agent_directory, ==, "/tmp");
+ g_assert_cmpstr(helper_directory, ==, "/var/tmp");
+ g_assert_true(user == 1);
+ g_assert_true(group == 2);
+ char *marg = mandos_argz;
+ g_assert_cmpstr(marg, ==, "mandos-client");
+ marg = argz_next(mandos_argz, mandos_argz_length, marg);
+ g_assert_cmpstr(marg, ==, "one");
+ marg = argz_next(mandos_argz, mandos_argz_length, marg);
+ g_assert_cmpstr(marg, ==, "two");
+ marg = argz_next(mandos_argz, mandos_argz_length, marg);
+ g_assert_cmpstr(marg, ==, "three");
+ g_assert_cmpuint((unsigned int)argz_count(mandos_argz,
+ mandos_argz_length),
+ ==, 4);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+static void test_parse_arguments_mixed(__attribute__((unused))
+ test_fixture *fixture,
+ __attribute__((unused))
+ gconstpointer user_data){
+ char *argv[] = {
+ strdup("prgname"),
+ strdup("mandos-client"),
+ strdup("--user"),
+ strdup("1"),
+ strdup("one"),
+ strdup("--agent-directory"),
+ strdup("/tmp"),
+ strdup("two"),
+ strdup("three"),
+ strdup("--helper-directory=/var/tmp"),
+ NULL };
+ const int argc = (sizeof(argv) / sizeof(char *)) - 1;
+
+ __attribute__((cleanup(cleanup_string)))
+ char *agent_directory = NULL;
+ __attribute__((cleanup(cleanup_string)))
+ char *helper_directory = NULL;
+ uid_t user = 0;
+ gid_t group = 0;
+ __attribute__((cleanup(cleanup_string)))
+ char *mandos_argz = NULL;
+ size_t mandos_argz_length = 0;
+
+ g_assert_true(parse_arguments(argc, argv, false, &agent_directory,
+ &helper_directory, &user, &group,
+ &mandos_argz, &mandos_argz_length));
+
+ g_assert_cmpstr(agent_directory, ==, "/tmp");
+ g_assert_cmpstr(helper_directory, ==, "/var/tmp");
+ g_assert_true(user == 1);
+ g_assert_true(group == 0);
+ char *marg = mandos_argz;
+ g_assert_cmpstr(marg, ==, "mandos-client");
+ marg = argz_next(mandos_argz, mandos_argz_length, marg);
+ g_assert_cmpstr(marg, ==, "one");
+ marg = argz_next(mandos_argz, mandos_argz_length, marg);
+ g_assert_cmpstr(marg, ==, "two");
+ marg = argz_next(mandos_argz, mandos_argz_length, marg);
+ g_assert_cmpstr(marg, ==, "three");
+ g_assert_cmpuint((unsigned int)argz_count(mandos_argz,
+ mandos_argz_length),
+ ==, 4);
+
+ for(char **arg = argv; *arg != NULL; arg++){
+ free(*arg);
+ }
+}
+
+/* End of tests section */
+
+/* Test boilerplate section; New tests should be added to the test
+ suite definition here, in the "run_tests" function.
+
+ Finally, this section also contains the should_only_run_tests()
+ function used by main() for deciding if tests should be run or to
+ start normally. */
+
+__attribute__((cold))
+static bool run_tests(int argc, char *argv[]){
+ g_test_init(&argc, &argv, NULL);
+
+ /* A macro to add a test with no setup or teardown functions */
+#define test_add(testpath, testfunc) \
+ do { \
+ g_test_add((testpath), test_fixture, NULL, NULL, \
+ (testfunc), NULL); \
+ } while(false)
+
+ /* Test the signal-related functions first, since some other tests
+ depend on these functions in their setups and teardowns */
+ test_add("/signal-handling/setup", test_setup_signal_handler);
+ test_add("/signal-handling/restore", test_restore_signal_handler);
+ test_add("/signal-handling/block", test_block_sigchld);
+ test_add("/signal-handling/restore-sigmask", test_restore_sigmask);
+
+ /* Regular non-signal-related tests; these use no setups or
+ teardowns */
+ test_add("/parse_arguments/noargs", test_parse_arguments_noargs);
+ test_add("/parse_arguments/invalid", test_parse_arguments_invalid);
+ test_add("/parse_arguments/long-dir",
+ test_parse_arguments_long_dir);
+ test_add("/parse_arguments/short-dir",
+ test_parse_arguments_short_dir);
+ test_add("/parse_arguments/helper-directory",
+ test_parse_arguments_helper_directory);
+ test_add("/parse_arguments/plugin-helper-dir",
+ test_parse_arguments_plugin_helper_dir);
+ test_add("/parse_arguments/user", test_parse_arguments_user);
+ test_add("/parse_arguments/user-invalid",
+ test_parse_arguments_user_invalid);
+ test_add("/parse_arguments/user-zero-invalid",
+ test_parse_arguments_user_zero_invalid);
+ test_add("/parse_arguments/group", test_parse_arguments_group);
+ test_add("/parse_arguments/group-invalid",
+ test_parse_arguments_group_invalid);
+ test_add("/parse_arguments/group-zero-invalid",
+ test_parse_arguments_group_zero_invalid);
+ test_add("/parse_arguments/mandos-noargs",
+ test_parse_arguments_mandos_noargs);
+ test_add("/parse_arguments/mandos-args",
+ test_parse_arguments_mandos_args);
+ test_add("/parse_arguments/all-args",
+ test_parse_arguments_all_args);
+ test_add("/parse_arguments/mixed", test_parse_arguments_mixed);
+ test_add("/queue/create", test_create_queue);
+ test_add("/queue/add", test_add_to_queue);
+ test_add("/queue/has_question/empty",
+ test_queue_has_question_empty);
+ test_add("/queue/has_question/false",
+ test_queue_has_question_false);
+ test_add("/queue/has_question/true", test_queue_has_question_true);
+ test_add("/queue/has_question/false2",
+ test_queue_has_question_false2);
+ test_add("/queue/has_question/true2",
+ test_queue_has_question_true2);
+ test_add("/buffer/cleanup", test_cleanup_buffer);
+ test_add("/string_set/net-set-contains-nothing",
+ test_string_set_new_set_contains_nothing);
+ test_add("/string_set/with-added-string-contains-it",
+ test_string_set_with_added_string_contains_it);
+ test_add("/string_set/cleared-does-not-contain-string",
+ test_string_set_cleared_does_not_contain_str);
+ test_add("/string_set/swap/one-with-empty",
+ test_string_set_swap_one_with_empty);
+ test_add("/string_set/swap/empty-with-one",
+ test_string_set_swap_empty_with_one);
+ test_add("/string_set/swap/one-with-one",
+ test_string_set_swap_one_with_one);
+
+ /* A macro to add a test using the setup and teardown functions */
+#define test_add_st(path, func) \
+ do { \
+ g_test_add((path), test_fixture, NULL, test_setup, (func), \
+ test_teardown); \
+ } while(false)
+
+ /* Signal-related tests; these use setups and teardowns which
+ establish, during each test run, a signal handler for, and a
+ signal mask blocking, the SIGCHLD signal, just like main() */
+ test_add_st("/wait_for_event/timeout", test_wait_for_event_timeout);
+ test_add_st("/wait_for_event/event", test_wait_for_event_event);
+ test_add_st("/wait_for_event/sigchld", test_wait_for_event_sigchld);
+ test_add_st("/run_queue/zeroes-next-run",
+ test_run_queue_zeroes_next_run);
+ test_add_st("/run_queue/clears-cancelled_filenames",
+ test_run_queue_clears_cancelled_filenames);
+ test_add_st("/run_queue/skips-cancelled-filenames",
+ test_run_queue_skips_cancelled_filenames);
+ test_add_st("/run_queue/one-task", test_run_queue_one_task);
+ test_add_st("/run_queue/two-tasks", test_run_queue_two_tasks);
+ test_add_st("/run_queue/two-tasks/quit",
+ test_run_queue_two_tasks_quit);
+ test_add_st("/run_queue/two-tasks-cleanup",
+ test_run_queue_two_tasks_cleanup);
+ test_add_st("/task-creators/start_mandos_client",
+ test_start_mandos_client);
+ test_add_st("/task-creators/start_mandos_client/execv",
+ test_start_mandos_client_execv);
+ test_add_st("/task-creators/start_mandos_client/suid/euid",
+ test_start_mandos_client_suid_euid);
+ test_add_st("/task-creators/start_mandos_client/suid/egid",
+ test_start_mandos_client_suid_egid);
+ test_add_st("/task-creators/start_mandos_client/suid/ruid",
+ test_start_mandos_client_suid_ruid);
+ test_add_st("/task-creators/start_mandos_client/suid/rgid",
+ test_start_mandos_client_suid_rgid);
+ test_add_st("/task-creators/start_mandos_client/read",
+ test_start_mandos_client_read);
+ test_add_st("/task-creators/start_mandos_client/helper-directory",
+ test_start_mandos_client_helper_directory);
+ test_add_st("/task-creators/start_mandos_client/sigmask",
+ test_start_mandos_client_sigmask);
+ test_add_st("/task/wait_for_mandos_client_exit/badpid",
+ test_wait_for_mandos_client_exit_badpid);
+ test_add_st("/task/wait_for_mandos_client_exit/noexit",
+ test_wait_for_mandos_client_exit_noexit);
+ test_add_st("/task/wait_for_mandos_client_exit/success",
+ test_wait_for_mandos_client_exit_success);
+ test_add_st("/task/wait_for_mandos_client_exit/failure",
+ test_wait_for_mandos_client_exit_failure);
+ test_add_st("/task/wait_for_mandos_client_exit/killed",
+ test_wait_for_mandos_client_exit_killed);
+ test_add_st("/task/read_mandos_client_output/readerror",
+ test_read_mandos_client_output_readerror);
+ test_add_st("/task/read_mandos_client_output/nodata",
+ test_read_mandos_client_output_nodata);
+ test_add_st("/task/read_mandos_client_output/eof",
+ test_read_mandos_client_output_eof);
+ test_add_st("/task/read_mandos_client_output/once",
+ test_read_mandos_client_output_once);
+ test_add_st("/task/read_mandos_client_output/malloc",
+ test_read_mandos_client_output_malloc);
+ test_add_st("/task/read_mandos_client_output/append",
+ test_read_mandos_client_output_append);
+ test_add_st("/task-creators/add_inotify_dir_watch",
+ test_add_inotify_dir_watch);
+ test_add_st("/task-creators/add_inotify_dir_watch/fail",
+ test_add_inotify_dir_watch_fail);
+ test_add_st("/task-creators/add_inotify_dir_watch/not-a-directory",
+ test_add_inotify_dir_watch_nondir);
+ test_add_st("/task-creators/add_inotify_dir_watch/EAGAIN",
+ test_add_inotify_dir_watch_EAGAIN);
+ test_add_st("/task-creators/add_inotify_dir_watch/IN_CLOSE_WRITE",
+ test_add_inotify_dir_watch_IN_CLOSE_WRITE);
+ test_add_st("/task-creators/add_inotify_dir_watch/IN_MOVED_TO",
+ test_add_inotify_dir_watch_IN_MOVED_TO);
+ test_add_st("/task-creators/add_inotify_dir_watch/IN_MOVED_FROM",
+ test_add_inotify_dir_watch_IN_MOVED_FROM);
+ test_add_st("/task-creators/add_inotify_dir_watch/IN_EXCL_UNLINK",
+ test_add_inotify_dir_watch_IN_EXCL_UNLINK);
+ test_add_st("/task-creators/add_inotify_dir_watch/IN_DELETE",
+ test_add_inotify_dir_watch_IN_DELETE);
+ test_add_st("/task/read_inotify_event/readerror",
+ test_read_inotify_event_readerror);
+ test_add_st("/task/read_inotify_event/bad-epoll",
+ test_read_inotify_event_bad_epoll);
+ test_add_st("/task/read_inotify_event/nodata",
+ test_read_inotify_event_nodata);
+ test_add_st("/task/read_inotify_event/eof",
+ test_read_inotify_event_eof);
+ test_add_st("/task/read_inotify_event/IN_CLOSE_WRITE",
+ test_read_inotify_event_IN_CLOSE_WRITE);
+ test_add_st("/task/read_inotify_event/IN_MOVED_TO",
+ test_read_inotify_event_IN_MOVED_TO);
+ test_add_st("/task/read_inotify_event/IN_MOVED_FROM",
+ test_read_inotify_event_IN_MOVED_FROM);
+ test_add_st("/task/read_inotify_event/IN_DELETE",
+ test_read_inotify_event_IN_DELETE);
+ test_add_st("/task/read_inotify_event/IN_CLOSE_WRITE/badname",
+ test_read_inotify_event_IN_CLOSE_WRITE_badname);
+ test_add_st("/task/read_inotify_event/IN_MOVED_TO/badname",
+ test_read_inotify_event_IN_MOVED_TO_badname);
+ test_add_st("/task/read_inotify_event/IN_MOVED_FROM/badname",
+ test_read_inotify_event_IN_MOVED_FROM_badname);
+ test_add_st("/task/read_inotify_event/IN_DELETE/badname",
+ test_read_inotify_event_IN_DELETE_badname);
+ test_add_st("/task/open_and_parse_question/ENOENT",
+ test_open_and_parse_question_ENOENT);
+ test_add_st("/task/open_and_parse_question/EIO",
+ test_open_and_parse_question_EIO);
+ test_add_st("/task/open_and_parse_question/parse-error",
+ test_open_and_parse_question_parse_error);
+ test_add_st("/task/open_and_parse_question/nosocket",
+ test_open_and_parse_question_nosocket);
+ test_add_st("/task/open_and_parse_question/badsocket",
+ test_open_and_parse_question_badsocket);
+ test_add_st("/task/open_and_parse_question/nopid",
+ test_open_and_parse_question_nopid);
+ test_add_st("/task/open_and_parse_question/badpid",
+ test_open_and_parse_question_badpid);
+ test_add_st("/task/open_and_parse_question/noexist_pid",
+ test_open_and_parse_question_noexist_pid);
+ test_add_st("/task/open_and_parse_question/no-notafter",
+ test_open_and_parse_question_no_notafter);
+ test_add_st("/task/open_and_parse_question/bad-notafter",
+ test_open_and_parse_question_bad_notafter);
+ test_add_st("/task/open_and_parse_question/notafter-0",
+ test_open_and_parse_question_notafter_0);
+ test_add_st("/task/open_and_parse_question/notafter-1",
+ test_open_and_parse_question_notafter_1);
+ test_add_st("/task/open_and_parse_question/notafter-1-1",
+ test_open_and_parse_question_notafter_1_1);
+ test_add_st("/task/open_and_parse_question/notafter-1-2",
+ test_open_and_parse_question_notafter_1_2);
+ test_add_st("/task/open_and_parse_question/equal-notafter",
+ test_open_and_parse_question_equal_notafter);
+ test_add_st("/task/open_and_parse_question/late-notafter",
+ test_open_and_parse_question_late_notafter);
+ test_add_st("/task/cancel_old_question/0-1-2",
+ test_cancel_old_question_0_1_2);
+ test_add_st("/task/cancel_old_question/0-2-1",
+ test_cancel_old_question_0_2_1);
+ test_add_st("/task/cancel_old_question/1-2-3",
+ test_cancel_old_question_1_2_3);
+ test_add_st("/task/cancel_old_question/1-3-2",
+ test_cancel_old_question_1_3_2);
+ test_add_st("/task/cancel_old_question/2-1-3",
+ test_cancel_old_question_2_1_3);
+ test_add_st("/task/cancel_old_question/2-3-1",
+ test_cancel_old_question_2_3_1);
+ test_add_st("/task/cancel_old_question/3-1-2",
+ test_cancel_old_question_3_1_2);
+ test_add_st("/task/cancel_old_question/3-2-1",
+ test_cancel_old_question_3_2_1);
+ test_add_st("/task/connect_question_socket/name-too-long",
+ test_connect_question_socket_name_too_long);
+ test_add_st("/task/connect_question_socket/connect-fail",
+ test_connect_question_socket_connect_fail);
+ test_add_st("/task/connect_question_socket/bad-epoll",
+ test_connect_question_socket_bad_epoll);
+ test_add_st("/task/connect_question_socket/usable",
+ test_connect_question_socket_usable);
+ test_add_st("/task/send_password_to_socket/client-not-exited",
+ test_send_password_to_socket_client_not_exited);
+ test_add_st("/task/send_password_to_socket/password-not-read",
+ test_send_password_to_socket_password_not_read);
+ test_add_st("/task/send_password_to_socket/EMSGSIZE",
+ test_send_password_to_socket_EMSGSIZE);
+ test_add_st("/task/send_password_to_socket/retry",
+ test_send_password_to_socket_retry);
+ test_add_st("/task/send_password_to_socket/bad-epoll",
+ test_send_password_to_socket_bad_epoll);
+ test_add_st("/task/send_password_to_socket/null-password",
+ test_send_password_to_socket_null_password);
+ test_add_st("/task/send_password_to_socket/empty-password",
+ test_send_password_to_socket_empty_password);
+ test_add_st("/task/send_password_to_socket/empty-str-password",
+ test_send_password_to_socket_empty_str_pass);
+ test_add_st("/task/send_password_to_socket/text-password",
+ test_send_password_to_socket_text_password);
+ test_add_st("/task/send_password_to_socket/binary-password",
+ test_send_password_to_socket_binary_password);
+ test_add_st("/task/send_password_to_socket/nuls-in-password",
+ test_send_password_to_socket_nuls_in_password);
+ test_add_st("/task-creators/add_existing_questions/ENOENT",
+ test_add_existing_questions_ENOENT);
+ test_add_st("/task-creators/add_existing_questions/no-questions",
+ test_add_existing_questions_no_questions);
+ test_add_st("/task-creators/add_existing_questions/one-question",
+ test_add_existing_questions_one_question);
+ test_add_st("/task-creators/add_existing_questions/two-questions",
+ test_add_existing_questions_two_questions);
+ test_add_st("/task-creators/add_existing_questions/non-questions",
+ test_add_existing_questions_non_questions);
+ test_add_st("/task-creators/add_existing_questions/both-types",
+ test_add_existing_questions_both_types);
+
+ return g_test_run() == 0;
+}
+
+static bool should_only_run_tests(int *argc_p, char **argv_p[]){
+ GOptionContext *context = g_option_context_new("");
+
+ g_option_context_set_help_enabled(context, FALSE);
+ g_option_context_set_ignore_unknown_options(context, TRUE);
+
+ gboolean run_tests = FALSE;
+ GOptionEntry entries[] = {
+ { "test", 0, 0, G_OPTION_ARG_NONE,
+ &run_tests, "Run tests", NULL },
+ { NULL }
+ };
+ g_option_context_add_main_entries(context, entries, NULL);
+
+ GError *error = NULL;
+
+ if(g_option_context_parse(context, argc_p, argv_p, &error) != TRUE){
+ g_option_context_free(context);
+ g_error("Failed to parse options: %s", error->message);
+ }
+
+ g_option_context_free(context);
+ return run_tests != FALSE;
+}
=== added file 'dracut-module/password-agent.xml'
--- dracut-module/password-agent.xml 1970-01-01 00:00:00 +0000
+++ dracut-module/password-agent.xml 2019-07-27 10:11:45 +0000
@@ -0,0 +1,469 @@
+
+
+
+
+%common;
+]>
+
+
+
+ Mandos Manual
+
+ Mandos
+ &version;
+ &TIMESTAMP;
+
+
+ Björn
+ Påhlsson
+
+ belorn@recompile.se
+
+
+
+ Teddy
+ Hogeborn
+
+ teddy@recompile.se
+
+
+
+
+ 2019
+ Teddy Hogeborn
+ Björn Påhlsson
+
+
+
+
+
+ &COMMANDNAME;
+ 8mandos
+
+
+
+ &COMMANDNAME;
+
+ Run Mandos client as a systemd password agent.
+
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ --
+
+ MANDOS_CLIENT
+
+ OPTIONS
+
+
+
+
+ &COMMANDNAME;
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+ &COMMANDNAME;
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+
+
+ DESCRIPTION
+
+ &COMMANDNAME; is a program which is meant to
+ be a systemd
+ 1 Password
+ Agent
(See Password Agents). The aim of this program is therefore
+ to acquire and then send a password to some other program which
+ will use the password to unlock the encrypted root disk.
+
+
+ This program is not meant to be invoked directly, but can be in
+ order to test it.
+
+
+
+
+ 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
+
+
+
+
+
+
+ Specify a different agent directory. The default is
+ /run/systemd/ask-password
as per the
+ Password Agents specification.
+
+
+
+
+
+
+
+
+ Specify a different helper directory. The default is
+ /lib/mandos/plugin-helpers
, which
+ will exist in the initial RAM disk
+ environment. (This will simply be passed to the
+ MANDOS_CLIENT program via the
+ MANDOSPLUGINHELPERDIR environment variable.
+ See
+ mandos-client8mandos.)
+
+
+
+
+
+
+
+
+ Change real user ID to USERID
+ when running MANDOS_CLIENT.
+ The default is 65534. Note: This
+ must be a number, not a name.
+
+
+
+
+
+
+
+
+ Change real group ID to GROUPID
+ when running MANDOS_CLIENT.
+ The default is 65534. Note: This
+ must be a number, not a name.
+
+
+
+
+
+ MANDOS_CLIENT
+
+
+ This specifies the file name for
+ mandos-client8mandos. If the
+
option is given, any
+ following options are passed to the MANDOS_CLIENT program. The default is
+ /lib/mandos/plugins.d/mandos-client
+ (which is the correct location for the initial
+ RAM disk environment) without any
+ options.
+
+
+
+
+
+
+
+
+
+ Gives a help message about options and their meanings.
+
+
+
+
+
+
+
+
+ Ignore normal operation; instead only run self-tests.
+ Adding the option may show more
+ options possible in combination with
+ .
+
+
+
+
+
+
+
+
+ Gives a short usage message.
+
+
+
+
+
+
+
+
+
+ Prints the program version.
+
+
+
+
+
+
+
+ OVERVIEW
+
+
+ This program, &COMMANDNAME;, will run on the client side in the
+ initial RAM disk environment, and is
+ responsible for getting a password from the Mandos client
+ program itself, and to send that password to whatever is
+ currently asking for a password using the systemd Password Agents mechanism.
+
+ To accomplish this, &COMMANDNAME; runs the
+ mandos-client program (which is the actual
+ client program communicating with the Mandos server) or,
+ alternatively, any executable file specified as
+ MANDOS_CLIENT, and, as soon as a
+ password is acquired from the
+ MANDOS_CLIENT program, sends that
+ password (as per the Password Agents specification) to all currently
+ unanswered password questions.
+
+
+ This program should be started (normally as a systemd service,
+ which in turn is normally started by a systemd.path
+ 5 file) as a reaction to
+ files named ask.xxxx
appearing in the agent directory
+ /run/systemd/ask-password
+ (or the directory specified by
+ ).
+
+
+
+
+ EXIT STATUS
+
+ Exit status of this program is zero if no errors were
+ encountered, and otherwise not.
+
+
+
+
+ ENVIRONMENT
+
+ This program does not use any environment variables itself, it
+ only passes on its environment to
+ MANDOS_CLIENT. Also, the
+ option will affect the
+ environment variable MANDOSPLUGINHELPERDIR for
+ MANDOS_CLIENT.
+
+
+
+
+ FILES
+
+
+
+ /run/systemd/ask-password
+
+
+ The default directory to watch for password questions as
+ per the Password Agents specification; can be changed
+ by the option.
+
+
+
+
+ /lib/mandos/plugin-helpers
+
+
+ The helper directory as supplied to
+ MANDOS_CLIENT via the
+ MANDOSPLUGINHELPERDIR environment
+ variable; can be changed by the
+ option.
+
+
+
+
+
+
+
+
+ BUGS
+
+
+
+
+ EXAMPLE
+
+
+ Normal invocation needs no options:
+
+
+ &COMMANDNAME;
+
+
+
+
+ Run an alternative MANDOS_CLIENT
+ program::
+
+
+ &COMMANDNAME; /usr/local/sbin/alternate
+
+
+
+
+ Use alternative locations for the helper directory and the
+ Mandos client, and add extra options suitable for running in
+ the normal file system:
+
+
+
+
+ &COMMANDNAME; --helper-directory=/usr/lib/x86_64-linux-gnu/mandos/plugin-helpers -- /usr/lib/x86_64-linux-gnu/mandos/plugins.d/mandos-client --pubkey=/etc/keys/mandos/pubkey.txt --seckey=/etc/keys/mandos/seckey.txt --tls-pubkey=/etc/keys/mandos/tls-pubkey.pem --tls-privkey=/etc/keys/mandos/tls-privkey.pem
+
+
+
+
+
+ Use the default location for
+ mandos-client
+ 8mandos, but add many
+ options to it:
+
+
+
+
+&COMMANDNAME; -- /lib/mandos/mandos-client --pubkey=/etc/mandos/keys/pubkey.txt --seckey=/etc/mandos/keys/seckey.txt --tls-pubkey=/etc/mandos/keys/tls-pubkey.pem --tls-privkey=/etc/mandos/keys/tls-privkey.pem
+
+
+
+
+
+ Only run the self-tests:
+
+
+ &COMMANDNAME; --test
+
+
+
+
+ SECURITY
+
+ This program will need to run as the root user in order to read
+ the agent directory and the ask.xxxx
files
+ there, and will, when starting the Mandos client program,
+ require the ability to set the real
user and
+ group ids to another user, by default user and group 65534,
+ which are assumed to be non-privileged. This is done in order
+ to match the expectations of mandos-client8mandos, which assumes that its executable file is
+ owned by the root user and also has the set-user-ID bit set (see
+ execve2).
+
+
+
+
+ SEE ALSO
+
+ intro
+ 8mandos,
+ mandos-client
+ 8mandos,
+ systemd
+ 1,
+
+
+
+
+ Password Agents
+
+
+
+ The specification for systemd Password
+ Agent
programs, which
+ &COMMANDNAME; follows.
+
+
+
+
+
+
+
+
+
+
+
+
=== added file 'init.d-mandos'
--- init.d-mandos 1970-01-01 00:00:00 +0000
+++ init.d-mandos 2018-02-10 13:23:58 +0000
@@ -0,0 +1,164 @@
+#! /bin/sh
+### BEGIN INIT INFO
+# Provides: mandos
+# Required-Start: $remote_fs $syslog avahi-daemon
+# Required-Stop: $remote_fs $syslog avahi-daemon
+# Default-Start: 2 3 4 5
+# Default-Stop: 0 1 6
+# Short-Description: Mandos server
+# Description: Server of encrypted passwords to Mandos clients
+### END INIT INFO
+
+# Author: Teddy Hogeborn
+# Author: Björn Påhlsson
+
+# Do NOT "set -e"
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/sbin:/usr/sbin:/bin:/usr/bin
+DESC="Mandos root file system password server"
+NAME=mandos
+DAEMON=/usr/sbin/$NAME
+DAEMON_ARGS=""
+if [ -d /run/. ]; then
+ PIDFILE=/run/$NAME.pid
+else
+ PIDFILE=/var/run/$NAME.pid
+fi
+SCRIPTNAME=/etc/init.d/$NAME
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+if [ -n "$CONFIGDIR" ]; then
+ DAEMON_ARGS="$DAEMON_ARGS --configdir $CONFIGDIR"
+fi
+
+# Load the VERBOSE setting and other rcS variables
+. /lib/init/vars.sh
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
+# and status_of_proc is working.
+. /lib/lsb/init-functions
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+ # Return
+ # 0 if daemon has been started
+ # 1 if daemon was already running
+ # 2 if daemon could not be started
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
+ || return 1
+ start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
+ $DAEMON_ARGS \
+ || return 2
+ # Add code here, if necessary, that waits for the process to be ready
+ # to handle requests from services started subsequently which depend
+ # on this one. As a last resort, sleep for some time.
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+ # Return
+ # 0 if daemon has been stopped
+ # 1 if daemon was already stopped
+ # 2 if daemon could not be stopped
+ # other if a failure occurred
+ start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
+ RETVAL="$?"
+ [ "$RETVAL" = 2 ] && return 2
+ # Wait for children to finish too if this is a daemon that forks
+ # and if the daemon is only ever run from this initscript.
+ # If the above conditions are not satisfied then add some other code
+ # that waits for the process to drop all resources that could be
+ # needed by services started subsequently. A last resort is to
+ # sleep for some time.
+ start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
+ [ "$?" = 2 ] && return 2
+ # Many daemons don't delete their pidfiles when they exit.
+ rm -f $PIDFILE
+ return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+ #
+ # If the daemon can reload its configuration without
+ # restarting (for example, when it is sent a SIGHUP),
+ # then implement that here.
+ #
+ start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
+ return 0
+}
+
+case "$1" in
+ start)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
+ do_start
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ stop)
+ [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
+ 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
+ esac
+ ;;
+ status)
+ status_of_proc "$DAEMON" "$NAME" -p "$PIDFILE" && exit 0 || exit $?
+ ;;
+ #reload|force-reload)
+ #
+ # If do_reload() is not implemented then leave this commented out
+ # and leave 'force-reload' as an alias for 'restart'.
+ #
+ #log_daemon_msg "Reloading $DESC" "$NAME"
+ #do_reload
+ #log_end_msg $?
+ #;;
+ restart|force-reload)
+ #
+ # If the "reload" option is implemented then remove the
+ # 'force-reload' alias
+ #
+ log_daemon_msg "Restarting $DESC" "$NAME"
+ do_stop
+ case "$?" in
+ 0|1)
+ do_start
+ case "$?" in
+ 0) log_end_msg 0 ;;
+ 1) log_end_msg 1 ;; # Old process is still running
+ *) log_end_msg 1 ;; # Failed to start
+ esac
+ ;;
+ *)
+ # Failed to stop
+ log_end_msg 1
+ ;;
+ esac
+ ;;
+ *)
+ #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
+ echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
+ exit 3
+ ;;
+esac
+
+:
=== added file 'initramfs-tools-conf'
--- initramfs-tools-conf 1970-01-01 00:00:00 +0000
+++ initramfs-tools-conf 2018-08-19 14:06:55 +0000
@@ -0,0 +1,17 @@
+# -*- shell-script -*-
+
+# Since the initramfs image will contain key files, we need to
+# restrict permissions on it by setting UMASK here.
+#
+# The proper place to set UMASK is (according to
+# /etc/cryptsetup-initramfs/conf-hook), in
+# /etc/initramfs-tools/initramfs.conf, which we shouldn't edit. The
+# corresponding directory for drop-in files from packages is
+# /usr/share/initramfs-tools/conf.d, and this file will be installed
+# there as "mandos-conf".
+#
+# This setting of UMASK will have unfortunate unintended side effects
+# on the files *inside* the initramfs, but these are later fixed by
+# "initramfs-tools-hook", installed as
+# "/usr/share/initramfs-tools/hooks/mandos".
+UMASK=0027
=== added file 'initramfs-tools-conf-hook'
--- initramfs-tools-conf-hook 1970-01-01 00:00:00 +0000
+++ initramfs-tools-conf-hook 2019-04-09 19:33:36 +0000
@@ -0,0 +1,14 @@
+# -*- shell-script -*-
+
+# The UMASK is set by the file "initramfs-tools-conf" (which is copied
+# to /usr/share/initramfs-tools/conf.d/mandos-conf on installation)
+# since there, as described therein, is the proper place to do that.
+# However, it is possible for other packages to override the UMASK in
+# any file in /usr/share/initramfs-tools/conf-hooks.d. Therefore,
+# this file ("initramfs-tools-conf-hook") will be installed as
+# "zz-mandos" in that directory to make sure UMASK is set correctly.
+
+# For more information on the effects of setting UMASK, see the
+# aforementioned /usr/share/initramfs-tools/conf.d/mandos-conf file.
+
+UMASK=0027
=== added file 'initramfs-tools-hook'
--- initramfs-tools-hook 1970-01-01 00:00:00 +0000
+++ initramfs-tools-hook 2018-08-19 14:06:55 +0000
@@ -0,0 +1,277 @@
+#!/bin/sh
+
+# This script will be run by 'mkinitramfs' when it creates the image.
+# Its job is to decide which files to install, then install them into
+# the staging area, where the initramfs is being created. This
+# happens when a new 'linux-image' package is installed, or when an
+# administrator runs 'update-initramfs' by hand to update an initramfs
+# image.
+
+# The environment contains at least:
+#
+# DESTDIR -- The staging directory where the image is being built.
+
+# No initramfs pre-requirements
+PREREQ="cryptroot"
+
+prereqs()
+{
+ echo "$PREREQ"
+}
+
+case $1 in
+# get pre-requisites
+prereqs)
+ prereqs
+ exit 0
+ ;;
+esac
+
+. /usr/share/initramfs-tools/hook-functions
+
+for d in /usr/lib \
+ "/usr/lib/`dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null`" \
+ "`rpm --eval='%{_libdir}' 2>/dev/null`" /usr/local/lib; do
+ if [ -d "$d"/mandos ]; then
+ libdir="$d"
+ break
+ fi
+done
+if [ -z "$libdir" ]; then
+ # Mandos not found
+ exit 1
+fi
+
+for d in /etc/keys/mandos /etc/mandos/keys; do
+ if [ -d "$d" ]; then
+ keydir="$d"
+ break
+ fi
+done
+if [ -z "$keydir" ]; then
+ # Mandos key directory not found
+ exit 1
+fi
+
+set `{ getent passwd _mandos \
+ || getent passwd nobody \
+ || echo ::65534:65534:::; } \
+ | cut --delimiter=: --fields=3,4 --only-delimited \
+ --output-delimiter=" "`
+mandos_user="$1"
+mandos_group="$2"
+
+# The Mandos network client uses the network
+auto_add_modules net
+# The Mandos network client uses IPv6
+force_load ipv6
+
+# These are directories inside the initrd
+CONFDIR="/conf/conf.d/mandos"
+MANDOSDIR="/lib/mandos"
+PLUGINDIR="${MANDOSDIR}/plugins.d"
+PLUGINHELPERDIR="${MANDOSDIR}/plugin-helpers"
+HOOKDIR="${MANDOSDIR}/network-hooks.d"
+
+# Make directories
+install --directory --mode=u=rwx,go=rx "${DESTDIR}${CONFDIR}" \
+ "${DESTDIR}${MANDOSDIR}" "${DESTDIR}${HOOKDIR}"
+install --owner=${mandos_user} --group=${mandos_group} --directory \
+ --mode=u=rwx "${DESTDIR}${PLUGINDIR}" \
+ "${DESTDIR}${PLUGINHELPERDIR}"
+
+copy_exec "$libdir"/mandos/mandos-to-cryptroot-unlock "${MANDOSDIR}"
+
+# Copy the Mandos plugin runner
+copy_exec "$libdir"/mandos/plugin-runner "${MANDOSDIR}"
+
+# Copy the plugins
+
+# Copy the packaged plugins
+for file in "$libdir"/mandos/plugins.d/*; do
+ base="`basename \"$file\"`"
+ # Is this plugin overridden?
+ if [ -e "/etc/mandos/plugins.d/$base" ]; then
+ continue
+ fi
+ case "$base" in
+ *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert)
+ : ;;
+ "*") echo "W: Mandos client plugin directory is empty." >&2 ;;
+ *) copy_exec "$file" "${PLUGINDIR}" ;;
+ esac
+done
+
+# Copy the packaged plugin helpers
+for file in "$libdir"/mandos/plugin-helpers/*; do
+ base="`basename \"$file\"`"
+ # Is this plugin overridden?
+ if [ -e "/etc/mandos/plugin-helpers/$base" ]; then
+ continue
+ fi
+ case "$base" in
+ *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert)
+ : ;;
+ "*") : ;;
+ *) copy_exec "$file" "${PLUGINHELPERDIR}" ;;
+ esac
+done
+
+# Copy any user-supplied plugins
+for file in /etc/mandos/plugins.d/*; do
+ base="`basename \"$file\"`"
+ case "$base" in
+ *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert)
+ : ;;
+ "*") : ;;
+ *) copy_exec "$file" "${PLUGINDIR}" ;;
+ esac
+done
+
+# Copy any user-supplied plugin helpers
+for file in /etc/mandos/plugin-helpers/*; do
+ base="`basename \"$file\"`"
+ case "$base" in
+ *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert)
+ : ;;
+ "*") : ;;
+ *) copy_exec "$file" "${PLUGINHELPERDIR}" ;;
+ esac
+done
+
+# Get DEVICE from initramfs.conf and other files
+. /etc/initramfs-tools/initramfs.conf
+for conf in /etc/initramfs-tools/conf.d/*; do
+ if [ -n `basename \"$conf\" | grep '^[[:alnum:]][[:alnum:]\._-]*$' \
+ | grep -v '\.dpkg-.*$'` ]; then
+ [ -f "${conf}" ] && . "${conf}"
+ fi
+done
+export DEVICE
+
+# Copy network hooks
+for hook in /etc/mandos/network-hooks.d/*; do
+ case "`basename \"$hook\"`" in
+ "*") continue ;;
+ *[!A-Za-z0-9_.-]*) continue ;;
+ *) test -d "$hook" || copy_exec "$hook" "${HOOKDIR}" ;;
+ esac
+ if [ -x "$hook" ]; then
+ # Copy any files needed by the network hook
+ MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=files \
+ VERBOSITY=0 "$hook" files | while read -r file target; do
+ if [ ! -e "${file}" ]; then
+ echo "WARNING: file ${file} not found, requested by Mandos network hook '${hook##*/}'" >&2
+ fi
+ if [ -z "${target}" ]; then
+ copy_exec "$file"
+ else
+ copy_exec "$file" "$target"
+ fi
+ done
+ # Copy and load any modules needed by the network hook
+ MANDOSNETHOOKDIR=/etc/mandos/network-hooks.d MODE=modules \
+ VERBOSITY=0 "$hook" modules | while read -r module; do
+ force_load "$module"
+ done
+ fi
+done
+
+# GPGME needs GnuPG
+gpg=/usr/bin/gpg
+libgpgme11_version="`dpkg-query --showformat='${Version}' --show libgpgme11`"
+if dpkg --compare-versions "$libgpgme11_version" ge 1.5.0-0.1; then
+ if [ -e /usr/bin/gpgconf ]; then
+ if [ ! -e "${DESTDIR}/usr/bin/gpgconf" ]; then
+ copy_exec /usr/bin/gpgconf
+ fi
+ gpg="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg:[^:]*://p'`"
+ gpgagent="`/usr/bin/gpgconf|sed --quiet --expression='s/^gpg-agent:[^:]*://p'`"
+ # Newer versions of GnuPG 2 requires the gpg-agent binary
+ if [ -e "$gpgagent" ] && [ ! -e "${DESTDIR}$gpgagent" ]; then
+ copy_exec "$gpgagent"
+ fi
+ fi
+elif dpkg --compare-versions "$libgpgme11_version" ge 1.4.1-0.1; then
+ gpg=/usr/bin/gpg2
+fi
+if [ ! -e "${DESTDIR}$gpg" ]; then
+ copy_exec "$gpg"
+fi
+unset gpg
+unset libgpgme11_version
+
+# Config files
+for file in /etc/mandos/plugin-runner.conf; do
+ if [ -d "$file" ]; then
+ continue
+ fi
+ cp --archive --sparse=always "$file" "${DESTDIR}${CONFDIR}"
+done
+
+if [ ${mandos_user} != 65534 ]; then
+ sed --in-place --expression="1i--userid=${mandos_user}" \
+ "${DESTDIR}${CONFDIR}/plugin-runner.conf"
+fi
+
+if [ ${mandos_group} != 65534 ]; then
+ sed --in-place --expression="1i--groupid=${mandos_group}" \
+ "${DESTDIR}${CONFDIR}/plugin-runner.conf"
+fi
+
+# Key files
+for file in "$keydir"/*; do
+ if [ -d "$file" ]; then
+ continue
+ fi
+ case "$file" in
+ *~|.*|\#*\#|*.dpkg-old|*.dpkg-bak|*.dpkg-new|*.dpkg-divert)
+ : ;;
+ "*") : ;;
+ *)
+ cp --archive --sparse=always "$file" \
+ "${DESTDIR}${CONFDIR}"
+ chown ${mandos_user}:${mandos_group} \
+ "${DESTDIR}${CONFDIR}/`basename \"$file\"`"
+ ;;
+ esac
+done
+# Use Diffie-Hellman parameters file if available
+if [ -e "${DESTDIR}${CONFDIR}"/dhparams.pem ]; then
+ sed --in-place \
+ --expression="1i--options-for=mandos-client:--dh-params=${CONFDIR}/dhparams.pem" \
+ "${DESTDIR}/${CONFDIR}/plugin-runner.conf"
+fi
+
+# /lib/mandos/plugin-runner will drop priviliges, but needs access to
+# its plugin directory and its config file. However, since almost all
+# files in initrd have been created with umask 027, this opening of
+# permissions is needed.
+#
+# (The umask is not really intended to affect the files inside the
+# initrd; it is intended to affect the initrd.img file itself, since
+# it now contains secret key files. There is, however, no other way
+# to set the permission of the initrd.img file without a race
+# condition. This umask is set by "initramfs-tools-conf", installed
+# as "/usr/share/initramfs-tools/conf.d/mandos-conf".)
+#
+for full in "${MANDOSDIR}" "${CONFDIR}"; do
+ while [ "$full" != "/" ]; do
+ chmod a+rX "${DESTDIR}$full"
+ full="`dirname \"$full\"`"
+ done
+done
+
+# Reset some other things to sane permissions which we have
+# inadvertently affected with our umask setting.
+for dir in / /bin /etc /keyscripts /sbin /scripts /usr /usr/bin; do
+ if [ -d "${DESTDIR}$dir" ]; then
+ chmod a+rX "${DESTDIR}$dir"
+ fi
+done
+for dir in "${DESTDIR}"/lib* "${DESTDIR}"/usr/lib*; do
+ if [ -d "$dir" ]; then
+ find "$dir" \! -perm -u+rw,g+r -prune -or \! -type l -print0 \
+ | xargs --null --no-run-if-empty chmod a+rX --
+ fi
+done
=== added file 'initramfs-tools-script'
--- initramfs-tools-script 1970-01-01 00:00:00 +0000
+++ initramfs-tools-script 2018-08-19 01:35:11 +0000
@@ -0,0 +1,181 @@
+#!/bin/sh -e
+#
+# This script will run in the initrd environment at boot and edit
+# /conf/conf.d/cryptroot to set /lib/mandos/plugin-runner as keyscript
+# when no other keyscript is set, before cryptsetup.
+#
+
+# This script should be installed as
+# "/usr/share/initramfs-tools/scripts/init-premount/mandos" which will
+# eventually be "/scripts/init-premount/mandos" in the initrd.img
+# file.
+
+PREREQ="udev"
+prereqs()
+{
+ echo "$PREREQ"
+}
+
+case $1 in
+prereqs)
+ prereqs
+ exit 0
+ ;;
+esac
+
+. /scripts/functions
+
+for param in `cat /proc/cmdline`; do
+ case "$param" in
+ ip=*) IPOPTS="${param#ip=}" ;;
+ mandos=*)
+ # Split option line on commas
+ old_ifs="$IFS"
+ IFS="$IFS,"
+ for mpar in ${param#mandos=}; do
+ IFS="$old_ifs"
+ case "$mpar" in
+ off) exit 0 ;;
+ connect) connect="" ;;
+ connect:*) connect="${mpar#connect:}" ;;
+ *) log_warning_msg "$0: Bad option ${mpar}" ;;
+ esac
+ done
+ unset mpar
+ IFS="$old_ifs"
+ unset old_ifs
+ ;;
+ esac
+done
+unset param
+
+chmod a=rwxt /tmp
+
+# Get DEVICE from /conf/initramfs.conf and other files
+. /conf/initramfs.conf
+for conf in /conf/conf.d/*; do
+ [ -f "${conf}" ] && . "${conf}"
+done
+if [ -e /conf/param.conf ]; then
+ . /conf/param.conf
+fi
+
+# Override DEVICE from sixth field of ip= kernel option, if passed
+case "$IPOPTS" in
+ *:*:*:*:*:*) # At least six fields
+ # Remove the first five fields
+ device="${IPOPTS#*:*:*:*:*:}"
+ # Remove all fields except the first one
+ DEVICE="${device%%:*}"
+ ;;
+esac
+
+# Add device setting (if any) to plugin-runner.conf
+if [ "${DEVICE+set}" = set ]; then
+ # Did we get the device from an ip= option?
+ if [ "${device+set}" = set ]; then
+ # Let ip= option override local config; append:
+ cat <<-EOF >>/conf/conf.d/mandos/plugin-runner.conf
+
+ --options-for=mandos-client:--interface=${DEVICE}
+EOF
+ else
+ # Prepend device setting so any later options would override:
+ sed -i -e \
+ '1i--options-for=mandos-client:--interface='"${DEVICE}" \
+ /conf/conf.d/mandos/plugin-runner.conf
+ fi
+fi
+unset device
+
+# If we are connecting directly, run "configure_networking" (from
+# /scripts/functions); it needs IPOPTS and DEVICE
+if [ "${connect+set}" = set ]; then
+ set +e # Required by library functions
+ configure_networking
+ set -e
+ if [ -n "$connect" ]; then
+ cat <<-EOF >>/conf/conf.d/mandos/plugin-runner.conf
+
+ --options-for=mandos-client:--connect=${connect}
+EOF
+ fi
+fi
+
+if [ -r /conf/conf.d/cryptroot ]; then
+ test -w /conf/conf.d
+
+ # Do not replace cryptroot file unless we need to.
+ replace_cryptroot=no
+
+ # Our keyscript
+ mandos=/lib/mandos/plugin-runner
+ test -x "$mandos"
+
+ # parse /conf/conf.d/cryptroot. Format:
+ # target=sda2_crypt,source=/dev/sda2,rootdev,key=none,keyscript=/foo/bar/baz
+ # Is the root device specially marked?
+ changeall=yes
+ while read -r options; do
+ case "$options" in
+ rootdev,*|*,rootdev,*|*,rootdev)
+ # If the root device is specially marked, don't change all
+ # lines in crypttab by default.
+ changeall=no
+ ;;
+ esac
+ done < /conf/conf.d/cryptroot
+
+ exec 3>/conf/conf.d/cryptroot.mandos
+ while read -r options; do
+ newopts=""
+ keyscript=""
+ changethis="$changeall"
+ # Split option line on commas
+ old_ifs="$IFS"
+ IFS="$IFS,"
+ for opt in $options; do
+ # Find the keyscript option, if any
+ case "$opt" in
+ keyscript=*)
+ keyscript="${opt#keyscript=}"
+ newopts="$newopts,$opt"
+ ;;
+ "") : ;;
+ # Always use Mandos on the root device, if marked
+ rootdev)
+ changethis=yes
+ newopts="$newopts,$opt"
+ ;;
+ # Don't use Mandos on resume device, if marked
+ resumedev)
+ changethis=no
+ newopts="$newopts,$opt"
+ ;;
+ *)
+ newopts="$newopts,$opt"
+ ;;
+ esac
+ done
+ IFS="$old_ifs"
+ unset old_ifs
+ # If there was no keyscript option, add one.
+ if [ "$changethis" = yes ] && [ -z "$keyscript" ]; then
+ replace_cryptroot=yes
+ newopts="$newopts,keyscript=$mandos"
+ fi
+ newopts="${newopts#,}"
+ echo "$newopts" >&3
+ done < /conf/conf.d/cryptroot
+ exec 3>&-
+
+ # If we need to, replace the old cryptroot file with the new file.
+ if [ "$replace_cryptroot" = yes ]; then
+ mv /conf/conf.d/cryptroot /conf/conf.d/cryptroot.mandos-old
+ mv /conf/conf.d/cryptroot.mandos /conf/conf.d/cryptroot
+ else
+ rm -f /conf/conf.d/cryptroot.mandos
+ fi
+elif [ -x /usr/bin/cryptroot-unlock ]; then
+ setsid /lib/mandos/mandos-to-cryptroot-unlock &
+fi
=== added file 'initramfs-tools-script-stop'
--- initramfs-tools-script-stop 1970-01-01 00:00:00 +0000
+++ initramfs-tools-script-stop 2018-08-19 14:58:40 +0000
@@ -0,0 +1,65 @@
+#!/bin/sh -e
+#
+# Script to wait for plugin-runner to exit before continuing boot
+#
+# Copyright © 2018 Teddy Hogeborn
+# Copyright © 2018 Björn Påhlsson
+#
+# This file is part of Mandos.
+#
+# Mandos 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.
+#
+# Mandos 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 Mandos. If not, see .
+#
+# Contact the authors at .
+#
+# This script will run in the initrd environment at boot and remove
+# the file keeping the dummy plugin running, forcing plugin-runner to
+# exit if it is still running.
+
+# This script should be installed as
+# "/usr/share/initramfs-tools/scripts/local-premount/mandos" which will
+# eventually be "/scripts/local-premount/mandos" in the initrd.img
+# file.
+
+PREREQ=""
+prereqs()
+{
+ echo "$PREREQ"
+}
+
+case $1 in
+prereqs)
+ prereqs
+ exit 0
+ ;;
+esac
+
+. /scripts/functions
+
+pid=$(cat /run/mandos-plugin-runner.pid 2>/dev/null)
+
+# If the dummy plugin is running, removing this file should force the
+# dummy plugin to exit successfully, thereby making plugin-runner shut
+# down all its other plugins and then exit itself.
+rm -f /run/mandos-keep-running >/dev/null 2>&1
+
+# Wait for exit of plugin-runner, if still running
+if [ -n "$pid" ]; then
+ while :; do
+ case "$(readlink /proc/"$pid"/exe 2>/dev/null)" in
+ */plugin-runner) sleep 1;;
+ *) break;;
+ esac
+ done
+ rm -f /run/mandos-plugin-runner.pid >/dev/null 2>&1
+fi
=== added file 'initramfs-unpack'
--- initramfs-unpack 1970-01-01 00:00:00 +0000
+++ initramfs-unpack 2019-07-27 10:11:45 +0000
@@ -0,0 +1,84 @@
+#!/bin/bash
+#
+# Initramfs unpacker - unpacks initramfs images into /tmp
+#
+# Copyright © 2013-2019 Teddy Hogeborn
+# Copyright © 2013-2019 Björn Påhlsson
+#
+# This file is part of Mandos.
+#
+# Mandos 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.
+#
+# Mandos 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 Mandos. If not, see
+# .
+#
+# Contact the authors at .
+
+cpio="cpio --extract --make-directories --unconditional --preserve-modification-time"
+
+if [ -z "$*" ]; then
+ set -- /boot/initrd.img-*
+fi
+
+for imgfile in "$@"; do
+ if ! [ -f "$imgfile" ]; then
+ echo "Error: Not an existing file: $imgfile" >&2
+ continue
+ fi
+ imgdir="${TMPDIR:-/tmp}/${imgfile##*/}"
+ if [ -d "$imgdir" ]; then
+ rm --recursive -- "$imgdir"
+ fi
+ mkdir --parents "$imgdir"
+ # Does this image contain microcode?
+ if $cpio --quiet --list --file="$imgfile" >/dev/null 2>&1; then
+ # Number of bytes to skip to get to the compressed archive
+ skip=$(($(LANG=C $cpio --io-size=1 --list --file="$imgfile" 2>&1 \
+ | sed --quiet \
+ --expression='s/^\([0-9]\+\) blocks$/\1/p')+8))
+ if [ -x /usr/lib/dracut/skipcpio ]; then
+ catimg="/usr/lib/dracut/skipcpio $imgfile"
+ else
+ catimg="dd if=$imgfile bs=$skip skip=1 status=noxfer"
+ fi
+ else
+ echo "No microcode detected"
+ catimg="cat -- $imgfile"
+ fi
+ while :; do
+ # Determine the compression method
+ if { $catimg 2>/dev/null | zcat --test >/dev/null 2>&1;
+ [ ${PIPESTATUS[-1]} -eq 0 ]; }; then
+ decomp="zcat"
+ elif { $catimg 2>/dev/null | bzip2 --test >/dev/null 2>&1;
+ [ ${PIPESTATUS[-1]} -eq 0 ]; }; then
+ decomp="bzip2 --stdout --decompress"
+ elif { $catimg 2>/dev/null | lzop --test >/dev/null 2>&1;
+ [ ${PIPESTATUS[-1]} -eq 0 ]; }; then
+ decomp="lzop --stdout --decompress"
+ else
+ skip=$((${skip}+1))
+ echo "Could not determine compression of ${imgfile}; trying to skip ${skip} bytes" >&2
+ catimg="dd if=$imgfile bs=$skip skip=1 status=noxfer"
+ continue
+ fi
+ break
+ done
+ case "$catimg" in
+ *skipcpio*) echo "Microcode detected, skipping";;
+ *) echo "Microcode detected, skipping ${skip} bytes";;
+ esac
+ $catimg 2>/dev/null | $decomp | ( cd -- "$imgdir" && $cpio --quiet )
+ if [ ${PIPESTATUS[-1]} -eq 0 ]; then
+ echo "$imgfile unpacked into $imgdir"
+ fi
+done
=== added file 'intro.xml'
--- intro.xml 1970-01-01 00:00:00 +0000
+++ intro.xml 2019-08-04 12:42:49 +0000
@@ -0,0 +1,472 @@
+
+
+
+%common;
+]>
+
+
+
+ Mandos Manual
+
+ Mandos
+ &version;
+ &TIMESTAMP;
+
+
+ Björn
+ Påhlsson
+
+ belorn@recompile.se
+
+
+
+ Teddy
+ Hogeborn
+
+ teddy@recompile.se
+
+
+
+
+ 2011
+ 2012
+ 2013
+ 2014
+ 2015
+ 2016
+ 2017
+ 2018
+ 2019
+ Teddy Hogeborn
+ Björn Påhlsson
+
+
+
+
+
+ intro
+ 8mandos
+
+
+
+ intro
+
+ Introduction to the Mandos system
+
+
+
+
+ DESCRIPTION
+
+ This is the the Mandos system, which allows computers to have
+ encrypted root file systems and at the same time be capable of
+ remote and/or unattended reboots.
+
+
+ The computers run a small client program in the initial RAM disk
+ environment which will communicate with a server over a network.
+ All network communication is encrypted using TLS. The clients
+ are identified by the server using a TLS public key; each client
+ has one unique to it. The server sends the clients an encrypted
+ password. The encrypted password is decrypted by the clients
+ using a separate OpenPGP key, and the password is then used to
+ unlock the root file system, whereupon the computers can
+ continue booting normally.
+
+
+
+
+ INTRODUCTION
+
+
+ You know how it is. You’ve heard of it happening. The Man
+ comes and takes away your servers, your friends’ servers, the
+ servers of everybody in the same hosting facility. The servers
+ of their neighbors, and their neighbors’ friends. The servers
+ of people who owe them money. And like
+ that, they’re gone. And you doubt you’ll
+ ever see them again.
+
+
+ That is why your servers have encrypted root file systems.
+ However, there’s a downside. There’s no going around it:
+ rebooting is a pain. Dragging out that rarely-used keyboard and
+ screen and unraveling cables behind your servers to plug them in
+ to type in that password is messy, especially if you have many
+ servers. There are some people who do clever things like using
+ serial line consoles and daisy-chain it to the next server, and
+ keep all the servers connected in a ring with serial cables,
+ which will work, if your servers are physically close enough.
+ There are also other out-of-band management solutions, but with
+ all these, you still have to be on hand and
+ manually type in the password at boot time. Otherwise the
+ server just sits there, waiting for a password.
+
+
+ Wouldn’t it be great if you could have the security of encrypted
+ root file systems and still have servers that could boot up
+ automatically if there was a short power outage while you were
+ asleep? That you could reboot at will, without having someone
+ run over to the server to type in the password?
+
+
+ Well, with Mandos, you (almost) can! The gain in convenience
+ will only be offset by a small loss in security. The setup is
+ as follows:
+
+
+ The server will still have its encrypted root file system. The
+ password to this file system will be stored on another computer
+ (henceforth known as the Mandos server) on the same local
+ network. The password will not be stored
+ in plaintext, but encrypted with OpenPGP. To decrypt this
+ password, a key is needed. This key (the Mandos client key)
+ will not be stored there, but back on the original server
+ (henceforth known as the Mandos client) in the initial RAM disk
+ image. Oh, and all network Mandos client/server communications
+ will be encrypted, using TLS (SSL).
+
+
+ So, at boot time, the Mandos client will ask for its encrypted
+ data over the network, decrypt the data to get the password, use
+ the password to decrypt the root file system, and the client can
+ then continue booting.
+
+
+ Now, of course the initial RAM disk image is not on the
+ encrypted root file system, so anyone who had physical access
+ could take the Mandos client computer offline and read the disk
+ with their own tools to get the authentication keys used by a
+ client. But, by then the Mandos server
+ should notice that the original server has been offline for too
+ long, and will no longer give out the encrypted key. The timing
+ here is the only real weak point, and the method, frequency and
+ timeout of the server’s checking can be adjusted to any desired
+ level of paranoia.
+
+
+ (The encrypted keys on the Mandos server is on its normal file
+ system, so those are safe, provided the root file system of
+ that server is encrypted.)
+
+
+
+
+ FREQUENTLY ASKED QUESTIONS
+
+ Couldn’t the security be defeated by…
+
+
+ Grabbing the Mandos client key from the
+ initrd really quickly?
+
+ This, as mentioned above, is the only real weak point. But if
+ you set the timing values tight enough, this will be really
+ difficult to do. An attacker would have to physically
+ disassemble the client computer, extract the key from the
+ initial RAM disk image, and then connect to a still
+ online Mandos server to get the encrypted key, and do
+ all this before the Mandos server timeout
+ kicks in and the Mandos server refuses to give out the key to
+ anyone.
+
+
+ Now, as the typical procedure seems to be to barge in and turn
+ off and grab all computers, to maybe look
+ at them months later, this is not likely. If someone does that,
+ the whole system will lock itself up
+ completely, since Mandos servers are no longer running.
+
+
+ For sophisticated attackers who could do
+ the clever thing, and had physical access
+ to the server for enough time, it would be simpler to get a key
+ for an encrypted file system by using hardware memory scanners
+ and reading it right off the memory bus.
+
+
+
+
+ Replay attacks?
+
+ Nope, the network stuff is all done over TLS, which provides
+ protection against that.
+
+
+
+
+ Man-in-the-middle?
+
+ No. The server only gives out the passwords to clients which
+ have in the TLS handshake proven that
+ they do indeed hold the private key corresponding to that
+ client.
+
+
+
+
+ How about sniffing the network traffic and decrypting it
+ later by physically grabbing the Mandos client and using its
+ key?
+
+ We only use PFS (Perfect Forward Security)
+ key exchange algorithms in TLS, which protects against this.
+
+
+
+
+ Physically grabbing the Mandos server computer?
+
+ You could protect that computer the
+ old-fashioned way, with a must-type-in-the-password-at-boot
+ method. Or you could have two computers be the Mandos server
+ for each other.
+
+
+ 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 reboot at the same
+ time they will stay down until someone types in the password
+ on one of them.
+
+
+
+
+ Faking checker results?
+
+ If the Mandos client does not have an SSH server, the default
+ is for the Mandos server to use
+ fping
, the replies to which
+ could be faked to eliminate the timeout. But this could
+ easily be changed to any shell command, with any security
+ measures you like. If the Mandos client
+ has an SSH server, the default
+ configuration (as generated by
+ mandos-keygen with the
+ option) is for the Mandos server
+ to use an ssh-keyscan command with strict
+ keychecking, which can not be faked. Alternatively, IPsec
+ could be used for the ping packets, making them secure.
+
+
+
+
+
+ SECURITY
+
+ So, in summary: The only weakness in the Mandos system is from
+ people who have:
+
+
+
+
+ The power to come in and physically take your servers,
+ and
+
+
+
+
+ The cunning and patience to do it carefully, one at a time,
+ and quickly, faking Mandos
+ client/server responses for each one before the timeout.
+
+
+
+
+ While there are some who may be threatened by people who have
+ both these attributes, they do not,
+ probably, constitute the majority.
+
+
+ If you do face such opponents, you must
+ figure that they could just as well open your servers and read
+ the file system keys right off the memory by running wires to
+ the memory bus.
+
+
+ What Mandos is designed to protect against is
+ not such determined, focused, and competent
+ attacks, but against the early morning knock on your door and
+ the sudden absence of all the servers in your server room.
+ Which it does nicely.
+
+
+
+
+ PLUGINS
+
+ In the early designs, the
+ mandos-client8mandos program (which
+ retrieves a password from the Mandos server) also prompted for a
+ password on the terminal, in case a Mandos server could not be
+ found. Other ways of retrieving a password could easily be
+ envisoned, but this multiplicity of purpose was seen to be too
+ complex to be a viable way to continue. Instead, the original
+ program was separated into mandos-client8mandos and password-prompt8mandos, and a plugin-runner8mandos exist to run them both in parallel, allowing
+ the first successful plugin to provide the password. This
+ opened up for any number of additional plugins to run, all
+ competing to be the first to find a password and provide it to
+ the plugin runner.
+
+
+ Four additional plugins are provided:
+
+
+
+
+ plymouth
+ 8mandos
+
+
+
+ This prompts for a password when using
+ plymouth8.
+
+
+
+
+
+ usplash
+ 8mandos
+
+
+
+ This prompts for a password when using
+ usplash8.
+
+
+
+
+
+ splashy
+ 8mandos
+
+
+
+ This prompts for a password when using
+ splashy8.
+
+
+
+
+
+ askpass-fifo
+ 8mandos
+
+
+
+ To provide compatibility with the "askpass" program from
+ cryptsetup, this plugin listens to the same FIFO as
+ askpass would do.
+
+
+
+
+
+ More plugins can easily be written and added by the system
+ administrator; see the section called "WRITING PLUGINS" in
+ plugin-runner
+ 8mandos to learn the
+ plugin requirements.
+
+
+
+
+ SYSTEMD
+
+ More advanced startup systems like systemd1,
+ already have their own plugin-like mechanisms for allowing
+ multiple agents to independently retrieve a password and deliver
+ it to the subsystem requesting a password to unlock the root
+ file system. On these systems, it would make no sense to run
+ plugin-runner8mandos, the plugins of
+ which would largely duplicate the work of (and conflict with)
+ the existing systems prompting for passwords.
+
+
+ As for systemd1 in particular, it has
+ its own Password Agents system. Mandos uses this via its
+ password-agent8mandos program, which
+ is run instead of plugin-runner8mandos when systemd1
+ is used during system startup.
+
+
+
+ BUGS
+
+
+
+
+ SEE ALSO
+
+ mandos
+ 8,
+ mandos.conf
+ 5,
+ mandos-clients.conf
+ 5,
+ mandos-ctl
+ 8,
+ mandos-monitor
+ 8,
+ plugin-runner
+ 8mandos,
+ password-agent
+ 8mandos,
+ mandos-client
+ 8mandos,
+ password-prompt
+ 8mandos,
+ plymouth
+ 8mandos,
+ usplash
+ 8mandos,
+ splashy
+ 8mandos,
+ askpass-fifo
+ 8mandos,
+ mandos-keygen
+ 8
+
+
+
+
+ Mandos
+
+
+
+ The Mandos home page.
+
+
+
+
+
+
+
+
+
+
+
=== added file 'legalnotice.xml'
--- legalnotice.xml 1970-01-01 00:00:00 +0000
+++ legalnotice.xml 2017-08-20 16:20:54 +0000
@@ -0,0 +1,28 @@
+
+
+
+
+ This manual page is part of Mandos.
+
+
+
+ Mandos 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.
+
+
+
+ Mandos 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 Mandos. If not, see http://www.gnu.org/licenses/.
+
+
=== modified file 'mandos'
--- mandos 2008-08-10 16:13:23 +0000
+++ mandos 2019-11-03 19:09:41 +0000
@@ -1,102 +1,343 @@
-#!/usr/bin/python
-# -*- mode: python; coding: utf-8 -*-
-#
+#!/usr/bin/python3 -bI
+# -*- mode: python; after-save-hook: (lambda () (let ((command (if (fboundp 'file-local-name) (file-local-name (buffer-file-name)) (or (file-remote-p (buffer-file-name) 'localname) (buffer-file-name))))) (if (= (progn (if (get-buffer "*Test*") (kill-buffer "*Test*")) (process-file-shell-command (format "%s --check" (shell-quote-argument command)) nil "*Test*")) 0) (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w))) (progn (with-current-buffer "*Test*" (compilation-mode)) (display-buffer "*Test*" '(display-buffer-in-side-window)))))); coding: utf-8 -*-
+#
# Mandos server - give out binary blobs to connecting clients.
-#
+#
# This program is partly derived from an example program for an Avahi
# service publisher, downloaded from
# . This includes the
-# methods "add" and "remove" in the "AvahiService" class, the
-# "server_state_changed" and "entry_group_state_changed" functions,
-# and some lines in "main".
-#
+# methods "add", "remove", "server_state_changed",
+# "entry_group_state_changed", "cleanup", and "activate" in the
+# "AvahiService" class, and some lines in "main".
+#
# Everything else is
-# Copyright © 2007-2008 Teddy Hogeborn & 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
+# Copyright © 2008-2019 Teddy Hogeborn
+# Copyright © 2008-2019 Björn Påhlsson
+#
+# This file is part of Mandos.
+#
+# Mandos 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
+# Mandos 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 SocketServer
+# along with Mandos. If not, see .
+#
+# Contact the authors at .
+#
+
+from __future__ import (division, absolute_import, print_function,
+ unicode_literals)
+
+try:
+ from future_builtins import *
+except ImportError:
+ pass
+
+try:
+ import SocketServer as socketserver
+except ImportError:
+ import socketserver
import socket
-import select
-from optparse import OptionParser
+import argparse
import datetime
import errno
-import gnutls.crypto
-import gnutls.connection
-import gnutls.errors
-import gnutls.library.functions
-import gnutls.library.constants
-import gnutls.library.types
-import ConfigParser
+try:
+ import ConfigParser as configparser
+except ImportError:
+ import configparser
import sys
import re
import os
import signal
-from sets import Set
import subprocess
import atexit
import stat
import logging
import logging.handlers
+import pwd
+import contextlib
+import struct
+import fcntl
+import functools
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+import multiprocessing
+import types
+import binascii
+import tempfile
+import itertools
+import collections
+import codecs
+import unittest
import dbus
-import gobject
-import avahi
+import dbus.service
+import gi
+from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop
import ctypes
-
-version = "1.0"
-
-logger = logging.Logger('mandos')
-syslogger = logging.handlers.SysLogHandler\
- (facility = logging.handlers.SysLogHandler.LOG_DAEMON,
- address = "/dev/log")
-syslogger.setFormatter(logging.Formatter\
- ('Mandos: %(levelname)s: %(message)s'))
-logger.addHandler(syslogger)
-
-console = logging.StreamHandler()
-console.setFormatter(logging.Formatter('%(name)s: %(levelname)s:'
- ' %(message)s'))
-logger.addHandler(console)
+import ctypes.util
+import xml.dom.minidom
+import inspect
+
+if sys.version_info.major == 2:
+ __metaclass__ = type
+
+# Show warnings by default
+if not sys.warnoptions:
+ import warnings
+ warnings.simplefilter("default")
+
+# Try to find the value of SO_BINDTODEVICE:
+try:
+ # This is where SO_BINDTODEVICE is in Python 3.3 (or 3.4?) and
+ # newer, and it is also the most natural place for it:
+ SO_BINDTODEVICE = socket.SO_BINDTODEVICE
+except AttributeError:
+ try:
+ # This is where SO_BINDTODEVICE was up to and including Python
+ # 2.6, and also 3.2:
+ from IN import SO_BINDTODEVICE
+ except ImportError:
+ # In Python 2.7 it seems to have been removed entirely.
+ # Try running the C preprocessor:
+ try:
+ cc = subprocess.Popen(["cc", "--language=c", "-E",
+ "/dev/stdin"],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ stdout = cc.communicate(
+ "#include \nSO_BINDTODEVICE\n")[0]
+ SO_BINDTODEVICE = int(stdout.splitlines()[-1])
+ except (OSError, ValueError, IndexError):
+ # No value found
+ SO_BINDTODEVICE = None
+
+if sys.version_info.major == 2:
+ str = unicode
+
+if sys.version_info < (3, 2):
+ configparser.Configparser = configparser.SafeConfigParser
+
+version = "1.8.9"
+stored_state_file = "clients.pickle"
+
+logger = logging.getLogger()
+logging.captureWarnings(True) # Show warnings via the logging system
+syslogger = None
+
+try:
+ if_nametoindex = ctypes.cdll.LoadLibrary(
+ ctypes.util.find_library("c")).if_nametoindex
+except (OSError, AttributeError):
+
+ def if_nametoindex(interface):
+ "Get an interface index the hard way, i.e. using fcntl()"
+ SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
+ with contextlib.closing(socket.socket()) as s:
+ ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
+ struct.pack(b"16s16x", interface))
+ interface_index = struct.unpack("I", ifreq[16:20])[0]
+ return interface_index
+
+
+def copy_function(func):
+ """Make a copy of a function"""
+ if sys.version_info.major == 2:
+ return types.FunctionType(func.func_code,
+ func.func_globals,
+ func.func_name,
+ func.func_defaults,
+ func.func_closure)
+ else:
+ return types.FunctionType(func.__code__,
+ func.__globals__,
+ func.__name__,
+ func.__defaults__,
+ func.__closure__)
+
+
+def initlogger(debug, level=logging.WARNING):
+ """init logger and add loglevel"""
+
+ global syslogger
+ syslogger = (logging.handlers.SysLogHandler(
+ facility=logging.handlers.SysLogHandler.LOG_DAEMON,
+ address="/dev/log"))
+ syslogger.setFormatter(logging.Formatter
+ ('Mandos [%(process)d]: %(levelname)s:'
+ ' %(message)s'))
+ logger.addHandler(syslogger)
+
+ if debug:
+ console = logging.StreamHandler()
+ console.setFormatter(logging.Formatter('%(asctime)s %(name)s'
+ ' [%(process)d]:'
+ ' %(levelname)s:'
+ ' %(message)s'))
+ logger.addHandler(console)
+ logger.setLevel(level)
+
+
+class PGPError(Exception):
+ """Exception if encryption/decryption fails"""
+ pass
+
+
+class PGPEngine:
+ """A simple class for OpenPGP symmetric encryption & decryption"""
+
+ def __init__(self):
+ self.tempdir = tempfile.mkdtemp(prefix="mandos-")
+ self.gpg = "gpg"
+ try:
+ output = subprocess.check_output(["gpgconf"])
+ for line in output.splitlines():
+ name, text, path = line.split(b":")
+ if name == b"gpg":
+ self.gpg = path
+ break
+ except OSError as e:
+ if e.errno != errno.ENOENT:
+ raise
+ self.gnupgargs = ['--batch',
+ '--homedir', self.tempdir,
+ '--force-mdc',
+ '--quiet']
+ # Only GPG version 1 has the --no-use-agent option.
+ if self.gpg == b"gpg" or self.gpg.endswith(b"/gpg"):
+ self.gnupgargs.append("--no-use-agent")
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ self._cleanup()
+ return False
+
+ def __del__(self):
+ self._cleanup()
+
+ def _cleanup(self):
+ if self.tempdir is not None:
+ # Delete contents of tempdir
+ for root, dirs, files in os.walk(self.tempdir,
+ topdown=False):
+ for filename in files:
+ os.remove(os.path.join(root, filename))
+ for dirname in dirs:
+ os.rmdir(os.path.join(root, dirname))
+ # Remove tempdir
+ os.rmdir(self.tempdir)
+ self.tempdir = None
+
+ def password_encode(self, password):
+ # Passphrase can not be empty and can not contain newlines or
+ # NUL bytes. So we prefix it and hex encode it.
+ encoded = b"mandos" + binascii.hexlify(password)
+ if len(encoded) > 2048:
+ # GnuPG can't handle long passwords, so encode differently
+ encoded = (b"mandos" + password.replace(b"\\", b"\\\\")
+ .replace(b"\n", b"\\n")
+ .replace(b"\0", b"\\x00"))
+ return encoded
+
+ def encrypt(self, data, password):
+ passphrase = self.password_encode(password)
+ with tempfile.NamedTemporaryFile(
+ dir=self.tempdir) as passfile:
+ passfile.write(passphrase)
+ passfile.flush()
+ proc = subprocess.Popen([self.gpg, '--symmetric',
+ '--passphrase-file',
+ passfile.name]
+ + self.gnupgargs,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ ciphertext, err = proc.communicate(input=data)
+ if proc.returncode != 0:
+ raise PGPError(err)
+ return ciphertext
+
+ def decrypt(self, data, password):
+ passphrase = self.password_encode(password)
+ with tempfile.NamedTemporaryFile(
+ dir=self.tempdir) as passfile:
+ passfile.write(passphrase)
+ passfile.flush()
+ proc = subprocess.Popen([self.gpg, '--decrypt',
+ '--passphrase-file',
+ passfile.name]
+ + self.gnupgargs,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ decrypted_plaintext, err = proc.communicate(input=data)
+ if proc.returncode != 0:
+ raise PGPError(err)
+ return decrypted_plaintext
+
+
+# Pretend that we have an Avahi module
+class avahi:
+ """This isn't so much a class as it is a module-like namespace."""
+ IF_UNSPEC = -1 # avahi-common/address.h
+ PROTO_UNSPEC = -1 # avahi-common/address.h
+ PROTO_INET = 0 # avahi-common/address.h
+ PROTO_INET6 = 1 # avahi-common/address.h
+ DBUS_NAME = "org.freedesktop.Avahi"
+ DBUS_INTERFACE_ENTRY_GROUP = DBUS_NAME + ".EntryGroup"
+ DBUS_INTERFACE_SERVER = DBUS_NAME + ".Server"
+ DBUS_PATH_SERVER = "/"
+
+ @staticmethod
+ def string_array_to_txt_array(t):
+ return dbus.Array((dbus.ByteArray(s.encode("utf-8"))
+ for s in t), signature="ay")
+ ENTRY_GROUP_ESTABLISHED = 2 # avahi-common/defs.h
+ ENTRY_GROUP_COLLISION = 3 # avahi-common/defs.h
+ ENTRY_GROUP_FAILURE = 4 # avahi-common/defs.h
+ SERVER_INVALID = 0 # avahi-common/defs.h
+ SERVER_REGISTERING = 1 # avahi-common/defs.h
+ SERVER_RUNNING = 2 # avahi-common/defs.h
+ SERVER_COLLISION = 3 # avahi-common/defs.h
+ SERVER_FAILURE = 4 # avahi-common/defs.h
+
class AvahiError(Exception):
- def __init__(self, value):
+ def __init__(self, value, *args, **kwargs):
self.value = value
- def __str__(self):
- return repr(self.value)
+ return super(AvahiError, self).__init__(value, *args,
+ **kwargs)
+
class AvahiServiceError(AvahiError):
pass
+
class AvahiGroupError(AvahiError):
pass
-class AvahiService(object):
+class AvahiService:
"""An Avahi (Zeroconf) service.
+
Attributes:
interface: integer; avahi.IF_UNSPEC or an interface index.
Used to optionally bind to the specified interface.
name: string; Example: 'Mandos'
type: string; Example: '_mandos._tcp'.
- See
+ See
port: integer; what port to announce
TXT: list of strings; TXT record for the service
domain: string; Domain to publish on, default to .local if empty.
@@ -104,535 +345,2571 @@
max_renames: integer; maximum number of renames
rename_count: integer; counter so we only rename after collisions
a sensible number of times
+ group: D-Bus Entry Group
+ server: D-Bus Server
+ bus: dbus.SystemBus()
"""
- def __init__(self, interface = avahi.IF_UNSPEC, name = None,
- type = None, port = None, TXT = None, domain = "",
- host = "", max_renames = 32768):
+
+ def __init__(self,
+ interface=avahi.IF_UNSPEC,
+ name=None,
+ servicetype=None,
+ port=None,
+ TXT=None,
+ domain="",
+ host="",
+ max_renames=32768,
+ protocol=avahi.PROTO_UNSPEC,
+ bus=None):
self.interface = interface
self.name = name
- self.type = type
+ self.type = servicetype
self.port = port
- if TXT is None:
- self.TXT = []
- else:
- self.TXT = TXT
+ self.TXT = TXT if TXT is not None else []
self.domain = domain
self.host = host
self.rename_count = 0
- def rename(self):
+ self.max_renames = max_renames
+ self.protocol = protocol
+ self.group = None # our entry group
+ self.server = None
+ self.bus = bus
+ self.entry_group_state_changed_match = None
+
+ def rename(self, remove=True):
"""Derived from the Avahi example code"""
if self.rename_count >= self.max_renames:
- logger.critical(u"No suitable service name found after %i"
- u" retries, exiting.", rename_count)
+ logger.critical("No suitable Zeroconf service name found"
+ " after %i retries, exiting.",
+ self.rename_count)
raise AvahiServiceError("Too many renames")
- name = server.GetAlternativeServiceName(name)
- logger.error(u"Changing name to %r ...", name)
- syslogger.setFormatter(logging.Formatter\
- ('Mandos (%s): %%(levelname)s:'
- ' %%(message)s' % name))
- self.remove()
- self.add()
+ self.name = str(
+ self.server.GetAlternativeServiceName(self.name))
self.rename_count += 1
+ logger.info("Changing Zeroconf service name to %r ...",
+ self.name)
+ if remove:
+ self.remove()
+ try:
+ self.add()
+ except dbus.exceptions.DBusException as error:
+ if (error.get_dbus_name()
+ == "org.freedesktop.Avahi.CollisionError"):
+ logger.info("Local Zeroconf service name collision.")
+ return self.rename(remove=False)
+ else:
+ logger.critical("D-Bus Exception", exc_info=error)
+ self.cleanup()
+ os._exit(1)
+
def remove(self):
"""Derived from the Avahi example code"""
- if group is not None:
- group.Reset()
+ if self.entry_group_state_changed_match is not None:
+ self.entry_group_state_changed_match.remove()
+ self.entry_group_state_changed_match = None
+ if self.group is not None:
+ self.group.Reset()
+
def add(self):
"""Derived from the Avahi example code"""
- global group
- if group is None:
- group = dbus.Interface\
- (bus.get_object(avahi.DBUS_NAME,
- server.EntryGroupNew()),
- avahi.DBUS_INTERFACE_ENTRY_GROUP)
- group.connect_to_signal('StateChanged',
- entry_group_state_changed)
- logger.debug(u"Adding service '%s' of type '%s' ...",
- service.name, service.type)
- group.AddService(
- self.interface, # interface
- avahi.PROTO_INET6, # protocol
- dbus.UInt32(0), # flags
- self.name, self.type,
- self.domain, self.host,
- dbus.UInt16(self.port),
- avahi.string_array_to_txt_array(self.TXT))
- group.Commit()
-
-# From the Avahi example code:
-group = None # our entry group
-# End of Avahi example code
-
-
-class Client(object):
+ self.remove()
+ if self.group is None:
+ self.group = dbus.Interface(
+ self.bus.get_object(avahi.DBUS_NAME,
+ self.server.EntryGroupNew()),
+ avahi.DBUS_INTERFACE_ENTRY_GROUP)
+ self.entry_group_state_changed_match = (
+ self.group.connect_to_signal(
+ 'StateChanged', self.entry_group_state_changed))
+ logger.debug("Adding Zeroconf service '%s' of type '%s' ...",
+ self.name, self.type)
+ self.group.AddService(
+ self.interface,
+ self.protocol,
+ dbus.UInt32(0), # flags
+ self.name, self.type,
+ self.domain, self.host,
+ dbus.UInt16(self.port),
+ avahi.string_array_to_txt_array(self.TXT))
+ self.group.Commit()
+
+ def entry_group_state_changed(self, state, error):
+ """Derived from the Avahi example code"""
+ logger.debug("Avahi entry group state change: %i", state)
+
+ if state == avahi.ENTRY_GROUP_ESTABLISHED:
+ logger.debug("Zeroconf service established.")
+ elif state == avahi.ENTRY_GROUP_COLLISION:
+ logger.info("Zeroconf service name collision.")
+ self.rename()
+ elif state == avahi.ENTRY_GROUP_FAILURE:
+ logger.critical("Avahi: Error in group state changed %s",
+ str(error))
+ raise AvahiGroupError("State changed: {!s}".format(error))
+
+ def cleanup(self):
+ """Derived from the Avahi example code"""
+ if self.group is not None:
+ try:
+ self.group.Free()
+ except (dbus.exceptions.UnknownMethodException,
+ dbus.exceptions.DBusException):
+ pass
+ self.group = None
+ self.remove()
+
+ def server_state_changed(self, state, error=None):
+ """Derived from the Avahi example code"""
+ logger.debug("Avahi server state change: %i", state)
+ bad_states = {
+ avahi.SERVER_INVALID: "Zeroconf server invalid",
+ avahi.SERVER_REGISTERING: None,
+ avahi.SERVER_COLLISION: "Zeroconf server name collision",
+ avahi.SERVER_FAILURE: "Zeroconf server failure",
+ }
+ if state in bad_states:
+ if bad_states[state] is not None:
+ if error is None:
+ logger.error(bad_states[state])
+ else:
+ logger.error(bad_states[state] + ": %r", error)
+ self.cleanup()
+ elif state == avahi.SERVER_RUNNING:
+ try:
+ self.add()
+ except dbus.exceptions.DBusException as error:
+ if (error.get_dbus_name()
+ == "org.freedesktop.Avahi.CollisionError"):
+ logger.info("Local Zeroconf service name"
+ " collision.")
+ return self.rename(remove=False)
+ else:
+ logger.critical("D-Bus Exception", exc_info=error)
+ self.cleanup()
+ os._exit(1)
+ else:
+ if error is None:
+ logger.debug("Unknown state: %r", state)
+ else:
+ logger.debug("Unknown state: %r: %r", state, error)
+
+ def activate(self):
+ """Derived from the Avahi example code"""
+ if self.server is None:
+ self.server = dbus.Interface(
+ self.bus.get_object(avahi.DBUS_NAME,
+ avahi.DBUS_PATH_SERVER,
+ follow_name_owner_changes=True),
+ avahi.DBUS_INTERFACE_SERVER)
+ self.server.connect_to_signal("StateChanged",
+ self.server_state_changed)
+ self.server_state_changed(self.server.GetState())
+
+
+class AvahiServiceToSyslog(AvahiService):
+ def rename(self, *args, **kwargs):
+ """Add the new name to the syslog messages"""
+ ret = super(AvahiServiceToSyslog, self).rename(*args, **kwargs)
+ syslogger.setFormatter(logging.Formatter(
+ 'Mandos ({}) [%(process)d]: %(levelname)s: %(message)s'
+ .format(self.name)))
+ return ret
+
+
+# Pretend that we have a GnuTLS module
+class gnutls:
+ """This isn't so much a class as it is a module-like namespace."""
+
+ library = ctypes.util.find_library("gnutls")
+ if library is None:
+ library = ctypes.util.find_library("gnutls-deb0")
+ _library = ctypes.cdll.LoadLibrary(library)
+ del library
+
+ # Unless otherwise indicated, the constants and types below are
+ # all from the gnutls/gnutls.h C header file.
+
+ # Constants
+ E_SUCCESS = 0
+ E_INTERRUPTED = -52
+ E_AGAIN = -28
+ CRT_OPENPGP = 2
+ CRT_RAWPK = 3
+ CLIENT = 2
+ SHUT_RDWR = 0
+ CRD_CERTIFICATE = 1
+ E_NO_CERTIFICATE_FOUND = -49
+ X509_FMT_DER = 0
+ NO_TICKETS = 1<<10
+ ENABLE_RAWPK = 1<<18
+ CTYPE_PEERS = 3
+ KEYID_USE_SHA256 = 1 # gnutls/x509.h
+ OPENPGP_FMT_RAW = 0 # gnutls/openpgp.h
+
+ # Types
+ class session_int(ctypes.Structure):
+ _fields_ = []
+ session_t = ctypes.POINTER(session_int)
+
+ class certificate_credentials_st(ctypes.Structure):
+ _fields_ = []
+ certificate_credentials_t = ctypes.POINTER(
+ certificate_credentials_st)
+ certificate_type_t = ctypes.c_int
+
+ class datum_t(ctypes.Structure):
+ _fields_ = [('data', ctypes.POINTER(ctypes.c_ubyte)),
+ ('size', ctypes.c_uint)]
+
+ class openpgp_crt_int(ctypes.Structure):
+ _fields_ = []
+ openpgp_crt_t = ctypes.POINTER(openpgp_crt_int)
+ openpgp_crt_fmt_t = ctypes.c_int # gnutls/openpgp.h
+ log_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_char_p)
+ credentials_type_t = ctypes.c_int
+ transport_ptr_t = ctypes.c_void_p
+ close_request_t = ctypes.c_int
+
+ # Exceptions
+ class Error(Exception):
+ def __init__(self, message=None, code=None, args=()):
+ # Default usage is by a message string, but if a return
+ # code is passed, convert it to a string with
+ # gnutls.strerror()
+ self.code = code
+ if message is None and code is not None:
+ message = gnutls.strerror(code)
+ return super(gnutls.Error, self).__init__(
+ message, *args)
+
+ class CertificateSecurityError(Error):
+ pass
+
+ # Classes
+ class Credentials:
+ def __init__(self):
+ self._c_object = gnutls.certificate_credentials_t()
+ gnutls.certificate_allocate_credentials(
+ ctypes.byref(self._c_object))
+ self.type = gnutls.CRD_CERTIFICATE
+
+ def __del__(self):
+ gnutls.certificate_free_credentials(self._c_object)
+
+ class ClientSession:
+ def __init__(self, socket, credentials=None):
+ self._c_object = gnutls.session_t()
+ gnutls_flags = gnutls.CLIENT
+ if gnutls.check_version(b"3.5.6"):
+ gnutls_flags |= gnutls.NO_TICKETS
+ if gnutls.has_rawpk:
+ gnutls_flags |= gnutls.ENABLE_RAWPK
+ gnutls.init(ctypes.byref(self._c_object), gnutls_flags)
+ del gnutls_flags
+ gnutls.set_default_priority(self._c_object)
+ gnutls.transport_set_ptr(self._c_object, socket.fileno())
+ gnutls.handshake_set_private_extensions(self._c_object,
+ True)
+ self.socket = socket
+ if credentials is None:
+ credentials = gnutls.Credentials()
+ gnutls.credentials_set(self._c_object, credentials.type,
+ ctypes.cast(credentials._c_object,
+ ctypes.c_void_p))
+ self.credentials = credentials
+
+ def __del__(self):
+ gnutls.deinit(self._c_object)
+
+ def handshake(self):
+ return gnutls.handshake(self._c_object)
+
+ def send(self, data):
+ data = bytes(data)
+ data_len = len(data)
+ while data_len > 0:
+ data_len -= gnutls.record_send(self._c_object,
+ data[-data_len:],
+ data_len)
+
+ def bye(self):
+ return gnutls.bye(self._c_object, gnutls.SHUT_RDWR)
+
+ # Error handling functions
+ def _error_code(result):
+ """A function to raise exceptions on errors, suitable
+ for the 'restype' attribute on ctypes functions"""
+ if result >= 0:
+ return result
+ if result == gnutls.E_NO_CERTIFICATE_FOUND:
+ raise gnutls.CertificateSecurityError(code=result)
+ raise gnutls.Error(code=result)
+
+ def _retry_on_error(result, func, arguments):
+ """A function to retry on some errors, suitable
+ for the 'errcheck' attribute on ctypes functions"""
+ while result < 0:
+ if result not in (gnutls.E_INTERRUPTED, gnutls.E_AGAIN):
+ return _error_code(result)
+ result = func(*arguments)
+ return result
+
+ # Unless otherwise indicated, the function declarations below are
+ # all from the gnutls/gnutls.h C header file.
+
+ # Functions
+ priority_set_direct = _library.gnutls_priority_set_direct
+ priority_set_direct.argtypes = [session_t, ctypes.c_char_p,
+ ctypes.POINTER(ctypes.c_char_p)]
+ priority_set_direct.restype = _error_code
+
+ init = _library.gnutls_init
+ init.argtypes = [ctypes.POINTER(session_t), ctypes.c_int]
+ init.restype = _error_code
+
+ set_default_priority = _library.gnutls_set_default_priority
+ set_default_priority.argtypes = [session_t]
+ set_default_priority.restype = _error_code
+
+ record_send = _library.gnutls_record_send
+ record_send.argtypes = [session_t, ctypes.c_void_p,
+ ctypes.c_size_t]
+ record_send.restype = ctypes.c_ssize_t
+ record_send.errcheck = _retry_on_error
+
+ certificate_allocate_credentials = (
+ _library.gnutls_certificate_allocate_credentials)
+ certificate_allocate_credentials.argtypes = [
+ ctypes.POINTER(certificate_credentials_t)]
+ certificate_allocate_credentials.restype = _error_code
+
+ certificate_free_credentials = (
+ _library.gnutls_certificate_free_credentials)
+ certificate_free_credentials.argtypes = [
+ certificate_credentials_t]
+ certificate_free_credentials.restype = None
+
+ handshake_set_private_extensions = (
+ _library.gnutls_handshake_set_private_extensions)
+ handshake_set_private_extensions.argtypes = [session_t,
+ ctypes.c_int]
+ handshake_set_private_extensions.restype = None
+
+ credentials_set = _library.gnutls_credentials_set
+ credentials_set.argtypes = [session_t, credentials_type_t,
+ ctypes.c_void_p]
+ credentials_set.restype = _error_code
+
+ strerror = _library.gnutls_strerror
+ strerror.argtypes = [ctypes.c_int]
+ strerror.restype = ctypes.c_char_p
+
+ certificate_type_get = _library.gnutls_certificate_type_get
+ certificate_type_get.argtypes = [session_t]
+ certificate_type_get.restype = _error_code
+
+ certificate_get_peers = _library.gnutls_certificate_get_peers
+ certificate_get_peers.argtypes = [session_t,
+ ctypes.POINTER(ctypes.c_uint)]
+ certificate_get_peers.restype = ctypes.POINTER(datum_t)
+
+ global_set_log_level = _library.gnutls_global_set_log_level
+ global_set_log_level.argtypes = [ctypes.c_int]
+ global_set_log_level.restype = None
+
+ global_set_log_function = _library.gnutls_global_set_log_function
+ global_set_log_function.argtypes = [log_func]
+ global_set_log_function.restype = None
+
+ deinit = _library.gnutls_deinit
+ deinit.argtypes = [session_t]
+ deinit.restype = None
+
+ handshake = _library.gnutls_handshake
+ handshake.argtypes = [session_t]
+ handshake.restype = _error_code
+ handshake.errcheck = _retry_on_error
+
+ transport_set_ptr = _library.gnutls_transport_set_ptr
+ transport_set_ptr.argtypes = [session_t, transport_ptr_t]
+ transport_set_ptr.restype = None
+
+ bye = _library.gnutls_bye
+ bye.argtypes = [session_t, close_request_t]
+ bye.restype = _error_code
+ bye.errcheck = _retry_on_error
+
+ check_version = _library.gnutls_check_version
+ check_version.argtypes = [ctypes.c_char_p]
+ check_version.restype = ctypes.c_char_p
+
+ _need_version = b"3.3.0"
+ if check_version(_need_version) is None:
+ raise self.Error("Needs GnuTLS {} or later"
+ .format(_need_version))
+
+ _tls_rawpk_version = b"3.6.6"
+ has_rawpk = bool(check_version(_tls_rawpk_version))
+
+ if has_rawpk:
+ # Types
+ class pubkey_st(ctypes.Structure):
+ _fields = []
+ pubkey_t = ctypes.POINTER(pubkey_st)
+
+ x509_crt_fmt_t = ctypes.c_int
+
+ # All the function declarations below are from gnutls/abstract.h
+ pubkey_init = _library.gnutls_pubkey_init
+ pubkey_init.argtypes = [ctypes.POINTER(pubkey_t)]
+ pubkey_init.restype = _error_code
+
+ pubkey_import = _library.gnutls_pubkey_import
+ pubkey_import.argtypes = [pubkey_t, ctypes.POINTER(datum_t),
+ x509_crt_fmt_t]
+ pubkey_import.restype = _error_code
+
+ pubkey_get_key_id = _library.gnutls_pubkey_get_key_id
+ pubkey_get_key_id.argtypes = [pubkey_t, ctypes.c_int,
+ ctypes.POINTER(ctypes.c_ubyte),
+ ctypes.POINTER(ctypes.c_size_t)]
+ pubkey_get_key_id.restype = _error_code
+
+ pubkey_deinit = _library.gnutls_pubkey_deinit
+ pubkey_deinit.argtypes = [pubkey_t]
+ pubkey_deinit.restype = None
+ else:
+ # All the function declarations below are from gnutls/openpgp.h
+
+ openpgp_crt_init = _library.gnutls_openpgp_crt_init
+ openpgp_crt_init.argtypes = [ctypes.POINTER(openpgp_crt_t)]
+ openpgp_crt_init.restype = _error_code
+
+ openpgp_crt_import = _library.gnutls_openpgp_crt_import
+ openpgp_crt_import.argtypes = [openpgp_crt_t,
+ ctypes.POINTER(datum_t),
+ openpgp_crt_fmt_t]
+ openpgp_crt_import.restype = _error_code
+
+ openpgp_crt_verify_self = _library.gnutls_openpgp_crt_verify_self
+ openpgp_crt_verify_self.argtypes = [openpgp_crt_t, ctypes.c_uint,
+ ctypes.POINTER(ctypes.c_uint)]
+ openpgp_crt_verify_self.restype = _error_code
+
+ openpgp_crt_deinit = _library.gnutls_openpgp_crt_deinit
+ openpgp_crt_deinit.argtypes = [openpgp_crt_t]
+ openpgp_crt_deinit.restype = None
+
+ openpgp_crt_get_fingerprint = (
+ _library.gnutls_openpgp_crt_get_fingerprint)
+ openpgp_crt_get_fingerprint.argtypes = [openpgp_crt_t,
+ ctypes.c_void_p,
+ ctypes.POINTER(
+ ctypes.c_size_t)]
+ openpgp_crt_get_fingerprint.restype = _error_code
+
+ if check_version(b"3.6.4"):
+ certificate_type_get2 = _library.gnutls_certificate_type_get2
+ certificate_type_get2.argtypes = [session_t, ctypes.c_int]
+ certificate_type_get2.restype = _error_code
+
+ # Remove non-public functions
+ del _error_code, _retry_on_error
+
+
+def call_pipe(connection, # : multiprocessing.Connection
+ func, *args, **kwargs):
+ """This function is meant to be called by multiprocessing.Process
+
+ This function runs func(*args, **kwargs), and writes the resulting
+ return value on the provided multiprocessing.Connection.
+ """
+ connection.send(func(*args, **kwargs))
+ connection.close()
+
+
+class Client:
"""A representation of a client host served by this server.
+
Attributes:
- name: string; from the config file, used in log messages
- 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(); object creation, not client host
- last_checked_ok: datetime.datetime() or None if not yet checked OK
- timeout: datetime.timedelta(); How long from last_checked_ok
- until this client is invalid
- interval: datetime.timedelta(); How often to start a new checker
- stop_hook: If set, called by stop() as stop_hook(self)
- 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
- stop_initiator_tag: - '' -
- checker_callback_tag: - '' -
- checker_command: string; External command which is run to check if
- client lives. %() expansions are done at
+ 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: multiprocessing.Process(); a running checker process used
+ to see if the client lives. 'None' if no process is
+ running.
+ checker_callback_tag: a GLib 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.
- Private attibutes:
- _timeout: Real variable for 'timeout'
- _interval: Real variable for 'interval'
- _timeout_milliseconds: Used when calling gobject.timeout_add()
- _interval_milliseconds: - '' -
+ checker_initiator_tag: a GLib event source tag, or None
+ created: datetime.datetime(); (UTC) object creation
+ client_structure: Object describing what attributes a client has
+ and is used for storing the client at exit
+ current_checker_command: string; current running checker_command
+ disable_initiator_tag: a GLib event source tag, or None
+ enabled: bool()
+ fingerprint: string (40 or 32 hexadecimal digits); used to
+ uniquely identify an OpenPGP client
+ key_id: string (64 hexadecimal digits); used to uniquely identify
+ a client using raw public keys
+ 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_checker_status: integer between 0 and 255 reflecting exit
+ status of last checker. -1 reflects crashed
+ checker, -2 means no checker completed yet.
+ last_checker_signal: The signal which killed the last checker, if
+ last_checker_status is -1
+ last_enabled: datetime.datetime(); (UTC) or None
+ name: string; from the config file, used in log messages and
+ D-Bus identifiers
+ secret: bytestring; sent verbatim (over TLS) to client
+ timeout: datetime.timedelta(); How long from last_checked_ok
+ until this client is disabled
+ extended_timeout: extra long timeout when secret has been sent
+ runtime_expansions: Allowed attributes for runtime expansion.
+ expires: datetime.datetime(); time (UTC) when a client will be
+ disabled, or None
+ server_settings: The server_settings dict from main()
"""
- def _set_timeout(self, timeout):
- "Setter function for 'timeout' attribute"
- self._timeout = timeout
- self._timeout_milliseconds = ((self.timeout.days
- * 24 * 60 * 60 * 1000)
- + (self.timeout.seconds * 1000)
- + (self.timeout.microseconds
- // 1000))
- timeout = property(lambda self: self._timeout,
- _set_timeout)
- del _set_timeout
- def _set_interval(self, interval):
- "Setter function for 'interval' attribute"
- self._interval = interval
- self._interval_milliseconds = ((self.interval.days
- * 24 * 60 * 60 * 1000)
- + (self.interval.seconds
- * 1000)
- + (self.interval.microseconds
- // 1000))
- interval = property(lambda self: self._interval,
- _set_interval)
- del _set_interval
- def __init__(self, name = None, stop_hook=None, config={}):
- """Note: the 'checker' key in 'config' sets the
- 'checker_command' attribute and *not* the 'checker'
- attribute."""
+
+ runtime_expansions = ("approval_delay", "approval_duration",
+ "created", "enabled", "expires", "key_id",
+ "fingerprint", "host", "interval",
+ "last_approval_request", "last_checked_ok",
+ "last_enabled", "name", "timeout")
+ client_defaults = {
+ "timeout": "PT5M",
+ "extended_timeout": "PT15M",
+ "interval": "PT2M",
+ "checker": "fping -q -- %%(host)s",
+ "host": "",
+ "approval_delay": "PT0S",
+ "approval_duration": "PT1S",
+ "approved_by_default": "True",
+ "enabled": "True",
+ }
+
+ @staticmethod
+ def config_parser(config):
+ """Construct a new dict of client settings of this form:
+ { client_name: {setting_name: value, ...}, ...}
+ with exceptions for any special settings as defined above.
+ NOTE: Must be a pure function. Must return the same result
+ value given the same arguments.
+ """
+ settings = {}
+ for client_name in config.sections():
+ section = dict(config.items(client_name))
+ client = settings[client_name] = {}
+
+ client["host"] = section["host"]
+ # Reformat values from string types to Python types
+ client["approved_by_default"] = config.getboolean(
+ client_name, "approved_by_default")
+ client["enabled"] = config.getboolean(client_name,
+ "enabled")
+
+ # Uppercase and remove spaces from key_id and fingerprint
+ # for later comparison purposes with return value from the
+ # key_id() and fingerprint() functions
+ client["key_id"] = (section.get("key_id", "").upper()
+ .replace(" ", ""))
+ client["fingerprint"] = (section["fingerprint"].upper()
+ .replace(" ", ""))
+ if "secret" in section:
+ client["secret"] = codecs.decode(section["secret"]
+ .encode("utf-8"),
+ "base64")
+ elif "secfile" in section:
+ with open(os.path.expanduser(os.path.expandvars
+ (section["secfile"])),
+ "rb") as secfile:
+ client["secret"] = secfile.read()
+ else:
+ raise TypeError("No secret or secfile for section {}"
+ .format(section))
+ client["timeout"] = string_to_delta(section["timeout"])
+ client["extended_timeout"] = string_to_delta(
+ section["extended_timeout"])
+ client["interval"] = string_to_delta(section["interval"])
+ client["approval_delay"] = string_to_delta(
+ section["approval_delay"])
+ client["approval_duration"] = string_to_delta(
+ section["approval_duration"])
+ client["checker_command"] = section["checker"]
+ client["last_approval_request"] = None
+ client["last_checked_ok"] = None
+ client["last_checker_status"] = -2
+
+ return settings
+
+ def __init__(self, settings, name=None, server_settings=None):
self.name = name
- logger.debug(u"Creating client %r", self.name)
- # Uppercase and remove spaces from fingerprint for later
- # comparison purposes with return value from the fingerprint()
- # function
- self.fingerprint = config["fingerprint"].upper()\
- .replace(u" ", u"")
- logger.debug(u" Fingerprint: %s", self.fingerprint)
- if "secret" in config:
- self.secret = config["secret"].decode(u"base64")
- elif "secfile" in config:
- sf = open(config["secfile"])
- self.secret = sf.read()
- sf.close()
+ if server_settings is None:
+ server_settings = {}
+ self.server_settings = server_settings
+ # adding all client settings
+ for setting, value in settings.items():
+ setattr(self, setting, value)
+
+ if self.enabled:
+ if not hasattr(self, "last_enabled"):
+ self.last_enabled = datetime.datetime.utcnow()
+ if not hasattr(self, "expires"):
+ self.expires = (datetime.datetime.utcnow()
+ + self.timeout)
else:
- raise TypeError(u"No secret or secfile for client %s"
- % self.name)
- self.host = config.get("host", "")
- self.created = datetime.datetime.now()
- self.last_checked_ok = None
- self.timeout = string_to_delta(config["timeout"])
- self.interval = string_to_delta(config["interval"])
- self.stop_hook = stop_hook
+ self.last_enabled = None
+ self.expires = None
+
+ logger.debug("Creating client %r", self.name)
+ logger.debug(" Key ID: %s", self.key_id)
+ logger.debug(" Fingerprint: %s", self.fingerprint)
+ self.created = settings.get("created",
+ datetime.datetime.utcnow())
+
+ # attributes specific for this server instance
self.checker = None
self.checker_initiator_tag = None
- self.stop_initiator_tag = None
+ self.disable_initiator_tag = None
self.checker_callback_tag = None
- self.check_command = config["checker"]
- def start(self):
+ self.current_checker_command = None
+ self.approved = None
+ self.approvals_pending = 0
+ self.changedstate = multiprocessing_manager.Condition(
+ multiprocessing_manager.Lock())
+ self.client_structure = [attr
+ for attr in self.__dict__.keys()
+ if not attr.startswith("_")]
+ self.client_structure.append("client_structure")
+
+ for name, t in inspect.getmembers(
+ type(self), lambda obj: isinstance(obj, property)):
+ if not name.startswith("_"):
+ self.client_structure.append(name)
+
+ # Send notice to process children that client state has changed
+ def send_changedstate(self):
+ with self.changedstate:
+ self.changedstate.notify_all()
+
+ def enable(self):
"""Start this client's checker and timeout hooks"""
+ if getattr(self, "enabled", False):
+ # Already enabled
+ return
+ self.expires = datetime.datetime.utcnow() + self.timeout
+ self.enabled = True
+ self.last_enabled = datetime.datetime.utcnow()
+ self.init_checker()
+ self.send_changedstate()
+
+ def disable(self, quiet=True):
+ """Disable this client."""
+ if not getattr(self, "enabled", False):
+ return False
+ if not quiet:
+ logger.info("Disabling client %s", self.name)
+ if getattr(self, "disable_initiator_tag", None) is not None:
+ GLib.source_remove(self.disable_initiator_tag)
+ self.disable_initiator_tag = None
+ self.expires = None
+ if getattr(self, "checker_initiator_tag", None) is not None:
+ GLib.source_remove(self.checker_initiator_tag)
+ self.checker_initiator_tag = None
+ self.stop_checker()
+ self.enabled = False
+ if not quiet:
+ self.send_changedstate()
+ # Do not run this again if called by a GLib.timeout_add
+ return False
+
+ def __del__(self):
+ self.disable()
+
+ def init_checker(self):
# 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)
+ if self.checker_initiator_tag is not None:
+ GLib.source_remove(self.checker_initiator_tag)
+ self.checker_initiator_tag = GLib.timeout_add(
+ int(self.interval.total_seconds() * 1000),
+ self.start_checker)
+ # Schedule a disable() when 'timeout' has passed
+ if self.disable_initiator_tag is not None:
+ GLib.source_remove(self.disable_initiator_tag)
+ self.disable_initiator_tag = GLib.timeout_add(
+ int(self.timeout.total_seconds() * 1000), self.disable)
# Also start a new checker *right now*.
self.start_checker()
- # Schedule a stop() when 'timeout' has passed
- self.stop_initiator_tag = gobject.timeout_add\
- (self._timeout_milliseconds,
- self.stop)
- def stop(self):
- """Stop this client.
- The possibility that a client might be restarted is left open,
- but not currently used."""
- # If this client doesn't have a secret, it is already stopped.
- if hasattr(self, "secret") and self.secret:
- logger.info(u"Stopping client %s", self.name)
- self.secret = None
- else:
- return False
- if getattr(self, "stop_initiator_tag", False):
- gobject.source_remove(self.stop_initiator_tag)
- self.stop_initiator_tag = None
- if getattr(self, "checker_initiator_tag", False):
- gobject.source_remove(self.checker_initiator_tag)
- self.checker_initiator_tag = None
- self.stop_checker()
- if self.stop_hook:
- self.stop_hook(self)
- # Do not run this again if called by a gobject.timeout_add
- return False
- def __del__(self):
- self.stop_hook = None
- self.stop()
- def checker_callback(self, pid, condition):
+
+ def checker_callback(self, source, condition, connection,
+ command):
"""The checker has completed, so take appropriate actions."""
- now = datetime.datetime.now()
+ # Read return code from connection (see call_pipe)
+ returncode = connection.recv()
+ connection.close()
+ if self.checker is not None:
+ self.checker.join()
self.checker_callback_tag = None
self.checker = None
- if os.WIFEXITED(condition) \
- and (os.WEXITSTATUS(condition) == 0):
- logger.info(u"Checker for %(name)s succeeded",
- vars(self))
- self.last_checked_ok = now
- gobject.source_remove(self.stop_initiator_tag)
- self.stop_initiator_tag = gobject.timeout_add\
- (self._timeout_milliseconds,
- self.stop)
- elif not os.WIFEXITED(condition):
- logger.warning(u"Checker for %(name)s crashed?",
+
+ if returncode >= 0:
+ self.last_checker_status = returncode
+ self.last_checker_signal = None
+ if self.last_checker_status == 0:
+ logger.info("Checker for %(name)s succeeded",
+ vars(self))
+ self.checked_ok()
+ else:
+ logger.info("Checker for %(name)s failed", vars(self))
+ else:
+ self.last_checker_status = -1
+ self.last_checker_signal = -returncode
+ logger.warning("Checker for %(name)s crashed?",
vars(self))
- else:
- logger.info(u"Checker for %(name)s failed",
- vars(self))
+ return False
+
+ def checked_ok(self):
+ """Assert that the client has been seen, alive and well."""
+ self.last_checked_ok = datetime.datetime.utcnow()
+ self.last_checker_status = 0
+ self.last_checker_signal = None
+ self.bump_timeout()
+
+ def bump_timeout(self, timeout=None):
+ """Bump up the timeout for this client."""
+ if timeout is None:
+ timeout = self.timeout
+ if self.disable_initiator_tag is not None:
+ GLib.source_remove(self.disable_initiator_tag)
+ self.disable_initiator_tag = None
+ if getattr(self, "enabled", False):
+ self.disable_initiator_tag = GLib.timeout_add(
+ int(timeout.total_seconds() * 1000), self.disable)
+ self.expires = datetime.datetime.utcnow() + timeout
+
+ 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.
+
If a checker already exists, leave it running and do
nothing."""
# The reason for not killing a running checker is that if we
- # did that, then if a checker (for some reason) started
- # running slowly and taking more than 'interval' time, the
- # client would inevitably timeout, since no checker would get
- # a chance to run to completion. If we instead leave running
+ # did that, and if a checker (for some reason) started running
+ # slowly and taking more than 'interval' time, then the client
+ # would inevitably timeout, since no checker would get a
+ # chance to run to completion. If we instead leave running
# checkers alone, the checker would have to take more time
- # than 'timeout' for the client to be declared invalid, which
- # is as it should be.
+ # than 'timeout' for the client to be disabled, which is as it
+ # should be.
+
+ if self.checker is not None and not self.checker.is_alive():
+ logger.warning("Checker was not alive; joining")
+ self.checker.join()
+ self.checker = None
+ # Start a new checker if needed
if self.checker is None:
- try:
- # In case check_command has exactly one % operator
- command = self.check_command % self.host
- except TypeError:
- # Escape attributes for the shell
- escaped_attrs = dict((key, re.escape(str(val)))
- for key, val in
- vars(self).iteritems())
- try:
- command = self.check_command % escaped_attrs
- except TypeError, error:
- logger.error(u'Could not format string "%s":'
- u' %s', self.check_command, error)
- return True # Try again later
- try:
- logger.info(u"Starting checker %r for %s",
- command, self.name)
- self.checker = subprocess.Popen(command,
- close_fds=True,
- shell=True, cwd="/")
- self.checker_callback_tag = gobject.child_watch_add\
- (self.checker.pid,
- self.checker_callback)
- except subprocess.OSError, error:
- logger.error(u"Failed to start subprocess: %s",
- error)
- # Re-run this periodically if run by gobject.timeout_add
+ # Escape attributes for the shell
+ escaped_attrs = {
+ attr: re.escape(str(getattr(self, attr)))
+ for attr in self.runtime_expansions}
+ try:
+ command = self.checker_command % escaped_attrs
+ except TypeError as error:
+ logger.error('Could not format string "%s"',
+ self.checker_command,
+ exc_info=error)
+ return True # Try again later
+ self.current_checker_command = command
+ logger.info("Starting checker %r for %s", command,
+ self.name)
+ # We don't need to redirect stdout and stderr, since
+ # in normal mode, that is already done by daemon(),
+ # and in debug mode we don't want to. (Stdin is
+ # always replaced by /dev/null.)
+ # The exception is when not debugging but nevertheless
+ # running in the foreground; use the previously
+ # created wnull.
+ popen_args = {"close_fds": True,
+ "shell": True,
+ "cwd": "/"}
+ if (not self.server_settings["debug"]
+ and self.server_settings["foreground"]):
+ popen_args.update({"stdout": wnull,
+ "stderr": wnull})
+ pipe = multiprocessing.Pipe(duplex=False)
+ self.checker = multiprocessing.Process(
+ target=call_pipe,
+ args=(pipe[1], subprocess.call, command),
+ kwargs=popen_args)
+ self.checker.start()
+ self.checker_callback_tag = GLib.io_add_watch(
+ GLib.IOChannel.unix_new(pipe[0].fileno()),
+ GLib.PRIORITY_DEFAULT, GLib.IO_IN,
+ self.checker_callback, pipe[0], command)
+ # Re-run this periodically if run by GLib.timeout_add
return True
+
def stop_checker(self):
"""Force the checker process, if any, to stop."""
if self.checker_callback_tag:
- gobject.source_remove(self.checker_callback_tag)
+ GLib.source_remove(self.checker_callback_tag)
self.checker_callback_tag = None
if getattr(self, "checker", None) is None:
return
- logger.debug(u"Stopping checker for %(name)s", vars(self))
- try:
- os.kill(self.checker.pid, signal.SIGTERM)
- #os.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
+ logger.debug("Stopping checker for %(name)s", vars(self))
+ self.checker.terminate()
self.checker = None
- def still_valid(self):
- """Has the timeout not yet passed for this client?"""
- now = datetime.datetime.now()
- if self.last_checked_ok is None:
- return now < (self.created + self.timeout)
- else:
- return now < (self.last_checked_ok + self.timeout)
-
-
-def peer_certificate(session):
- "Return the peer's OpenPGP certificate as a bytestring"
- # If not an OpenPGP certificate...
- if gnutls.library.functions.gnutls_certificate_type_get\
- (session._c_object) \
- != gnutls.library.constants.GNUTLS_CRT_OPENPGP:
- # ...do the normal thing
- return session.peer_certificate
- list_size = ctypes.c_uint()
- cert_list = gnutls.library.functions.gnutls_certificate_get_peers\
- (session._c_object, ctypes.byref(list_size))
- if list_size.value == 0:
- return None
- cert = cert_list[0]
- return ctypes.string_at(cert.data, cert.size)
-
-
-def fingerprint(openpgp):
- "Convert an OpenPGP bytestring to a hexdigit fingerprint string"
- # New GnuTLS "datum" with the OpenPGP public key
- datum = gnutls.library.types.gnutls_datum_t\
- (ctypes.cast(ctypes.c_char_p(openpgp),
- ctypes.POINTER(ctypes.c_ubyte)),
- ctypes.c_uint(len(openpgp)))
- # New empty GnuTLS certificate
- crt = gnutls.library.types.gnutls_openpgp_crt_t()
- gnutls.library.functions.gnutls_openpgp_crt_init\
- (ctypes.byref(crt))
- # Import the OpenPGP public key into the certificate
- gnutls.library.functions.gnutls_openpgp_crt_import\
- (crt, ctypes.byref(datum),
- gnutls.library.constants.GNUTLS_OPENPGP_FMT_RAW)
- # New buffer for the fingerprint
- buffer = ctypes.create_string_buffer(20)
- buffer_length = ctypes.c_size_t()
- # Get the fingerprint from the certificate into the buffer
- gnutls.library.functions.gnutls_openpgp_crt_get_fingerprint\
- (crt, ctypes.byref(buffer), ctypes.byref(buffer_length))
- # Deinit the certificate
- gnutls.library.functions.gnutls_openpgp_crt_deinit(crt)
- # Convert the buffer to a Python bytestring
- fpr = ctypes.string_at(buffer, buffer_length.value)
- # Convert the bytestring to hexadecimal notation
- hex_fpr = u''.join(u"%02X" % ord(char) for char in fpr)
- return hex_fpr
-
-
-class tcp_handler(SocketServer.BaseRequestHandler, object):
- """A TCP request handler class.
- Instantiated by IPv6_TCPServer for each request to handle it.
+
+
+def dbus_service_property(dbus_interface,
+ signature="v",
+ access="readwrite",
+ byte_arrays=False):
+ """Decorators for marking methods of a DBusObjectWithProperties to
+ become properties on the D-Bus.
+
+ 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 != "ay":
+ raise ValueError("Byte arrays not supported for non-'ay'"
+ " signature {!r}".format(signature))
+
+ def decorator(func):
+ func._dbus_is_property = True
+ func._dbus_interface = dbus_interface
+ func._dbus_signature = signature
+ func._dbus_access = access
+ func._dbus_name = func.__name__
+ if func._dbus_name.endswith("_dbus_property"):
+ func._dbus_name = func._dbus_name[:-14]
+ func._dbus_get_args_options = {'byte_arrays': byte_arrays}
+ return func
+
+ return decorator
+
+
+def dbus_interface_annotations(dbus_interface):
+ """Decorator for marking functions returning interface annotations
+
+ Usage:
+
+ @dbus_interface_annotations("org.example.Interface")
+ def _foo(self): # Function name does not matter
+ return {"org.freedesktop.DBus.Deprecated": "true",
+ "org.freedesktop.DBus.Property.EmitsChangedSignal":
+ "false"}
+ """
+
+ def decorator(func):
+ func._dbus_is_interface = True
+ func._dbus_interface = dbus_interface
+ func._dbus_name = dbus_interface
+ return func
+
+ return decorator
+
+
+def dbus_annotations(annotations):
+ """Decorator to annotate D-Bus methods, signals or properties
+ Usage:
+
+ @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true",
+ "org.freedesktop.DBus.Property."
+ "EmitsChangedSignal": "false"})
+ @dbus_service_property("org.example.Interface", signature="b",
+ access="r")
+ def Property_dbus_property(self):
+ return dbus.Boolean(False)
+
+ See also the DBusObjectWithAnnotations class.
+ """
+
+ def decorator(func):
+ func._dbus_annotations = annotations
+ return func
+
+ return decorator
+
+
+class DBusPropertyException(dbus.exceptions.DBusException):
+ """A base class for D-Bus property-related exceptions
+ """
+ pass
+
+
+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 DBusObjectWithAnnotations(dbus.service.Object):
+ """A D-Bus object with annotations.
+
+ Classes inheriting from this can use the dbus_annotations
+ decorator to add annotations to methods or signals.
+ """
+
+ @staticmethod
+ def _is_dbus_thing(thing):
+ """Returns a function testing if an attribute is a D-Bus thing
+
+ If called like _is_dbus_thing("method") it returns a function
+ suitable for use as predicate to inspect.getmembers().
+ """
+ return lambda obj: getattr(obj, "_dbus_is_{}".format(thing),
+ False)
+
+ def _get_all_dbus_things(self, thing):
+ """Returns a generator of (name, attribute) pairs
+ """
+ return ((getattr(athing.__get__(self), "_dbus_name", name),
+ athing.__get__(self))
+ for cls in self.__class__.__mro__
+ for name, athing in
+ inspect.getmembers(cls, self._is_dbus_thing(thing)))
+
+ @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
+ out_signature="s",
+ path_keyword='object_path',
+ connection_keyword='connection')
+ def Introspect(self, object_path, connection):
+ """Overloading of standard D-Bus method.
+
+ Inserts annotation tags on methods and signals.
+ """
+ xmlstring = dbus.service.Object.Introspect(self, object_path,
+ connection)
+ try:
+ document = xml.dom.minidom.parseString(xmlstring)
+
+ for if_tag in document.getElementsByTagName("interface"):
+ # Add annotation tags
+ for typ in ("method", "signal"):
+ for tag in if_tag.getElementsByTagName(typ):
+ annots = dict()
+ for name, prop in (self.
+ _get_all_dbus_things(typ)):
+ if (name == tag.getAttribute("name")
+ and prop._dbus_interface
+ == if_tag.getAttribute("name")):
+ annots.update(getattr(
+ prop, "_dbus_annotations", {}))
+ for name, value in annots.items():
+ ann_tag = document.createElement(
+ "annotation")
+ ann_tag.setAttribute("name", name)
+ ann_tag.setAttribute("value", value)
+ tag.appendChild(ann_tag)
+ # Add interface annotation tags
+ for annotation, value in dict(
+ itertools.chain.from_iterable(
+ annotations().items()
+ for name, annotations
+ in self._get_all_dbus_things("interface")
+ if name == if_tag.getAttribute("name")
+ )).items():
+ ann_tag = document.createElement("annotation")
+ ann_tag.setAttribute("name", annotation)
+ ann_tag.setAttribute("value", value)
+ if_tag.appendChild(ann_tag)
+ # Fix argument name for the Introspect method itself
+ if (if_tag.getAttribute("name")
+ == dbus.INTROSPECTABLE_IFACE):
+ for cn in if_tag.getElementsByTagName("method"):
+ if cn.getAttribute("name") == "Introspect":
+ for arg in cn.getElementsByTagName("arg"):
+ if (arg.getAttribute("direction")
+ == "out"):
+ arg.setAttribute("name",
+ "xml_data")
+ xmlstring = document.toxml("utf-8")
+ document.unlink()
+ except (AttributeError, xml.dom.DOMException,
+ xml.parsers.expat.ExpatError) as error:
+ logger.error("Failed to override Introspection method",
+ exc_info=error)
+ return xmlstring
+
+
+class DBusObjectWithProperties(DBusObjectWithAnnotations):
+ """A D-Bus object with properties.
+
+ Classes inheriting from this can use the dbus_service_property
+ decorator to expose methods as D-Bus properties. It exposes the
+ standard Get(), Set(), and GetAll() methods on the D-Bus.
+ """
+
+ def _get_dbus_property(self, interface_name, property_name):
+ """Returns a bound method if one exists which is a D-Bus
+ property with the specified name and interface.
+ """
+ for cls in self.__class__.__mro__:
+ for name, value in inspect.getmembers(
+ cls, self._is_dbus_thing("property")):
+ if (value._dbus_name == property_name
+ and value._dbus_interface == interface_name):
+ return value.__get__(self)
+
+ # No such property
+ raise DBusPropertyNotFound("{}:{}.{}".format(
+ self.dbus_object_path, interface_name, property_name))
+
+ @classmethod
+ def _get_all_interface_names(cls):
+ """Get a sequence of all interfaces supported by an object"""
+ return (name for name in set(getattr(getattr(x, attr),
+ "_dbus_interface", None)
+ for x in (inspect.getmro(cls))
+ for attr in dir(x))
+ if name is not None)
+
+ @dbus.service.method(dbus.PROPERTIES_IFACE,
+ in_signature="ss",
+ out_signature="v")
+ def Get(self, interface_name, property_name):
+ """Standard D-Bus property Get() method, see D-Bus standard.
+ """
+ prop = self._get_dbus_property(interface_name, property_name)
+ if prop._dbus_access == "write":
+ raise DBusPropertyAccessException(property_name)
+ value = prop()
+ if not hasattr(value, "variant_level"):
+ return value
+ return type(value)(value, variant_level=value.variant_level+1)
+
+ @dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="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 == "read":
+ raise DBusPropertyAccessException(property_name)
+ if prop._dbus_get_args_options["byte_arrays"]:
+ # The byte_arrays option is not supported yet on
+ # signatures other than "ay".
+ if prop._dbus_signature != "ay":
+ raise ValueError("Byte arrays not supported for non-"
+ "'ay' signature {!r}"
+ .format(prop._dbus_signature))
+ value = dbus.ByteArray(b''.join(chr(byte)
+ for byte in value))
+ prop(value)
+
+ @dbus.service.method(dbus.PROPERTIES_IFACE,
+ in_signature="s",
+ out_signature="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".
+ """
+ properties = {}
+ for name, prop in self._get_all_dbus_things("property"):
+ if (interface_name
+ and interface_name != prop._dbus_interface):
+ # Interface non-empty but did not match
+ continue
+ # Ignore write-only properties
+ if prop._dbus_access == "write":
+ continue
+ value = prop()
+ if not hasattr(value, "variant_level"):
+ properties[name] = value
+ continue
+ properties[name] = type(value)(
+ value, variant_level=value.variant_level + 1)
+ return dbus.Dictionary(properties, signature="sv")
+
+ @dbus.service.signal(dbus.PROPERTIES_IFACE, signature="sa{sv}as")
+ def PropertiesChanged(self, interface_name, changed_properties,
+ invalidated_properties):
+ """Standard D-Bus PropertiesChanged() signal, see D-Bus
+ standard.
+ """
+ pass
+
+ @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
+ out_signature="s",
+ path_keyword='object_path',
+ connection_keyword='connection')
+ def Introspect(self, object_path, connection):
+ """Overloading of standard D-Bus method.
+
+ Inserts property tags and interface annotation tags.
+ """
+ xmlstring = DBusObjectWithAnnotations.Introspect(self,
+ object_path,
+ connection)
+ try:
+ document = xml.dom.minidom.parseString(xmlstring)
+
+ def make_tag(document, name, prop):
+ e = document.createElement("property")
+ e.setAttribute("name", name)
+ e.setAttribute("type", prop._dbus_signature)
+ e.setAttribute("access", prop._dbus_access)
+ return e
+
+ for if_tag in document.getElementsByTagName("interface"):
+ # Add property tags
+ for tag in (make_tag(document, name, prop)
+ for name, prop
+ in self._get_all_dbus_things("property")
+ if prop._dbus_interface
+ == if_tag.getAttribute("name")):
+ if_tag.appendChild(tag)
+ # Add annotation tags for properties
+ for tag in if_tag.getElementsByTagName("property"):
+ annots = dict()
+ for name, prop in self._get_all_dbus_things(
+ "property"):
+ if (name == tag.getAttribute("name")
+ and prop._dbus_interface
+ == if_tag.getAttribute("name")):
+ annots.update(getattr(
+ prop, "_dbus_annotations", {}))
+ for name, value in annots.items():
+ ann_tag = document.createElement(
+ "annotation")
+ ann_tag.setAttribute("name", name)
+ ann_tag.setAttribute("value", value)
+ tag.appendChild(ann_tag)
+ # Add the names to the return values for the
+ # "org.freedesktop.DBus.Properties" methods
+ if (if_tag.getAttribute("name")
+ == "org.freedesktop.DBus.Properties"):
+ for cn in if_tag.getElementsByTagName("method"):
+ if cn.getAttribute("name") == "Get":
+ for arg in cn.getElementsByTagName("arg"):
+ if (arg.getAttribute("direction")
+ == "out"):
+ arg.setAttribute("name", "value")
+ elif cn.getAttribute("name") == "GetAll":
+ for arg in cn.getElementsByTagName("arg"):
+ if (arg.getAttribute("direction")
+ == "out"):
+ arg.setAttribute("name", "props")
+ xmlstring = document.toxml("utf-8")
+ document.unlink()
+ except (AttributeError, xml.dom.DOMException,
+ xml.parsers.expat.ExpatError) as error:
+ logger.error("Failed to override Introspection method",
+ exc_info=error)
+ return xmlstring
+
+
+try:
+ dbus.OBJECT_MANAGER_IFACE
+except AttributeError:
+ dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
+
+
+class DBusObjectWithObjectManager(DBusObjectWithAnnotations):
+ """A D-Bus object with an ObjectManager.
+
+ Classes inheriting from this exposes the standard
+ GetManagedObjects call and the InterfacesAdded and
+ InterfacesRemoved signals on the standard
+ "org.freedesktop.DBus.ObjectManager" interface.
+
+ Note: No signals are sent automatically; they must be sent
+ manually.
+ """
+ @dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
+ out_signature="a{oa{sa{sv}}}")
+ def GetManagedObjects(self):
+ """This function must be overridden"""
+ raise NotImplementedError()
+
+ @dbus.service.signal(dbus.OBJECT_MANAGER_IFACE,
+ signature="oa{sa{sv}}")
+ def InterfacesAdded(self, object_path, interfaces_and_properties):
+ pass
+
+ @dbus.service.signal(dbus.OBJECT_MANAGER_IFACE, signature="oas")
+ def InterfacesRemoved(self, object_path, interfaces):
+ pass
+
+ @dbus.service.method(dbus.INTROSPECTABLE_IFACE,
+ out_signature="s",
+ path_keyword='object_path',
+ connection_keyword='connection')
+ def Introspect(self, object_path, connection):
+ """Overloading of standard D-Bus method.
+
+ Override return argument name of GetManagedObjects to be
+ "objpath_interfaces_and_properties"
+ """
+ xmlstring = DBusObjectWithAnnotations.Introspect(self,
+ object_path,
+ connection)
+ try:
+ document = xml.dom.minidom.parseString(xmlstring)
+
+ for if_tag in document.getElementsByTagName("interface"):
+ # Fix argument name for the GetManagedObjects method
+ if (if_tag.getAttribute("name")
+ == dbus.OBJECT_MANAGER_IFACE):
+ for cn in if_tag.getElementsByTagName("method"):
+ if (cn.getAttribute("name")
+ == "GetManagedObjects"):
+ for arg in cn.getElementsByTagName("arg"):
+ if (arg.getAttribute("direction")
+ == "out"):
+ arg.setAttribute(
+ "name",
+ "objpath_interfaces"
+ "_and_properties")
+ xmlstring = document.toxml("utf-8")
+ document.unlink()
+ except (AttributeError, xml.dom.DOMException,
+ xml.parsers.expat.ExpatError) as error:
+ logger.error("Failed to override Introspection method",
+ exc_info=error)
+ return xmlstring
+
+
+def datetime_to_dbus(dt, variant_level=0):
+ """Convert a UTC datetime.datetime() to a D-Bus type."""
+ if dt is None:
+ return dbus.String("", variant_level=variant_level)
+ return dbus.String(dt.isoformat(), variant_level=variant_level)
+
+
+def alternate_dbus_interfaces(alt_interface_names, deprecate=True):
+ """A class decorator; applied to a subclass of
+ dbus.service.Object, it will add alternate D-Bus attributes with
+ interface names according to the "alt_interface_names" mapping.
+ Usage:
+
+ @alternate_dbus_interfaces({"org.example.Interface":
+ "net.example.AlternateInterface"})
+ class SampleDBusObject(dbus.service.Object):
+ @dbus.service.method("org.example.Interface")
+ def SampleDBusMethod():
+ pass
+
+ The above "SampleDBusMethod" on "SampleDBusObject" will be
+ reachable via two interfaces: "org.example.Interface" and
+ "net.example.AlternateInterface", the latter of which will have
+ its D-Bus annotation "org.freedesktop.DBus.Deprecated" set to
+ "true", unless "deprecate" is passed with a False value.
+
+ This works for methods and signals, and also for D-Bus properties
+ (from DBusObjectWithProperties) and interfaces (from the
+ dbus_interface_annotations decorator).
+ """
+
+ def wrapper(cls):
+ for orig_interface_name, alt_interface_name in (
+ alt_interface_names.items()):
+ attr = {}
+ interface_names = set()
+ # Go though all attributes of the class
+ for attrname, attribute in inspect.getmembers(cls):
+ # Ignore non-D-Bus attributes, and D-Bus attributes
+ # with the wrong interface name
+ if (not hasattr(attribute, "_dbus_interface")
+ or not attribute._dbus_interface.startswith(
+ orig_interface_name)):
+ continue
+ # Create an alternate D-Bus interface name based on
+ # the current name
+ alt_interface = attribute._dbus_interface.replace(
+ orig_interface_name, alt_interface_name)
+ interface_names.add(alt_interface)
+ # Is this a D-Bus signal?
+ if getattr(attribute, "_dbus_is_signal", False):
+ # Extract the original non-method undecorated
+ # function by black magic
+ if sys.version_info.major == 2:
+ nonmethod_func = (dict(
+ zip(attribute.func_code.co_freevars,
+ attribute.__closure__))
+ ["func"].cell_contents)
+ else:
+ nonmethod_func = (dict(
+ zip(attribute.__code__.co_freevars,
+ attribute.__closure__))
+ ["func"].cell_contents)
+ # Create a new, but exactly alike, function
+ # object, and decorate it to be a new D-Bus signal
+ # with the alternate D-Bus interface name
+ new_function = copy_function(nonmethod_func)
+ new_function = (dbus.service.signal(
+ alt_interface,
+ attribute._dbus_signature)(new_function))
+ # Copy annotations, if any
+ try:
+ new_function._dbus_annotations = dict(
+ attribute._dbus_annotations)
+ except AttributeError:
+ pass
+
+ # Define a creator of a function to call both the
+ # original and alternate functions, so both the
+ # original and alternate signals gets sent when
+ # the function is called
+ def fixscope(func1, func2):
+ """This function is a scope container to pass
+ func1 and func2 to the "call_both" function
+ outside of its arguments"""
+
+ @functools.wraps(func2)
+ def call_both(*args, **kwargs):
+ """This function will emit two D-Bus
+ signals by calling func1 and func2"""
+ func1(*args, **kwargs)
+ func2(*args, **kwargs)
+ # Make wrapper function look like a D-Bus
+ # signal
+ for name, attr in inspect.getmembers(func2):
+ if name.startswith("_dbus_"):
+ setattr(call_both, name, attr)
+
+ return call_both
+ # Create the "call_both" function and add it to
+ # the class
+ attr[attrname] = fixscope(attribute, new_function)
+ # Is this a D-Bus method?
+ elif getattr(attribute, "_dbus_is_method", False):
+ # Create a new, but exactly alike, function
+ # object. Decorate it to be a new D-Bus method
+ # with the alternate D-Bus interface name. Add it
+ # to the class.
+ attr[attrname] = (
+ dbus.service.method(
+ alt_interface,
+ attribute._dbus_in_signature,
+ attribute._dbus_out_signature)
+ (copy_function(attribute)))
+ # Copy annotations, if any
+ try:
+ attr[attrname]._dbus_annotations = dict(
+ attribute._dbus_annotations)
+ except AttributeError:
+ pass
+ # Is this a D-Bus property?
+ elif getattr(attribute, "_dbus_is_property", False):
+ # Create a new, but exactly alike, function
+ # object, and decorate it to be a new D-Bus
+ # property with the alternate D-Bus interface
+ # name. Add it to the class.
+ attr[attrname] = (dbus_service_property(
+ alt_interface, attribute._dbus_signature,
+ attribute._dbus_access,
+ attribute._dbus_get_args_options
+ ["byte_arrays"])
+ (copy_function(attribute)))
+ # Copy annotations, if any
+ try:
+ attr[attrname]._dbus_annotations = dict(
+ attribute._dbus_annotations)
+ except AttributeError:
+ pass
+ # Is this a D-Bus interface?
+ elif getattr(attribute, "_dbus_is_interface", False):
+ # Create a new, but exactly alike, function
+ # object. Decorate it to be a new D-Bus interface
+ # with the alternate D-Bus interface name. Add it
+ # to the class.
+ attr[attrname] = (
+ dbus_interface_annotations(alt_interface)
+ (copy_function(attribute)))
+ if deprecate:
+ # Deprecate all alternate interfaces
+ iname = "_AlternateDBusNames_interface_annotation{}"
+ for interface_name in interface_names:
+
+ @dbus_interface_annotations(interface_name)
+ def func(self):
+ return {"org.freedesktop.DBus.Deprecated":
+ "true"}
+ # Find an unused name
+ for aname in (iname.format(i)
+ for i in itertools.count()):
+ if aname not in attr:
+ attr[aname] = func
+ break
+ if interface_names:
+ # Replace the class with a new subclass of it with
+ # methods, signals, etc. as created above.
+ if sys.version_info.major == 2:
+ cls = type(b"{}Alternate".format(cls.__name__),
+ (cls, ), attr)
+ else:
+ cls = type("{}Alternate".format(cls.__name__),
+ (cls, ), attr)
+ return cls
+
+ return wrapper
+
+
+@alternate_dbus_interfaces({"se.recompile.Mandos":
+ "se.bsnet.fukt.Mandos"})
+class ClientDBus(Client, DBusObjectWithProperties):
+ """A Client class using D-Bus
+
+ Attributes:
+ dbus_object_path: dbus.ObjectPath
+ bus: dbus.SystemBus()
+ """
+
+ runtime_expansions = (Client.runtime_expansions
+ + ("dbus_object_path", ))
+
+ _interface = "se.recompile.Mandos.Client"
+
+ # dbus.service.Object doesn't use super(), so we can't either.
+
+ def __init__(self, bus=None, *args, **kwargs):
+ self.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 = str(self.name).translate(
+ {ord("."): ord("_"),
+ ord("-"): ord("_")})
+ self.dbus_object_path = dbus.ObjectPath(
+ "/clients/" + client_object_name)
+ DBusObjectWithProperties.__init__(self, self.bus,
+ self.dbus_object_path)
+
+ def notifychangeproperty(transform_func, dbus_name,
+ type_func=lambda x: x,
+ variant_level=1,
+ invalidate_only=False,
+ _interface=_interface):
+ """ Modify a variable so that it's a property which announces
+ its changes to DBus.
+
+ transform_fun: Function that takes a value and a variant_level
+ and transforms it to a D-Bus type.
+ dbus_name: D-Bus name of the variable
+ type_func: Function that transform the value before sending it
+ to the D-Bus. Default: no transform
+ variant_level: D-Bus variant level. Default: 1
+ """
+ attrname = "_{}".format(dbus_name)
+
+ def setter(self, value):
+ if hasattr(self, "dbus_object_path"):
+ if (not hasattr(self, attrname) or
+ type_func(getattr(self, attrname, None))
+ != type_func(value)):
+ if invalidate_only:
+ self.PropertiesChanged(
+ _interface, dbus.Dictionary(),
+ dbus.Array((dbus_name, )))
+ else:
+ dbus_value = transform_func(
+ type_func(value),
+ variant_level=variant_level)
+ self.PropertyChanged(dbus.String(dbus_name),
+ dbus_value)
+ self.PropertiesChanged(
+ _interface,
+ dbus.Dictionary({dbus.String(dbus_name):
+ dbus_value}),
+ dbus.Array())
+ setattr(self, attrname, value)
+
+ return property(lambda self: getattr(self, attrname), setter)
+
+ expires = notifychangeproperty(datetime_to_dbus, "Expires")
+ approvals_pending = notifychangeproperty(dbus.Boolean,
+ "ApprovalPending",
+ type_func=bool)
+ enabled = notifychangeproperty(dbus.Boolean, "Enabled")
+ last_enabled = notifychangeproperty(datetime_to_dbus,
+ "LastEnabled")
+ checker = notifychangeproperty(
+ dbus.Boolean, "CheckerRunning",
+ type_func=lambda checker: checker is not None)
+ last_checked_ok = notifychangeproperty(datetime_to_dbus,
+ "LastCheckedOK")
+ last_checker_status = notifychangeproperty(dbus.Int16,
+ "LastCheckerStatus")
+ last_approval_request = notifychangeproperty(
+ datetime_to_dbus, "LastApprovalRequest")
+ approved_by_default = notifychangeproperty(dbus.Boolean,
+ "ApprovedByDefault")
+ approval_delay = notifychangeproperty(
+ dbus.UInt64, "ApprovalDelay",
+ type_func=lambda td: td.total_seconds() * 1000)
+ approval_duration = notifychangeproperty(
+ dbus.UInt64, "ApprovalDuration",
+ type_func=lambda td: td.total_seconds() * 1000)
+ host = notifychangeproperty(dbus.String, "Host")
+ timeout = notifychangeproperty(
+ dbus.UInt64, "Timeout",
+ type_func=lambda td: td.total_seconds() * 1000)
+ extended_timeout = notifychangeproperty(
+ dbus.UInt64, "ExtendedTimeout",
+ type_func=lambda td: td.total_seconds() * 1000)
+ interval = notifychangeproperty(
+ dbus.UInt64, "Interval",
+ type_func=lambda td: td.total_seconds() * 1000)
+ checker_command = notifychangeproperty(dbus.String, "Checker")
+ secret = notifychangeproperty(dbus.ByteArray, "Secret",
+ invalidate_only=True)
+
+ del notifychangeproperty
+
+ def __del__(self, *args, **kwargs):
+ try:
+ self.remove_from_connection()
+ except LookupError:
+ pass
+ if hasattr(DBusObjectWithProperties, "__del__"):
+ DBusObjectWithProperties.__del__(self, *args, **kwargs)
+ Client.__del__(self, *args, **kwargs)
+
+ def checker_callback(self, source, condition,
+ connection, command, *args, **kwargs):
+ ret = Client.checker_callback(self, source, condition,
+ connection, command, *args,
+ **kwargs)
+ exitstatus = self.last_checker_status
+ if exitstatus >= 0:
+ # Emit D-Bus signal
+ self.CheckerCompleted(dbus.Int16(exitstatus),
+ # This is specific to GNU libC
+ dbus.Int64(exitstatus << 8),
+ dbus.String(command))
+ else:
+ # Emit D-Bus signal
+ self.CheckerCompleted(dbus.Int16(-1),
+ dbus.Int64(
+ # This is specific to GNU libC
+ (exitstatus << 8)
+ | self.last_checker_signal),
+ dbus.String(command))
+ return ret
+
+ def start_checker(self, *args, **kwargs):
+ old_checker_pid = getattr(self.checker, "pid", None)
+ r = Client.start_checker(self, *args, **kwargs)
+ # Only if new checker process was started
+ if (self.checker is not None
+ and old_checker_pid != self.checker.pid):
+ # Emit D-Bus signal
+ self.CheckerStarted(self.current_checker_command)
+ return r
+
+ def _reset_approved(self):
+ self.approved = None
+ return False
+
+ def approve(self, value=True):
+ self.approved = value
+ GLib.timeout_add(int(self.approval_duration.total_seconds()
+ * 1000), self._reset_approved)
+ self.send_changedstate()
+
+ # D-Bus methods, signals & properties
+
+ # Interfaces
+
+ # Signals
+
+ # CheckerCompleted - signal
+ @dbus.service.signal(_interface, signature="nxs")
+ def CheckerCompleted(self, exitcode, waitstatus, command):
+ "D-Bus signal"
+ pass
+
+ # CheckerStarted - signal
+ @dbus.service.signal(_interface, signature="s")
+ def CheckerStarted(self, command):
+ "D-Bus signal"
+ pass
+
+ # PropertyChanged - signal
+ @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
+ @dbus.service.signal(_interface, signature="sv")
+ def PropertyChanged(self, property, value):
+ "D-Bus signal"
+ pass
+
+ # GotSecret - signal
+ @dbus.service.signal(_interface)
+ 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, signature="s")
+ def Rejected(self, reason):
+ "D-Bus signal"
+ pass
+
+ # NeedApproval - signal
+ @dbus.service.signal(_interface, signature="tb")
+ def NeedApproval(self, timeout, default):
+ "D-Bus signal"
+ return self.need_approval()
+
+ # Methods
+
+ # Approve - method
+ @dbus.service.method(_interface, in_signature="b")
+ def Approve(self, value):
+ self.approve(value)
+
+ # CheckedOK - method
+ @dbus.service.method(_interface)
+ def CheckedOK(self):
+ self.checked_ok()
+
+ # Enable - method
+ @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
+ @dbus.service.method(_interface)
+ def Enable(self):
+ "D-Bus method"
+ self.enable()
+
+ # StartChecker - method
+ @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
+ @dbus.service.method(_interface)
+ def StartChecker(self):
+ "D-Bus method"
+ self.start_checker()
+
+ # Disable - method
+ @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
+ @dbus.service.method(_interface)
+ def Disable(self):
+ "D-Bus method"
+ self.disable()
+
+ # StopChecker - method
+ @dbus_annotations({"org.freedesktop.DBus.Deprecated": "true"})
+ @dbus.service.method(_interface)
+ def StopChecker(self):
+ self.stop_checker()
+
+ # Properties
+
+ # ApprovalPending - property
+ @dbus_service_property(_interface, signature="b", access="read")
+ def ApprovalPending_dbus_property(self):
+ return dbus.Boolean(bool(self.approvals_pending))
+
+ # ApprovedByDefault - property
+ @dbus_service_property(_interface,
+ signature="b",
+ access="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)
+
+ # ApprovalDelay - property
+ @dbus_service_property(_interface,
+ signature="t",
+ access="readwrite")
+ def ApprovalDelay_dbus_property(self, value=None):
+ if value is None: # get
+ return dbus.UInt64(self.approval_delay.total_seconds()
+ * 1000)
+ self.approval_delay = datetime.timedelta(0, 0, 0, value)
+
+ # ApprovalDuration - property
+ @dbus_service_property(_interface,
+ signature="t",
+ access="readwrite")
+ def ApprovalDuration_dbus_property(self, value=None):
+ if value is None: # get
+ return dbus.UInt64(self.approval_duration.total_seconds()
+ * 1000)
+ self.approval_duration = datetime.timedelta(0, 0, 0, value)
+
+ # Name - property
+ @dbus_annotations(
+ {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
+ @dbus_service_property(_interface, signature="s", access="read")
+ def Name_dbus_property(self):
+ return dbus.String(self.name)
+
+ # KeyID - property
+ @dbus_annotations(
+ {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
+ @dbus_service_property(_interface, signature="s", access="read")
+ def KeyID_dbus_property(self):
+ return dbus.String(self.key_id)
+
+ # Fingerprint - property
+ @dbus_annotations(
+ {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
+ @dbus_service_property(_interface, signature="s", access="read")
+ def Fingerprint_dbus_property(self):
+ return dbus.String(self.fingerprint)
+
+ # Host - property
+ @dbus_service_property(_interface,
+ signature="s",
+ access="readwrite")
+ def Host_dbus_property(self, value=None):
+ if value is None: # get
+ return dbus.String(self.host)
+ self.host = str(value)
+
+ # Created - property
+ @dbus_annotations(
+ {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const"})
+ @dbus_service_property(_interface, signature="s", access="read")
+ def Created_dbus_property(self):
+ return datetime_to_dbus(self.created)
+
+ # LastEnabled - property
+ @dbus_service_property(_interface, signature="s", access="read")
+ def LastEnabled_dbus_property(self):
+ return datetime_to_dbus(self.last_enabled)
+
+ # Enabled - property
+ @dbus_service_property(_interface,
+ signature="b",
+ access="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="s",
+ access="readwrite")
+ def LastCheckedOK_dbus_property(self, value=None):
+ if value is not None:
+ self.checked_ok()
+ return
+ return datetime_to_dbus(self.last_checked_ok)
+
+ # LastCheckerStatus - property
+ @dbus_service_property(_interface, signature="n", access="read")
+ def LastCheckerStatus_dbus_property(self):
+ return dbus.Int16(self.last_checker_status)
+
+ # Expires - property
+ @dbus_service_property(_interface, signature="s", access="read")
+ def Expires_dbus_property(self):
+ return datetime_to_dbus(self.expires)
+
+ # LastApprovalRequest - property
+ @dbus_service_property(_interface, signature="s", access="read")
+ def LastApprovalRequest_dbus_property(self):
+ return datetime_to_dbus(self.last_approval_request)
+
+ # Timeout - property
+ @dbus_service_property(_interface,
+ signature="t",
+ access="readwrite")
+ def Timeout_dbus_property(self, value=None):
+ if value is None: # get
+ return dbus.UInt64(self.timeout.total_seconds() * 1000)
+ old_timeout = self.timeout
+ self.timeout = datetime.timedelta(0, 0, 0, value)
+ # Reschedule disabling
+ if self.enabled:
+ now = datetime.datetime.utcnow()
+ self.expires += self.timeout - old_timeout
+ if self.expires <= now:
+ # The timeout has passed
+ self.disable()
+ else:
+ if (getattr(self, "disable_initiator_tag", None)
+ is None):
+ return
+ GLib.source_remove(self.disable_initiator_tag)
+ self.disable_initiator_tag = GLib.timeout_add(
+ int((self.expires - now).total_seconds() * 1000),
+ self.disable)
+
+ # ExtendedTimeout - property
+ @dbus_service_property(_interface,
+ signature="t",
+ access="readwrite")
+ def ExtendedTimeout_dbus_property(self, value=None):
+ if value is None: # get
+ return dbus.UInt64(self.extended_timeout.total_seconds()
+ * 1000)
+ self.extended_timeout = datetime.timedelta(0, 0, 0, value)
+
+ # Interval - property
+ @dbus_service_property(_interface,
+ signature="t",
+ access="readwrite")
+ def Interval_dbus_property(self, value=None):
+ if value is None: # get
+ return dbus.UInt64(self.interval.total_seconds() * 1000)
+ self.interval = datetime.timedelta(0, 0, 0, value)
+ if getattr(self, "checker_initiator_tag", None) is None:
+ return
+ if self.enabled:
+ # Reschedule checker run
+ GLib.source_remove(self.checker_initiator_tag)
+ self.checker_initiator_tag = GLib.timeout_add(
+ value, self.start_checker)
+ self.start_checker() # Start one now, too
+
+ # Checker - property
+ @dbus_service_property(_interface,
+ signature="s",
+ access="readwrite")
+ def Checker_dbus_property(self, value=None):
+ if value is None: # get
+ return dbus.String(self.checker_command)
+ self.checker_command = str(value)
+
+ # CheckerRunning - property
+ @dbus_service_property(_interface,
+ signature="b",
+ access="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_annotations(
+ {"org.freedesktop.DBus.Property.EmitsChangedSignal": "const",
+ "org.freedesktop.DBus.Deprecated": "true"})
+ @dbus_service_property(_interface, signature="o", access="read")
+ def ObjectPath_dbus_property(self):
+ return self.dbus_object_path # is already a dbus.ObjectPath
+
+ # Secret = property
+ @dbus_annotations(
+ {"org.freedesktop.DBus.Property.EmitsChangedSignal":
+ "invalidates"})
+ @dbus_service_property(_interface,
+ signature="ay",
+ access="write",
+ byte_arrays=True)
+ def Secret_dbus_property(self, value):
+ self.secret = bytes(value)
+
+ del _interface
+
+
+class ProxyClient:
+ def __init__(self, child_pipe, key_id, fpr, address):
+ self._pipe = child_pipe
+ self._pipe.send(('init', key_id, fpr, address))
+ if not self._pipe.recv():
+ raise KeyError(key_id or fpr)
+
+ 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.
+
+ Instantiated once for each connection to handle it.
Note: This will run in its own forked process."""
-
+
def handle(self):
- logger.info(u"TCP connection from: %s",
- unicode(self.client_address))
- 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 = ':'.join(("NONE", "+VERS-TLS1.1", "+AES-256-CBC",
- # "+SHA1", "+COMP-NULL", "+CTYPE-OPENPGP",
- # "+DHE-DSS"))
- priority = "NORMAL" # Fallback default, since this
- # MUST be set.
- if self.server.settings["priority"]:
- priority = self.server.settings["priority"]
- gnutls.library.functions.gnutls_priority_set_direct\
- (session._c_object, priority, None);
-
- try:
- session.handshake()
- except gnutls.errors.GNUTLSError, error:
- logger.warning(u"Handshake failed: %s", error)
- # Do not run session.bye() here: the session is not
- # established. Just abandon the request.
- return
- try:
- fpr = fingerprint(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)
- client = None
- for c in self.server.clients:
- if c.fingerprint == fpr:
- client = c
- break
- if not client:
- logger.warning(u"Client not found for fingerprint: %s",
- fpr)
- 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():
- logger.warning(u"Client %(name)s is invalid",
- vars(client))
- session.bye()
- return
- 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()
-
-
-class IPv6_TCPServer(SocketServer.ForkingTCPServer, object):
- """IPv6 TCP server. Accepts 'None' as address and/or port.
+ with contextlib.closing(self.server.child_pipe) as child_pipe:
+ logger.info("TCP connection from: %s",
+ str(self.client_address))
+ logger.debug("Pipe FD: %d",
+ self.server.child_pipe.fileno())
+
+ session = gnutls.ClientSession(self.request)
+
+ # priority = ':'.join(("NONE", "+VERS-TLS1.1",
+ # "+AES-256-CBC", "+SHA1",
+ # "+COMP-NULL", "+CTYPE-OPENPGP",
+ # "+DHE-DSS"))
+ # Use a fallback default, since this MUST be set.
+ priority = self.server.gnutls_priority
+ if priority is None:
+ priority = "NORMAL"
+ gnutls.priority_set_direct(session._c_object,
+ priority.encode("utf-8"),
+ None)
+
+ # Start communication using the Mandos protocol
+ # Get protocol number
+ line = self.request.makefile().readline()
+ logger.debug("Protocol version: %r", line)
+ try:
+ if int(line.strip().split()[0]) > 1:
+ raise RuntimeError(line)
+ except (ValueError, IndexError, RuntimeError) as error:
+ logger.error("Unknown protocol version: %s", error)
+ return
+
+ # Start GnuTLS connection
+ try:
+ session.handshake()
+ except gnutls.Error as error:
+ logger.warning("Handshake failed: %s", error)
+ # Do not run session.bye() here: the session is not
+ # established. Just abandon the request.
+ return
+ logger.debug("Handshake succeeded")
+
+ approval_required = False
+ try:
+ if gnutls.has_rawpk:
+ fpr = b""
+ try:
+ key_id = self.key_id(
+ self.peer_certificate(session))
+ except (TypeError, gnutls.Error) as error:
+ logger.warning("Bad certificate: %s", error)
+ return
+ logger.debug("Key ID: %s", key_id)
+
+ else:
+ key_id = b""
+ try:
+ fpr = self.fingerprint(
+ self.peer_certificate(session))
+ except (TypeError, gnutls.Error) as error:
+ logger.warning("Bad certificate: %s", error)
+ return
+ logger.debug("Fingerprint: %s", fpr)
+
+ try:
+ client = ProxyClient(child_pipe, key_id, 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.info("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("Client %s needs approval",
+ client.name)
+ if self.server.use_dbus:
+ # Emit D-Bus signal
+ client.NeedApproval(
+ client.approval_delay.total_seconds()
+ * 1000, client.approved_by_default)
+ else:
+ logger.warning("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
+ time = datetime.datetime.now()
+ client.changedstate.acquire()
+ client.changedstate.wait(delay.total_seconds())
+ 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
+
+ try:
+ session.send(client.secret)
+ except gnutls.Error as error:
+ logger.warning("gnutls send failed",
+ exc_info=error)
+ return
+
+ logger.info("Sending secret to %s", client.name)
+ # bump the timeout using extended_timeout
+ client.bump_timeout(client.extended_timeout)
+ if self.server.use_dbus:
+ # Emit D-Bus signal
+ client.GotSecret()
+
+ finally:
+ if approval_required:
+ client.approvals_pending -= 1
+ try:
+ session.bye()
+ except gnutls.Error as error:
+ logger.warning("GnuTLS bye failed",
+ exc_info=error)
+
+ @staticmethod
+ def peer_certificate(session):
+ "Return the peer's certificate as a bytestring"
+ try:
+ cert_type = gnutls.certificate_type_get2(session._c_object,
+ gnutls.CTYPE_PEERS)
+ except AttributeError:
+ cert_type = gnutls.certificate_type_get(session._c_object)
+ if gnutls.has_rawpk:
+ valid_cert_types = frozenset((gnutls.CRT_RAWPK,))
+ else:
+ valid_cert_types = frozenset((gnutls.CRT_OPENPGP,))
+ # If not a valid certificate type...
+ if cert_type not in valid_cert_types:
+ logger.info("Cert type %r not in %r", cert_type,
+ valid_cert_types)
+ # ...return invalid data
+ return b""
+ list_size = ctypes.c_uint(1)
+ cert_list = (gnutls.certificate_get_peers
+ (session._c_object, ctypes.byref(list_size)))
+ if not bool(cert_list) and list_size.value != 0:
+ raise gnutls.Error("error getting peer certificate")
+ if list_size.value == 0:
+ return None
+ cert = cert_list[0]
+ return ctypes.string_at(cert.data, cert.size)
+
+ @staticmethod
+ def key_id(certificate):
+ "Convert a certificate bytestring to a hexdigit key ID"
+ # New GnuTLS "datum" with the public key
+ datum = gnutls.datum_t(
+ ctypes.cast(ctypes.c_char_p(certificate),
+ ctypes.POINTER(ctypes.c_ubyte)),
+ ctypes.c_uint(len(certificate)))
+ # XXX all these need to be created in the gnutls "module"
+ # New empty GnuTLS certificate
+ pubkey = gnutls.pubkey_t()
+ gnutls.pubkey_init(ctypes.byref(pubkey))
+ # Import the raw public key into the certificate
+ gnutls.pubkey_import(pubkey,
+ ctypes.byref(datum),
+ gnutls.X509_FMT_DER)
+ # New buffer for the key ID
+ buf = ctypes.create_string_buffer(32)
+ buf_len = ctypes.c_size_t(len(buf))
+ # Get the key ID from the raw public key into the buffer
+ gnutls.pubkey_get_key_id(pubkey,
+ gnutls.KEYID_USE_SHA256,
+ ctypes.cast(ctypes.byref(buf),
+ ctypes.POINTER(ctypes.c_ubyte)),
+ ctypes.byref(buf_len))
+ # Deinit the certificate
+ gnutls.pubkey_deinit(pubkey)
+
+ # Convert the buffer to a Python bytestring
+ key_id = ctypes.string_at(buf, buf_len.value)
+ # Convert the bytestring to hexadecimal notation
+ hex_key_id = binascii.hexlify(key_id).upper()
+ return hex_key_id
+
+ @staticmethod
+ def fingerprint(openpgp):
+ "Convert an OpenPGP bytestring to a hexdigit fingerprint"
+ # New GnuTLS "datum" with the OpenPGP public key
+ datum = gnutls.datum_t(
+ ctypes.cast(ctypes.c_char_p(openpgp),
+ ctypes.POINTER(ctypes.c_ubyte)),
+ ctypes.c_uint(len(openpgp)))
+ # New empty GnuTLS certificate
+ crt = gnutls.openpgp_crt_t()
+ gnutls.openpgp_crt_init(ctypes.byref(crt))
+ # Import the OpenPGP public key into the certificate
+ gnutls.openpgp_crt_import(crt, ctypes.byref(datum),
+ gnutls.OPENPGP_FMT_RAW)
+ # Verify the self signature in the key
+ crtverify = ctypes.c_uint()
+ gnutls.openpgp_crt_verify_self(crt, 0,
+ ctypes.byref(crtverify))
+ if crtverify.value != 0:
+ gnutls.openpgp_crt_deinit(crt)
+ raise gnutls.CertificateSecurityError(code
+ =crtverify.value)
+ # New buffer for the fingerprint
+ buf = ctypes.create_string_buffer(20)
+ buf_len = ctypes.c_size_t()
+ # Get the fingerprint from the certificate into the buffer
+ gnutls.openpgp_crt_get_fingerprint(crt, ctypes.byref(buf),
+ ctypes.byref(buf_len))
+ # Deinit the certificate
+ gnutls.openpgp_crt_deinit(crt)
+ # Convert the buffer to a Python bytestring
+ fpr = ctypes.string_at(buf, buf_len.value)
+ # Convert the bytestring to hexadecimal notation
+ hex_fpr = binascii.hexlify(fpr).upper()
+ return hex_fpr
+
+
+class MultiprocessingMixIn:
+ """Like socketserver.ThreadingMixIn, but with multiprocessing"""
+
+ def sub_process_main(self, request, address):
+ try:
+ self.finish_request(request, address)
+ except Exception:
+ self.handle_error(request, address)
+ self.close_request(request)
+
+ def process_request(self, request, address):
+ """Start a new process to process the request."""
+ proc = multiprocessing.Process(target=self.sub_process_main,
+ args=(request, address))
+ proc.start()
+ return proc
+
+
+class MultiprocessingMixInWithPipe(MultiprocessingMixIn):
+ """ 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
+ """
+ parent_pipe, self.child_pipe = multiprocessing.Pipe()
+
+ proc = MultiprocessingMixIn.process_request(self, request,
+ client_address)
+ self.child_pipe.close()
+ self.add_pipe(parent_pipe, proc)
+
+ def add_pipe(self, parent_pipe, proc):
+ """Dummy function; override as necessary"""
+ raise NotImplementedError()
+
+
+class IPv6_TCPServer(MultiprocessingMixInWithPipe,
+ socketserver.TCPServer):
+ """IPv6-capable TCP server. Accepts 'None' as address and/or port
+
Attributes:
- settings: Server settings
- clients: Set() of Client objects
+ enabled: Boolean; whether this server is activated yet
+ interface: None or a network interface name (string)
+ use_ipv6: Boolean; to use IPv6 or not
"""
- address_family = socket.AF_INET6
- def __init__(self, *args, **kwargs):
- if "settings" in kwargs:
- self.settings = kwargs["settings"]
- del kwargs["settings"]
- if "clients" in kwargs:
- self.clients = kwargs["clients"]
- del kwargs["clients"]
- return super(type(self), self).__init__(*args, **kwargs)
+
+ def __init__(self, server_address, RequestHandlerClass,
+ interface=None,
+ use_ipv6=True,
+ socketfd=None):
+ """If socketfd is set, use that file descriptor instead of
+ creating a new one with socket.socket().
+ """
+ self.interface = interface
+ if use_ipv6:
+ self.address_family = socket.AF_INET6
+ if socketfd is not None:
+ # Save the file descriptor
+ self.socketfd = socketfd
+ # Save the original socket.socket() function
+ self.socket_socket = socket.socket
+
+ # To implement --socket, we monkey patch socket.socket.
+ #
+ # (When socketserver.TCPServer is a new-style class, we
+ # could make self.socket into a property instead of monkey
+ # patching socket.socket.)
+ #
+ # Create a one-time-only replacement for socket.socket()
+ @functools.wraps(socket.socket)
+ def socket_wrapper(*args, **kwargs):
+ # Restore original function so subsequent calls are
+ # not affected.
+ socket.socket = self.socket_socket
+ del self.socket_socket
+ # This time only, return a new socket object from the
+ # saved file descriptor.
+ return socket.fromfd(self.socketfd, *args, **kwargs)
+ # Replace socket.socket() function with wrapper
+ socket.socket = socket_wrapper
+ # The socketserver.TCPServer.__init__ will call
+ # socket.socket(), which might be our replacement,
+ # socket_wrapper(), if socketfd was set.
+ socketserver.TCPServer.__init__(self, server_address,
+ RequestHandlerClass)
+
def server_bind(self):
"""This overrides the normal server_bind() function
to bind to an interface if one was specified, and also NOT to
bind to an address or port if they were not specified."""
- if self.settings["interface"]:
- # 25 is from /usr/include/asm-i486/socket.h
- SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
+ global SO_BINDTODEVICE
+ if self.interface is not None:
+ if SO_BINDTODEVICE is None:
+ # Fall back to a hard-coded value which seems to be
+ # common enough.
+ logger.warning("SO_BINDTODEVICE not found, trying 25")
+ SO_BINDTODEVICE = 25
try:
- self.socket.setsockopt(socket.SOL_SOCKET,
- SO_BINDTODEVICE,
- self.settings["interface"])
- except socket.error, error:
- if error[0] == errno.EPERM:
- logger.error(u"No permission to"
- u" bind to interface %s",
- self.settings["interface"])
+ self.socket.setsockopt(
+ socket.SOL_SOCKET, SO_BINDTODEVICE,
+ (self.interface + "\0").encode("utf-8"))
+ except socket.error as error:
+ if error.errno == errno.EPERM:
+ logger.error("No permission to bind to"
+ " interface %s", self.interface)
+ elif error.errno == errno.ENOPROTOOPT:
+ logger.error("SO_BINDTODEVICE not available;"
+ " cannot bind to interface %s",
+ self.interface)
+ elif error.errno == errno.ENODEV:
+ logger.error("Interface %s does not exist,"
+ " cannot bind", self.interface)
else:
- raise error
+ raise
# Only bind(2) the socket if we really need to.
if self.server_address[0] or self.server_address[1]:
+ if self.server_address[1]:
+ self.allow_reuse_address = True
if not self.server_address[0]:
- in6addr_any = "::"
- self.server_address = (in6addr_any,
+ if self.address_family == socket.AF_INET6:
+ any_address = "::" # in6addr_any
+ else:
+ any_address = "0.0.0.0" # INADDR_ANY
+ self.server_address = (any_address,
self.server_address[1])
elif not self.server_address[1]:
- self.server_address = (self.server_address[0],
- 0)
-# if self.settings["interface"]:
+ self.server_address = (self.server_address[0], 0)
+# if self.interface:
# self.server_address = (self.server_address[0],
# 0, # port
# 0, # flowinfo
# if_nametoindex
-# (self.settings
-# ["interface"]))
- return super(type(self), self).server_bind()
+# (self.interface))
+ return socketserver.TCPServer.server_bind(self)
+
+
+class MandosServer(IPv6_TCPServer):
+ """Mandos server.
+
+ Attributes:
+ clients: set of Client objects
+ gnutls_priority GnuTLS priority string
+ use_dbus: Boolean; to emit D-Bus signals or not
+
+ Assumes a GLib.MainLoop event loop.
+ """
+
+ def __init__(self, server_address, RequestHandlerClass,
+ interface=None,
+ use_ipv6=True,
+ clients=None,
+ gnutls_priority=None,
+ use_dbus=True,
+ socketfd=None):
+ self.enabled = False
+ self.clients = clients
+ if self.clients is None:
+ self.clients = {}
+ self.use_dbus = use_dbus
+ self.gnutls_priority = gnutls_priority
+ IPv6_TCPServer.__init__(self, server_address,
+ RequestHandlerClass,
+ interface=interface,
+ use_ipv6=use_ipv6,
+ socketfd=socketfd)
+
+ def server_activate(self):
+ if self.enabled:
+ return socketserver.TCPServer.server_activate(self)
+
+ def enable(self):
+ self.enabled = True
+
+ def add_pipe(self, parent_pipe, proc):
+ # Call "handle_ipc" for both data and EOF events
+ GLib.io_add_watch(
+ GLib.IOChannel.unix_new(parent_pipe.fileno()),
+ GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
+ functools.partial(self.handle_ipc,
+ parent_pipe=parent_pipe,
+ proc=proc))
+
+ def handle_ipc(self, source, condition,
+ parent_pipe=None,
+ proc=None,
+ client_object=None):
+ # error, or the other end of multiprocessing.Pipe has closed
+ if condition & (GLib.IO_ERR | GLib.IO_HUP):
+ # Wait for other process to exit
+ proc.join()
+ return False
+
+ # Read a request from the child
+ request = parent_pipe.recv()
+ command = request[0]
+
+ if command == 'init':
+ key_id = request[1].decode("ascii")
+ fpr = request[2].decode("ascii")
+ address = request[3]
+
+ for c in self.clients.values():
+ if key_id == "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855":
+ continue
+ if key_id and c.key_id == key_id:
+ client = c
+ break
+ if fpr and c.fingerprint == fpr:
+ client = c
+ break
+ else:
+ logger.info("Client not found for key ID: %s, address"
+ ": %s", key_id or fpr, address)
+ if self.use_dbus:
+ # Emit D-Bus signal
+ mandos_dbus_service.ClientNotFound(key_id or fpr,
+ address[0])
+ parent_pipe.send(False)
+ return False
+
+ GLib.io_add_watch(
+ GLib.IOChannel.unix_new(parent_pipe.fileno()),
+ GLib.PRIORITY_DEFAULT, GLib.IO_IN | GLib.IO_HUP,
+ functools.partial(self.handle_ipc,
+ parent_pipe=parent_pipe,
+ proc=proc,
+ client_object=client))
+ parent_pipe.send(True)
+ # remove the old hook in favor of the new above hook on
+ # same fileno
+ 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 isinstance(client_object.__getattribute__(attrname),
+ collections.Callable):
+ 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
+
+
+def rfc3339_duration_to_delta(duration):
+ """Parse an RFC 3339 "duration" and return a datetime.timedelta
+
+ >>> rfc3339_duration_to_delta("P7D") == datetime.timedelta(7)
+ True
+ >>> rfc3339_duration_to_delta("PT60S") == datetime.timedelta(0, 60)
+ True
+ >>> rfc3339_duration_to_delta("PT60M") == datetime.timedelta(0, 3600)
+ True
+ >>> rfc3339_duration_to_delta("PT24H") == datetime.timedelta(1)
+ True
+ >>> rfc3339_duration_to_delta("P1W") == datetime.timedelta(7)
+ True
+ >>> rfc3339_duration_to_delta("PT5M30S") == datetime.timedelta(0, 330)
+ True
+ >>> rfc3339_duration_to_delta("P1DT3M20S") == datetime.timedelta(1, 200)
+ True
+ """
+
+ # Parsing an RFC 3339 duration with regular expressions is not
+ # possible - there would have to be multiple places for the same
+ # values, like seconds. The current code, while more esoteric, is
+ # cleaner without depending on a parsing library. If Python had a
+ # built-in library for parsing we would use it, but we'd like to
+ # avoid excessive use of external libraries.
+
+ # New type for defining tokens, syntax, and semantics all-in-one
+ Token = collections.namedtuple("Token", (
+ "regexp", # To match token; if "value" is not None, must have
+ # a "group" containing digits
+ "value", # datetime.timedelta or None
+ "followers")) # Tokens valid after this token
+ # RFC 3339 "duration" tokens, syntax, and semantics; taken from
+ # the "duration" ABNF definition in RFC 3339, Appendix A.
+ token_end = Token(re.compile(r"$"), None, frozenset())
+ token_second = Token(re.compile(r"(\d+)S"),
+ datetime.timedelta(seconds=1),
+ frozenset((token_end, )))
+ token_minute = Token(re.compile(r"(\d+)M"),
+ datetime.timedelta(minutes=1),
+ frozenset((token_second, token_end)))
+ token_hour = Token(re.compile(r"(\d+)H"),
+ datetime.timedelta(hours=1),
+ frozenset((token_minute, token_end)))
+ token_time = Token(re.compile(r"T"),
+ None,
+ frozenset((token_hour, token_minute,
+ token_second)))
+ token_day = Token(re.compile(r"(\d+)D"),
+ datetime.timedelta(days=1),
+ frozenset((token_time, token_end)))
+ token_month = Token(re.compile(r"(\d+)M"),
+ datetime.timedelta(weeks=4),
+ frozenset((token_day, token_end)))
+ token_year = Token(re.compile(r"(\d+)Y"),
+ datetime.timedelta(weeks=52),
+ frozenset((token_month, token_end)))
+ token_week = Token(re.compile(r"(\d+)W"),
+ datetime.timedelta(weeks=1),
+ frozenset((token_end, )))
+ token_duration = Token(re.compile(r"P"), None,
+ frozenset((token_year, token_month,
+ token_day, token_time,
+ token_week)))
+ # Define starting values:
+ # Value so far
+ value = datetime.timedelta()
+ found_token = None
+ # Following valid tokens
+ followers = frozenset((token_duration, ))
+ # String left to parse
+ s = duration
+ # Loop until end token is found
+ while found_token is not token_end:
+ # Search for any currently valid tokens
+ for token in followers:
+ match = token.regexp.match(s)
+ if match is not None:
+ # Token found
+ if token.value is not None:
+ # Value found, parse digits
+ factor = int(match.group(1), 10)
+ # Add to value so far
+ value += factor * token.value
+ # Strip token from string
+ s = token.regexp.sub("", s, 1)
+ # Go to found token
+ found_token = token
+ # Set valid next tokens
+ followers = found_token.followers
+ break
+ else:
+ # No currently valid tokens were found
+ raise ValueError("Invalid RFC 3339 duration: {!r}"
+ .format(duration))
+ # End token found
+ return value
def string_to_delta(interval):
"""Parse a string and return a datetime.timedelta
- >>> string_to_delta('7d')
- datetime.timedelta(7)
- >>> string_to_delta('60s')
- datetime.timedelta(0, 60)
- >>> string_to_delta('60m')
- datetime.timedelta(0, 3600)
- >>> string_to_delta('24h')
- datetime.timedelta(1)
- >>> string_to_delta(u'1w')
- datetime.timedelta(7)
+ >>> string_to_delta('7d') == datetime.timedelta(7)
+ True
+ >>> string_to_delta('60s') == datetime.timedelta(0, 60)
+ True
+ >>> string_to_delta('60m') == datetime.timedelta(0, 3600)
+ True
+ >>> string_to_delta('24h') == datetime.timedelta(1)
+ True
+ >>> string_to_delta('1w') == datetime.timedelta(7)
+ True
+ >>> string_to_delta('5m 30s') == datetime.timedelta(0, 330)
+ True
"""
- try:
- suffix=unicode(interval[-1])
- value=int(interval[:-1])
- if suffix == u"d":
- delta = datetime.timedelta(value)
- elif suffix == u"s":
- delta = datetime.timedelta(0, value)
- elif suffix == u"m":
- delta = datetime.timedelta(0, 0, 0, 0, value)
- elif suffix == u"h":
- delta = datetime.timedelta(0, 0, 0, 0, 0, value)
- elif suffix == u"w":
- delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
- else:
- raise ValueError
- except (ValueError, IndexError):
- raise ValueError
- return delta
-
-
-def server_state_changed(state):
- """Derived from the Avahi example code"""
- if state == avahi.SERVER_COLLISION:
- logger.error(u"Server name collision")
- service.remove()
- elif state == avahi.SERVER_RUNNING:
- service.add()
-
-
-def entry_group_state_changed(state, error):
- """Derived from the Avahi example code"""
- logger.debug(u"state change: %i", state)
-
- if state == avahi.ENTRY_GROUP_ESTABLISHED:
- logger.debug(u"Service established.")
- elif state == avahi.ENTRY_GROUP_COLLISION:
- logger.warning(u"Service name collision.")
- service.rename()
- elif state == avahi.ENTRY_GROUP_FAILURE:
- logger.critical(u"Error in group state changed %s",
- unicode(error))
- raise AvahiGroupError("State changed: %s", str(error))
-
-def if_nametoindex(interface):
- """Call the C function if_nametoindex(), or equivalent"""
- global if_nametoindex
- try:
- if "ctypes.util" not in sys.modules:
- import ctypes.util
- if_nametoindex = ctypes.cdll.LoadLibrary\
- (ctypes.util.find_library("c")).if_nametoindex
- except (OSError, AttributeError):
- if "struct" not in sys.modules:
- import struct
- if "fcntl" not in sys.modules:
- import fcntl
- def if_nametoindex(interface):
- "Get an interface index the hard way, i.e. using fcntl()"
- SIOCGIFINDEX = 0x8933 # From /usr/include/linux/sockios.h
- s = socket.socket()
- ifreq = fcntl.ioctl(s, SIOCGIFINDEX,
- struct.pack("16s16x", interface))
- s.close()
- interface_index = struct.unpack("I", ifreq[16:20])[0]
- return interface_index
- return if_nametoindex(interface)
-
-
-def daemon(nochdir = False, noclose = False):
+
+ try:
+ return rfc3339_duration_to_delta(interval)
+ except ValueError:
+ pass
+
+ timevalue = datetime.timedelta(0)
+ for s in interval.split():
+ try:
+ suffix = s[-1]
+ value = int(s[:-1])
+ if suffix == "d":
+ delta = datetime.timedelta(value)
+ elif suffix == "s":
+ delta = datetime.timedelta(0, value)
+ elif suffix == "m":
+ delta = datetime.timedelta(0, 0, 0, 0, value)
+ elif suffix == "h":
+ delta = datetime.timedelta(0, 0, 0, 0, 0, value)
+ elif suffix == "w":
+ delta = datetime.timedelta(0, 0, 0, 0, 0, 0, value)
+ else:
+ raise ValueError("Unknown suffix {!r}".format(suffix))
+ except IndexError as e:
+ raise ValueError(*(e.args))
+ timevalue += delta
+ return timevalue
+
+
+def daemon(nochdir=False, noclose=False):
"""See daemon(3). Standard BSD Unix function.
+
This should really exist as os.daemon, but it doesn't (yet)."""
if os.fork():
sys.exit()
@@ -643,10 +2920,11 @@
sys.exit()
if not noclose:
# Close all standard open file descriptors
- null = os.open(os.path.devnull, os.O_NOCTTY | os.O_RDWR)
+ null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
if not stat.S_ISCHR(os.fstat(null).st_mode):
raise OSError(errno.ENODEV,
- "/dev/null not a character device")
+ "{} not a character device"
+ .format(os.devnull))
os.dup2(null, sys.stdin.fileno())
os.dup2(null, sys.stdout.fileno())
os.dup2(null, sys.stderr.fileno())
@@ -655,198 +2933,721 @@
def main():
- global main_loop_started
- main_loop_started = False
-
- parser = OptionParser(version = "%%prog %s" % version)
- parser.add_option("-i", "--interface", type="string",
- metavar="IF", help="Bind to interface IF")
- parser.add_option("-a", "--address", type="string",
- help="Address to listen for requests on")
- parser.add_option("-p", "--port", type="int",
- help="Port number to receive requests on")
- parser.add_option("--check", action="store_true", default=False,
- help="Run self-test")
- parser.add_option("--debug", action="store_true",
- help="Debug mode; run in foreground and log to"
- " terminal")
- parser.add_option("--priority", type="string", help="GnuTLS"
- " priority string (see GnuTLS documentation)")
- parser.add_option("--servicename", type="string", metavar="NAME",
- help="Zeroconf service name")
- parser.add_option("--configdir", type="string",
- default="/etc/mandos", metavar="DIR",
- help="Directory to search for configuration"
- " files")
- (options, args) = parser.parse_args()
-
- if options.check:
- import doctest
- doctest.testmod()
- sys.exit()
-
+
+ ##################################################################
+ # Parsing of options, both command line and config file
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("-v", "--version", action="version",
+ version="%(prog)s {}".format(version),
+ help="show version number and exit")
+ parser.add_argument("-i", "--interface", metavar="IF",
+ help="Bind to interface IF")
+ parser.add_argument("-a", "--address",
+ help="Address to listen for requests on")
+ parser.add_argument("-p", "--port", type=int,
+ help="Port number to receive requests on")
+ parser.add_argument("--check", action="store_true",
+ help="Run self-test")
+ parser.add_argument("--debug", action="store_true",
+ help="Debug mode; run in foreground and log"
+ " to terminal", default=None)
+ parser.add_argument("--debuglevel", metavar="LEVEL",
+ help="Debug level for stdout output")
+ parser.add_argument("--priority", help="GnuTLS"
+ " priority string (see GnuTLS documentation)")
+ parser.add_argument("--servicename",
+ metavar="NAME", help="Zeroconf service name")
+ parser.add_argument("--configdir",
+ default="/etc/mandos", metavar="DIR",
+ help="Directory to search for configuration"
+ " files")
+ parser.add_argument("--no-dbus", action="store_false",
+ dest="use_dbus", help="Do not provide D-Bus"
+ " system bus interface", default=None)
+ parser.add_argument("--no-ipv6", action="store_false",
+ dest="use_ipv6", help="Do not use IPv6",
+ default=None)
+ parser.add_argument("--no-restore", action="store_false",
+ dest="restore", help="Do not restore stored"
+ " state", default=None)
+ parser.add_argument("--socket", type=int,
+ help="Specify a file descriptor to a network"
+ " socket to use instead of creating one")
+ parser.add_argument("--statedir", metavar="DIR",
+ help="Directory to save/restore state in")
+ parser.add_argument("--foreground", action="store_true",
+ help="Run in foreground", default=None)
+ parser.add_argument("--no-zeroconf", action="store_false",
+ dest="zeroconf", help="Do not use Zeroconf",
+ default=None)
+
+ options = parser.parse_args()
+
# Default values for config file for server-global settings
- server_defaults = { "interface": "",
- "address": "",
- "port": "",
- "debug": "False",
- "priority":
- "SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP",
- "servicename": "Mandos",
- }
-
+ if gnutls.has_rawpk:
+ priority = ("SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA"
+ ":!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA")
+ else:
+ priority = ("SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA"
+ ":+SIGN-DSA-SHA256")
+ server_defaults = {"interface": "",
+ "address": "",
+ "port": "",
+ "debug": "False",
+ "priority": priority,
+ "servicename": "Mandos",
+ "use_dbus": "True",
+ "use_ipv6": "True",
+ "debuglevel": "",
+ "restore": "True",
+ "socket": "",
+ "statedir": "/var/lib/mandos",
+ "foreground": "False",
+ "zeroconf": "True",
+ }
+ del priority
+
# Parse config file for server-global settings
- server_config = ConfigParser.SafeConfigParser(server_defaults)
+ server_config = configparser.ConfigParser(server_defaults)
del server_defaults
server_config.read(os.path.join(options.configdir, "mandos.conf"))
- server_section = "server"
- # Convert the SafeConfigParser object to a dict
- server_settings = dict(server_config.items(server_section))
- # Use getboolean on the boolean config option
- server_settings["debug"] = server_config.getboolean\
- (server_section, "debug")
+ # Convert the ConfigParser object to a dict
+ server_settings = server_config.defaults()
+ # Use the appropriate methods on the non-string config options
+ for option in ("debug", "use_dbus", "use_ipv6", "restore",
+ "foreground", "zeroconf"):
+ server_settings[option] = server_config.getboolean("DEFAULT",
+ option)
+ if server_settings["port"]:
+ server_settings["port"] = server_config.getint("DEFAULT",
+ "port")
+ if server_settings["socket"]:
+ server_settings["socket"] = server_config.getint("DEFAULT",
+ "socket")
+ # Later, stdin will, and stdout and stderr might, be dup'ed
+ # over with an opened os.devnull. But we don't want this to
+ # happen with a supplied network socket.
+ if 0 <= server_settings["socket"] <= 2:
+ server_settings["socket"] = os.dup(server_settings
+ ["socket"])
del server_config
-
+
# Override the settings from the config file with command line
# options, if set.
for option in ("interface", "address", "port", "debug",
- "priority", "servicename", "configdir"):
+ "priority", "servicename", "configdir", "use_dbus",
+ "use_ipv6", "debuglevel", "restore", "statedir",
+ "socket", "foreground", "zeroconf"):
value = getattr(options, option)
if value is not None:
server_settings[option] = value
del options
+ # Force all strings to be unicode
+ for option in server_settings.keys():
+ if isinstance(server_settings[option], bytes):
+ server_settings[option] = (server_settings[option]
+ .decode("utf-8"))
+ # Force all boolean options to be boolean
+ for option in ("debug", "use_dbus", "use_ipv6", "restore",
+ "foreground", "zeroconf"):
+ server_settings[option] = bool(server_settings[option])
+ # Debug implies foreground
+ if server_settings["debug"]:
+ server_settings["foreground"] = True
# Now we have our good server settings in "server_settings"
-
+
+ ##################################################################
+
+ if (not server_settings["zeroconf"]
+ and not (server_settings["port"]
+ or server_settings["socket"] != "")):
+ parser.error("Needs port or socket to work without Zeroconf")
+
+ # For convenience
debug = server_settings["debug"]
-
- if not debug:
- syslogger.setLevel(logging.WARNING)
- console.setLevel(logging.WARNING)
-
+ debuglevel = server_settings["debuglevel"]
+ use_dbus = server_settings["use_dbus"]
+ use_ipv6 = server_settings["use_ipv6"]
+ stored_state_path = os.path.join(server_settings["statedir"],
+ stored_state_file)
+ foreground = server_settings["foreground"]
+ zeroconf = server_settings["zeroconf"]
+
+ if debug:
+ initlogger(debug, logging.DEBUG)
+ else:
+ if not debuglevel:
+ initlogger(debug)
+ else:
+ level = getattr(logging, debuglevel.upper())
+ initlogger(debug, level)
+
if server_settings["servicename"] != "Mandos":
- syslogger.setFormatter(logging.Formatter\
- ('Mandos (%s): %%(levelname)s:'
- ' %%(message)s'
- % server_settings["servicename"]))
-
+ syslogger.setFormatter(
+ logging.Formatter('Mandos ({}) [%(process)d]:'
+ ' %(levelname)s: %(message)s'.format(
+ server_settings["servicename"])))
+
# Parse config file with clients
- client_defaults = { "timeout": "1h",
- "interval": "5m",
- "checker": "fping -q -- %%(host)s",
- }
- client_config = ConfigParser.SafeConfigParser(client_defaults)
+ client_config = configparser.ConfigParser(Client.client_defaults)
client_config.read(os.path.join(server_settings["configdir"],
"clients.conf"))
-
- global service
- service = AvahiService(name = server_settings["servicename"],
- type = "_mandos._tcp", );
- if server_settings["interface"]:
- service.interface = if_nametoindex(server_settings["interface"])
-
+
+ global mandos_dbus_service
+ mandos_dbus_service = None
+
+ socketfd = None
+ if server_settings["socket"] != "":
+ socketfd = server_settings["socket"]
+ tcp_server = MandosServer(
+ (server_settings["address"], server_settings["port"]),
+ ClientHandler,
+ interface=(server_settings["interface"] or None),
+ use_ipv6=use_ipv6,
+ gnutls_priority=server_settings["priority"],
+ use_dbus=use_dbus,
+ socketfd=socketfd)
+ if not foreground:
+ pidfilename = "/run/mandos.pid"
+ if not os.path.isdir("/run/."):
+ pidfilename = "/var/run/mandos.pid"
+ pidfile = None
+ try:
+ pidfile = codecs.open(pidfilename, "w", encoding="utf-8")
+ except IOError as e:
+ logger.error("Could not open file %r", pidfilename,
+ exc_info=e)
+
+ for name, group in (("_mandos", "_mandos"),
+ ("mandos", "mandos"),
+ ("nobody", "nogroup")):
+ try:
+ uid = pwd.getpwnam(name).pw_uid
+ gid = pwd.getpwnam(group).pw_gid
+ break
+ except KeyError:
+ continue
+ else:
+ uid = 65534
+ gid = 65534
+ try:
+ os.setgid(gid)
+ os.setuid(uid)
+ if debug:
+ logger.debug("Did setuid/setgid to {}:{}".format(uid,
+ gid))
+ except OSError as error:
+ logger.warning("Failed to setuid/setgid to {}:{}: {}"
+ .format(uid, gid, os.strerror(error.errno)))
+ if error.errno != errno.EPERM:
+ raise
+
+ if debug:
+ # Enable all possible GnuTLS debugging
+
+ # "Use a log level over 10 to enable all debugging options."
+ # - GnuTLS manual
+ gnutls.global_set_log_level(11)
+
+ @gnutls.log_func
+ def debug_gnutls(level, string):
+ logger.debug("GnuTLS: %s", string[:-1])
+
+ gnutls.global_set_log_function(debug_gnutls)
+
+ # Redirect stdin so all checkers get /dev/null
+ null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
+ os.dup2(null, sys.stdin.fileno())
+ if null > 2:
+ os.close(null)
+
+ # Need to fork before connecting to D-Bus
+ if not foreground:
+ # Close all input and output, do double fork, etc.
+ daemon()
+
+ if gi.version_info < (3, 10, 2):
+ # multiprocessing will use threads, so before we use GLib we
+ # need to inform GLib that threads will be used.
+ GLib.threads_init()
+
global main_loop
- global bus
- global server
# From the Avahi example code
- DBusGMainLoop(set_as_default=True )
- main_loop = gobject.MainLoop()
+ DBusGMainLoop(set_as_default=True)
+ main_loop = GLib.MainLoop()
bus = dbus.SystemBus()
- server = dbus.Interface(
- bus.get_object( avahi.DBUS_NAME, avahi.DBUS_PATH_SERVER ),
- avahi.DBUS_INTERFACE_SERVER )
# End of Avahi example code
-
- clients = Set()
- def remove_from_clients(client):
- clients.remove(client)
- if not clients:
- logger.critical(u"No clients left, exiting")
- sys.exit()
-
- clients.update(Set(Client(name = section,
- stop_hook = remove_from_clients,
- config
- = dict(client_config.items(section)))
- for section in client_config.sections()))
- if not clients:
- logger.critical(u"No clients defined")
- sys.exit(1)
-
- if not debug:
- logger.removeHandler(console)
- daemon()
-
- pidfilename = "/var/run/mandos/mandos.pid"
- pid = os.getpid()
- try:
- pidfile = open(pidfilename, "w")
- pidfile.write(str(pid) + "\n")
- pidfile.close()
+ if use_dbus:
+ try:
+ bus_name = dbus.service.BusName("se.recompile.Mandos",
+ bus,
+ do_not_queue=True)
+ old_bus_name = dbus.service.BusName(
+ "se.bsnet.fukt.Mandos", bus,
+ do_not_queue=True)
+ except dbus.exceptions.DBusException as e:
+ logger.error("Disabling D-Bus:", exc_info=e)
+ use_dbus = False
+ server_settings["use_dbus"] = False
+ tcp_server.use_dbus = False
+ if zeroconf:
+ protocol = avahi.PROTO_INET6 if use_ipv6 else avahi.PROTO_INET
+ service = AvahiServiceToSyslog(
+ name=server_settings["servicename"],
+ servicetype="_mandos._tcp",
+ protocol=protocol,
+ bus=bus)
+ if server_settings["interface"]:
+ service.interface = if_nametoindex(
+ server_settings["interface"].encode("utf-8"))
+
+ global multiprocessing_manager
+ multiprocessing_manager = multiprocessing.Manager()
+
+ client_class = Client
+ if use_dbus:
+ client_class = functools.partial(ClientDBus, bus=bus)
+
+ client_settings = Client.config_parser(client_config)
+ old_client_settings = {}
+ clients_data = {}
+
+ # This is used to redirect stdout and stderr for checker processes
+ global wnull
+ wnull = open(os.devnull, "w") # A writable /dev/null
+ # Only used if server is running in foreground but not in debug
+ # mode
+ if debug or not foreground:
+ wnull.close()
+
+ # Get client data and settings from last running state.
+ if server_settings["restore"]:
+ try:
+ with open(stored_state_path, "rb") as stored_state:
+ if sys.version_info.major == 2:
+ clients_data, old_client_settings = pickle.load(
+ stored_state)
+ else:
+ bytes_clients_data, bytes_old_client_settings = (
+ pickle.load(stored_state, encoding="bytes"))
+ # Fix bytes to strings
+ # clients_data
+ # .keys()
+ clients_data = {(key.decode("utf-8")
+ if isinstance(key, bytes)
+ else key): value
+ for key, value in
+ bytes_clients_data.items()}
+ del bytes_clients_data
+ for key in clients_data:
+ value = {(k.decode("utf-8")
+ if isinstance(k, bytes) else k): v
+ for k, v in
+ clients_data[key].items()}
+ clients_data[key] = value
+ # .client_structure
+ value["client_structure"] = [
+ (s.decode("utf-8")
+ if isinstance(s, bytes)
+ else s) for s in
+ value["client_structure"]]
+ # .name, .host, and .checker_command
+ for k in ("name", "host", "checker_command"):
+ if isinstance(value[k], bytes):
+ value[k] = value[k].decode("utf-8")
+ if "key_id" not in value:
+ value["key_id"] = ""
+ elif "fingerprint" not in value:
+ value["fingerprint"] = ""
+ # old_client_settings
+ # .keys()
+ old_client_settings = {
+ (key.decode("utf-8")
+ if isinstance(key, bytes)
+ else key): value
+ for key, value in
+ bytes_old_client_settings.items()}
+ del bytes_old_client_settings
+ # .host and .checker_command
+ for value in old_client_settings.values():
+ for attribute in ("host", "checker_command"):
+ if isinstance(value[attribute], bytes):
+ value[attribute] = (value[attribute]
+ .decode("utf-8"))
+ os.remove(stored_state_path)
+ except IOError as e:
+ if e.errno == errno.ENOENT:
+ logger.warning("Could not load persistent state:"
+ " {}".format(os.strerror(e.errno)))
+ else:
+ logger.critical("Could not load persistent state:",
+ exc_info=e)
+ raise
+ except EOFError as e:
+ logger.warning("Could not load persistent state: "
+ "EOFError:",
+ exc_info=e)
+
+ with PGPEngine() as pgp:
+ for client_name, client in clients_data.items():
+ # Skip removed clients
+ if client_name not in client_settings:
+ continue
+
+ # Decide which value to use after restoring saved state.
+ # We have three different values: Old config file,
+ # new config file, and saved state.
+ # New config value takes precedence if it differs from old
+ # config value, otherwise use saved state.
+ for name, value in client_settings[client_name].items():
+ try:
+ # For each value in new config, check if it
+ # differs from the old config value (Except for
+ # the "secret" attribute)
+ if (name != "secret"
+ and (value !=
+ old_client_settings[client_name][name])):
+ client[name] = value
+ except KeyError:
+ pass
+
+ # Clients who has passed its expire date can still be
+ # enabled if its last checker was successful. A Client
+ # whose checker succeeded before we stored its state is
+ # assumed to have successfully run all checkers during
+ # downtime.
+ if client["enabled"]:
+ if datetime.datetime.utcnow() >= client["expires"]:
+ if not client["last_checked_ok"]:
+ logger.warning(
+ "disabling client {} - Client never "
+ "performed a successful checker".format(
+ client_name))
+ client["enabled"] = False
+ elif client["last_checker_status"] != 0:
+ logger.warning(
+ "disabling client {} - Client last"
+ " checker failed with error code"
+ " {}".format(
+ client_name,
+ client["last_checker_status"]))
+ client["enabled"] = False
+ else:
+ client["expires"] = (
+ datetime.datetime.utcnow()
+ + client["timeout"])
+ logger.debug("Last checker succeeded,"
+ " keeping {} enabled".format(
+ client_name))
+ try:
+ client["secret"] = pgp.decrypt(
+ client["encrypted_secret"],
+ client_settings[client_name]["secret"])
+ except PGPError:
+ # If decryption fails, we use secret from new settings
+ logger.debug("Failed to decrypt {} old secret".format(
+ client_name))
+ client["secret"] = (client_settings[client_name]
+ ["secret"])
+
+ # Add/remove clients based on new changes made to config
+ for client_name in (set(old_client_settings)
+ - set(client_settings)):
+ del clients_data[client_name]
+ for client_name in (set(client_settings)
+ - set(old_client_settings)):
+ clients_data[client_name] = client_settings[client_name]
+
+ # Create all client objects
+ for client_name, client in clients_data.items():
+ tcp_server.clients[client_name] = client_class(
+ name=client_name,
+ settings=client,
+ server_settings=server_settings)
+
+ if not tcp_server.clients:
+ logger.warning("No clients defined")
+
+ if not foreground:
+ if pidfile is not None:
+ pid = os.getpid()
+ try:
+ with pidfile:
+ print(pid, file=pidfile)
+ except IOError:
+ logger.error("Could not write to file %r with PID %d",
+ pidfilename, pid)
del pidfile
- except IOError, err:
- logger.error(u"Could not write %s file with PID %d",
- pidfilename, os.getpid())
-
+ del pidfilename
+
+ for termsig in (signal.SIGHUP, signal.SIGTERM):
+ GLib.unix_signal_add(GLib.PRIORITY_HIGH, termsig,
+ lambda: main_loop.quit() and False)
+
+ if use_dbus:
+
+ @alternate_dbus_interfaces(
+ {"se.recompile.Mandos": "se.bsnet.fukt.Mandos"})
+ class MandosDBusService(DBusObjectWithObjectManager):
+ """A D-Bus proxy object"""
+
+ def __init__(self):
+ dbus.service.Object.__init__(self, bus, "/")
+
+ _interface = "se.recompile.Mandos"
+
+ @dbus.service.signal(_interface, signature="o")
+ def ClientAdded(self, objpath):
+ "D-Bus signal"
+ pass
+
+ @dbus.service.signal(_interface, signature="ss")
+ def ClientNotFound(self, key_id, address):
+ "D-Bus signal"
+ pass
+
+ @dbus_annotations({"org.freedesktop.DBus.Deprecated":
+ "true"})
+ @dbus.service.signal(_interface, signature="os")
+ def ClientRemoved(self, objpath, name):
+ "D-Bus signal"
+ pass
+
+ @dbus_annotations({"org.freedesktop.DBus.Deprecated":
+ "true"})
+ @dbus.service.method(_interface, out_signature="ao")
+ def GetAllClients(self):
+ "D-Bus method"
+ return dbus.Array(c.dbus_object_path for c in
+ tcp_server.clients.values())
+
+ @dbus_annotations({"org.freedesktop.DBus.Deprecated":
+ "true"})
+ @dbus.service.method(_interface,
+ out_signature="a{oa{sv}}")
+ def GetAllClientsWithProperties(self):
+ "D-Bus method"
+ return dbus.Dictionary(
+ {c.dbus_object_path: c.GetAll(
+ "se.recompile.Mandos.Client")
+ for c in tcp_server.clients.values()},
+ signature="oa{sv}")
+
+ @dbus.service.method(_interface, in_signature="o")
+ def RemoveClient(self, object_path):
+ "D-Bus method"
+ for c in tcp_server.clients.values():
+ if c.dbus_object_path == object_path:
+ del tcp_server.clients[c.name]
+ c.remove_from_connection()
+ # Don't signal the disabling
+ c.disable(quiet=True)
+ # Emit D-Bus signal for removal
+ self.client_removed_signal(c)
+ return
+ raise KeyError(object_path)
+
+ del _interface
+
+ @dbus.service.method(dbus.OBJECT_MANAGER_IFACE,
+ out_signature="a{oa{sa{sv}}}")
+ def GetManagedObjects(self):
+ """D-Bus method"""
+ return dbus.Dictionary(
+ {client.dbus_object_path:
+ dbus.Dictionary(
+ {interface: client.GetAll(interface)
+ for interface in
+ client._get_all_interface_names()})
+ for client in tcp_server.clients.values()})
+
+ def client_added_signal(self, client):
+ """Send the new standard signal and the old signal"""
+ if use_dbus:
+ # New standard signal
+ self.InterfacesAdded(
+ client.dbus_object_path,
+ dbus.Dictionary(
+ {interface: client.GetAll(interface)
+ for interface in
+ client._get_all_interface_names()}))
+ # Old signal
+ self.ClientAdded(client.dbus_object_path)
+
+ def client_removed_signal(self, client):
+ """Send the new standard signal and the old signal"""
+ if use_dbus:
+ # New standard signal
+ self.InterfacesRemoved(
+ client.dbus_object_path,
+ client._get_all_interface_names())
+ # Old signal
+ self.ClientRemoved(client.dbus_object_path,
+ client.name)
+
+ mandos_dbus_service = MandosDBusService()
+
+ # Save modules to variables to exempt the modules from being
+ # unloaded before the function registered with atexit() is run.
+ mp = multiprocessing
+ wn = wnull
+
def cleanup():
"Cleanup function; run on exit"
- global group
- # From the Avahi example code
- if not group is None:
- group.Free()
- group = None
- # End of Avahi example code
-
- while clients:
- client = clients.pop()
- client.stop_hook = None
- client.stop()
-
+ if zeroconf:
+ service.cleanup()
+
+ mp.active_children()
+ wn.close()
+ if not (tcp_server.clients or client_settings):
+ return
+
+ # Store client before exiting. Secrets are encrypted with key
+ # based on what config file has. If config file is
+ # removed/edited, old secret will thus be unrecovable.
+ clients = {}
+ with PGPEngine() as pgp:
+ for client in tcp_server.clients.values():
+ key = client_settings[client.name]["secret"]
+ client.encrypted_secret = pgp.encrypt(client.secret,
+ key)
+ client_dict = {}
+
+ # A list of attributes that can not be pickled
+ # + secret.
+ exclude = {"bus", "changedstate", "secret",
+ "checker", "server_settings"}
+ for name, typ in inspect.getmembers(dbus.service
+ .Object):
+ exclude.add(name)
+
+ client_dict["encrypted_secret"] = (client
+ .encrypted_secret)
+ for attr in client.client_structure:
+ if attr not in exclude:
+ client_dict[attr] = getattr(client, attr)
+
+ clients[client.name] = client_dict
+ del client_settings[client.name]["secret"]
+
+ try:
+ with tempfile.NamedTemporaryFile(
+ mode='wb',
+ suffix=".pickle",
+ prefix='clients-',
+ dir=os.path.dirname(stored_state_path),
+ delete=False) as stored_state:
+ pickle.dump((clients, client_settings), stored_state,
+ protocol=2)
+ tempname = stored_state.name
+ os.rename(tempname, stored_state_path)
+ except (IOError, OSError) as e:
+ if not debug:
+ try:
+ os.remove(tempname)
+ except NameError:
+ pass
+ if e.errno in (errno.ENOENT, errno.EACCES, errno.EEXIST):
+ logger.warning("Could not save persistent state: {}"
+ .format(os.strerror(e.errno)))
+ else:
+ logger.warning("Could not save persistent state:",
+ exc_info=e)
+ raise
+
+ # Delete all clients, and settings from config
+ while tcp_server.clients:
+ name, client = tcp_server.clients.popitem()
+ if use_dbus:
+ client.remove_from_connection()
+ # Don't signal the disabling
+ client.disable(quiet=True)
+ # Emit D-Bus signal for removal
+ if use_dbus:
+ mandos_dbus_service.client_removed_signal(client)
+ client_settings.clear()
+
atexit.register(cleanup)
-
- if not debug:
- 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())
-
- for client in clients:
- client.start()
-
- tcp_server = IPv6_TCPServer((server_settings["address"],
- server_settings["port"]),
- tcp_handler,
- settings=server_settings,
- clients=clients)
+
+ for client in tcp_server.clients.values():
+ if use_dbus:
+ # Emit D-Bus signal for adding
+ mandos_dbus_service.client_added_signal(client)
+ # Need to initiate checking of clients
+ if client.enabled:
+ client.init_checker()
+
+ tcp_server.enable()
+ tcp_server.server_activate()
+
# Find out what port we got
- service.port = tcp_server.socket.getsockname()[1]
- logger.info(u"Now listening on address %r, port %d, flowinfo %d,"
- u" scope_id %d" % tcp_server.socket.getsockname())
-
- #service.interface = tcp_server.socket.getsockname()[3]
-
+ if zeroconf:
+ service.port = tcp_server.socket.getsockname()[1]
+ if use_ipv6:
+ logger.info("Now listening on address %r, port %d,"
+ " flowinfo %d, scope_id %d",
+ *tcp_server.socket.getsockname())
+ else: # IPv4
+ logger.info("Now listening on address %r, port %d",
+ *tcp_server.socket.getsockname())
+
+ # service.interface = tcp_server.socket.getsockname()[3]
+
try:
- # From the Avahi example code
- server.connect_to_signal("StateChanged", server_state_changed)
- try:
- server_state_changed(server.GetState())
- except dbus.exceptions.DBusException, error:
- logger.critical(u"DBusException: %s", error)
- sys.exit(1)
- # End of Avahi example code
-
- gobject.io_add_watch(tcp_server.fileno(), gobject.IO_IN,
- lambda *args, **kwargs:
- tcp_server.handle_request\
- (*args[2:], **kwargs) or True)
-
- logger.debug(u"Starting main loop")
- main_loop_started = True
+ if zeroconf:
+ # From the Avahi example code
+ try:
+ service.activate()
+ except dbus.exceptions.DBusException as error:
+ logger.critical("D-Bus Exception", exc_info=error)
+ cleanup()
+ sys.exit(1)
+ # End of Avahi example code
+
+ GLib.io_add_watch(
+ GLib.IOChannel.unix_new(tcp_server.fileno()),
+ GLib.PRIORITY_DEFAULT, GLib.IO_IN,
+ lambda *args, **kwargs: (tcp_server.handle_request
+ (*args[2:], **kwargs) or True))
+
+ logger.debug("Starting main loop")
main_loop.run()
- except AvahiError, error:
- logger.critical(u"AvahiError: %s" + unicode(error))
+ except AvahiError as error:
+ logger.critical("Avahi Error", exc_info=error)
+ cleanup()
sys.exit(1)
except KeyboardInterrupt:
if debug:
- print
+ print("", file=sys.stderr)
+ logger.debug("Server received KeyboardInterrupt")
+ logger.debug("Server exiting")
+ # Must run before the D-Bus bus name gets deregistered
+ cleanup()
+
+
+def should_only_run_tests():
+ parser = argparse.ArgumentParser(add_help=False)
+ parser.add_argument("--check", action='store_true')
+ args, unknown_args = parser.parse_known_args()
+ run_tests = args.check
+ if run_tests:
+ # Remove --check argument from sys.argv
+ sys.argv[1:] = unknown_args
+ return run_tests
+
+# Add all tests from doctest strings
+def load_tests(loader, tests, none):
+ import doctest
+ tests.addTests(doctest.DocTestSuite())
+ return tests
if __name__ == '__main__':
- main()
+ try:
+ if should_only_run_tests():
+ # Call using ./mandos --check [--verbose]
+ unittest.main()
+ else:
+ main()
+ finally:
+ logging.shutdown()
=== modified file 'mandos-clients.conf.xml'
--- mandos-clients.conf.xml 2008-08-09 01:39:09 +0000
+++ mandos-clients.conf.xml 2019-02-10 04:20:26 +0000
@@ -1,64 +1,55 @@
-
-
+
/etc/mandos/clients.conf">
+
+
+%common;
]>
-
+
- &CONFNAME;
-
- &CONFNAME;
- &VERSION;
+ Mandos Manual
+
+ Mandos
+ &version;
+ &TIMESTAMP;
Björn
Påhlsson
- belorn@fukt.bsnet.se
+ belorn@recompile.se
Teddy
Hogeborn
- teddy@fukt.bsnet.se
+ teddy@recompile.se
2008
- Teddy Hogeborn & Björn Påhlsson
+ 2009
+ 2010
+ 2011
+ 2012
+ 2013
+ 2014
+ 2015
+ 2016
+ 2017
+ 2018
+ 2019
+ Teddy Hogeborn
+ Björn Påhlsson
-
-
- This manual page 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 manual page 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
- .
-
-
+
-
+
&CONFNAME;
5
@@ -67,156 +58,447 @@
&CONFNAME;
- Configuration file for Mandos clients
+ Configuration file for the Mandos server
-
+
-
- &CONFPATH;
-
+ &CONFPATH;
-
+
DESCRIPTION
- The file &CONFPATH; is the configuration file for mandos where
- each client that will be abel to use the service need to be
- specified. The configuration file is looked on at the startup of
- the service, so to reenable timedout clients one need to only
- restart the server. The format starts with a section under []
- which is eather [DEFAULT] or a client
- name. Values is set through the use of VAR = VALUE pair. Values
- may not be empty.
-
-
-
-
- DEFAULTS
-
- The paramters for [DEFAULT] are:
-
-
-
-
-
- timeout
-
-
- This option allows you to override the default timeout
- that clients will get. By default mandos will use 1hr.
-
-
-
-
-
- interval
-
-
- This option allows you to override the default interval
- used between checkups for disconnected clients. By default
- mandos will use 5m.
-
-
-
-
-
- checker
-
-
- This option allows you to override the default shell
- command that the server will use to check up if the client
- is still up. By default mandos will "fping -q -- %%(host)s"
-
-
-
-
-
-
-
-
- CLIENTS
-
- The paramters for clients are:
-
-
-
-
-
- fingerprint
-
-
- This option sets the openpgp fingerprint that identifies
- the public certificate that clients authenticates themself
- through gnutls. The string need to be in hex-decimal form.
-
-
-
-
-
- secret
-
-
- Base 64 encoded OpenPGP encrypted password encrypted by
- the clients openpgp certificate.
-
-
-
-
-
- secfile
-
-
- Base 64 encoded OpenPGP encrypted password encrypted by
- the clients openpgp certificate as a binary file.
-
-
-
-
-
- host
-
-
- Host name that can be used in for checking that the client is up.
-
-
-
-
-
- checker
-
-
- Shell command that the server will use to check up if a
- client is still up.
-
-
-
-
-
- timeout
-
-
- Duration that a client can be down whitout be removed from
- the client list.
-
-
-
-
-
-
-
-
- EXAMPLES
+ The file &CONFPATH; is a configuration file for mandos
+ 8, read by it at startup.
+ The file needs to list all clients that should be able to use
+ the service. The settings in this file can be overridden by
+ runtime changes to the server, which it saves across restarts.
+ (See the section called PERSISTENT STATE
in
+ mandos8.) However, any changes to this file (including adding and removing
+ clients) will, at startup, override changes done during runtime.
+
+
+ The format starts with a [section
+ header] which is either
+ [DEFAULT] or [client
+ name]. The client
+ name can be anything, and is not tied to a host
+ name. Following the section header is any number of
+ option=value
entries,
+ with continuations in the style of RFC 822. option: value
is also accepted. Note that
+ leading whitespace is removed from values. Values can contain
+ format strings which refer to other values in the same section,
+ or values in the DEFAULT
section (see ). Lines beginning with #
+ or ;
are ignored and may be used to provide
+ comments.
+
+
+
+
+ OPTIONS
+
+ Note: all option values are subject to
+ start time expansion, see .
+
+
+ Unknown options are ignored. The used options are as follows:
+
+
+
+
+
+
+
+
+ This option is optional.
+
+
+ How long to wait for external approval before resorting to
+ use the value. The
+ default is PT0S
, 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
.
+
+
+
+
+
+
+
+
+ This option is optional.
+
+
+ This option overrides the default shell command that the
+ server will use to check if the client is still up. Any
+ output of the command will be ignored, only the exit code
+ is checked: If the exit code of the command is zero, the
+ client is considered up. The command will be run using
+ /bin/sh
+
, so
+ PATH will be searched. The default
+ value for the checker command is fping %%(host)s
. Note that
+ mandos-keygen, when generating output
+ to be inserted into this file, normally looks for an SSH
+ server on the Mandos client, and, if it finds one, outputs
+ a option to check for the
+ client’s SSH key fingerprint – this is more secure against
+ spoofing.
+
+
+ In addition to normal start time expansion, this option
+ will also be subject to runtime expansion; see .
+
+
+
+
+
+
+
+
+ This option is optional.
+
+
+ Extended timeout is an added timeout that is given once
+ after a password has been sent successfully to a client.
+ The timeout is by default longer than the normal timeout,
+ and is used for handling the extra long downtime while a
+ machine is booting up. Time to take into consideration
+ when changing this value is file system checks and quota
+ checks. The default value is 15 minutes.
+
+
+ The format of TIME is the same
+ as for timeout below.
+
+
+
+
+
+
+
+
+ This option is required.
+
+
+ This option sets the OpenPGP fingerprint that identifies
+ the public key that clients authenticate themselves with
+ through TLS. The string needs to be in hexadecimal form,
+ but spaces or upper/lower case are not significant.
+
+
+
+
+
+
+
+
+ This option is optional.
+
+
+ This option sets the certificate key ID that identifies
+ the public key that clients authenticate themselves with
+ through TLS. The string needs to be in hexadecimal form,
+ but spaces or upper/lower case are not significant.
+
+
+
+
+
+
+
+
+ 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 2 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.
+
+
+
+
+
+
+
+
+ If this option is not specified, the option is required
+ to be present.
+
+
+ If present, this option must be set to a string of
+ base64-encoded binary data. It will be decoded and sent
+ to the client matching the above
+ or . This should, of course,
+ be OpenPGP encrypted data, decryptable only by the client.
+ The program mandos-keygen8 can, using its
+ option, be used to generate
+ this, if desired.
+
+
+ Note: this value of this option will probably be very
+ long. A useful feature to avoid having unreadably-long
+ lines is that a line beginning with white space adds to
+ the value of the previous line, RFC 822-style.
+
+
+
+
+
+
+
+
+ This option is optional.
+
+
+ The timeout is how long the server will wait, after a
+ successful checker run, until a client is disabled and not
+ allowed to get the data this server holds. By default
+ Mandos will use 5 minutes. See also the
+ option.
+
+
+ The TIME is specified as an RFC
+ 3339 duration; for example
+ P1Y2M3DT4H5M6S
meaning
+ one year, two months, three days, four hours, five
+ minutes, and six seconds. Some values can be omitted, see
+ RFC 3339 Appendix A for details.
+
+
+
+
+
+
+
+
+ Whether this client should be enabled by default. The
+ default is true
.
+
+
+
+
+
+
+
+
+ EXPANSION
+
+ There are two forms of expansion: Start time expansion and
+ runtime expansion.
+
+
+ START TIME EXPANSION
+
+ Any string in an option value of the form
+ %(foo)s
will be replaced by the value of the option
+ foo either in the same section, or, if it
+ does not exist there, the [DEFAULT]
+ section. This is done at start time, when the configuration
+ file is read.
+
+
+ Note that this means that, in order to include an actual
+ percent character (%
) in an option value, two
+ percent characters in a row (%%
) must be
+ entered.
+
+
+
+ RUNTIME EXPANSION
+
+ This is currently only done for the checker
+ option.
+
+
+ Any string in an option value of the form
+ %%(foo)s
will be replaced by the value of the attribute
+ foo of the internal
+ Client
object in the
+ Mandos server. The currently allowed values for
+ foo are:
+ approval_delay
,
+ approval_duration
,
+ created
,
+ enabled
,
+ expires
,
+ key_id
,
+ 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
+ percent character (%
) in a
+ checker option, four
+ percent characters in a row (%%%%
) must be
+ entered. Also, a bad format here will lead to an immediate
+ but silent run-time fatal exit; debug
+ mode is needed to expose an error of this kind.
+
+
+
+
+
+
+ FILES
+
+ The file described here is &CONFPATH;
+
+
+
+
+ BUGS
+
+ The format for specifying times for timeout
+ and interval is not very good.
+
+
+ The difference between
+ %%(foo)s and
+ %(foo)s is
+ obscure.
+
+
+
+
+
+ EXAMPLE
[DEFAULT]
-timeout = 1h
-interval = 5m
+timeout = PT5M
+interval = PT2M
checker = fping -q -- %%(host)s
-[example_client]
+# Client "foo"
+[foo]
+key_id = 788cd77115cd0bb7b2d5e0ae8496f6b48149d5e712c652076b1fd2d957ef7c1f
fingerprint = 7788 2722 5BA7 DE53 9C5A 7CFA 59CF F7CD BD9A 5920
-
secret =
hQIOA6QdEjBs2L/HEAf/TCyrDe5Xnm9esa+Pb/vWF9CUqfn4srzVgSu234
REJMVv7lBSrPE2132Lmd2gqF1HeLKDJRSVxJpt6xoWOChGHg+TMyXDxK+N
@@ -233,18 +515,53 @@
5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm
4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O
QlnHIvPzEArRQLo=
- =iHhv
+host = foo.example.org
+interval = PT1M
-host = localhost
-interval = 5m
+# Client "bar"
+[bar]
+key_id = F90C7A81D72D1EA69A51031A91FF8885F36C8B46D155C8C58709A4C99AE9E361
+fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27
+secfile = /etc/mandos/bar-secret
+timeout = PT15M
+approved_by_default = False
+approval_delay = PT30S
-
+
-
- FILES
+
+ SEE ALSO
- The file described here is &CONFPATH;
+ intro
+ 8mandos,
+ mandos-keygen
+ 8,
+ mandos.conf
+ 5,
+ mandos
+ 8,
+ fping
+ 8
+
+
+
+ RFC 3339: Date and Time on the Internet:
+ Timestamps
+
+
+
+ The time intervals are in the "duration" format, as
+ specified in ABNF in Appendix A of RFC 3339.
+
+
+
+
+
+
+
+
+
=== added file 'mandos-ctl'
--- mandos-ctl 1970-01-01 00:00:00 +0000
+++ mandos-ctl 2019-11-03 19:09:41 +0000
@@ -0,0 +1,2535 @@
+#!/usr/bin/python3 -bbI
+# -*- after-save-hook: (lambda () (let ((command (if (fboundp 'file-local-name) (file-local-name (buffer-file-name)) (or (file-remote-p (buffer-file-name) 'localname) (buffer-file-name))))) (if (= (progn (if (get-buffer "*Test*") (kill-buffer "*Test*")) (process-file-shell-command (format "%s --check" (shell-quote-argument command)) nil "*Test*")) 0) (let ((w (get-buffer-window "*Test*"))) (if w (delete-window w))) (progn (with-current-buffer "*Test*" (compilation-mode)) (display-buffer "*Test*" '(display-buffer-in-side-window)))))); coding: utf-8 -*-
+#
+# Mandos Monitor - Control and monitor the Mandos server
+#
+# Copyright © 2008-2019 Teddy Hogeborn
+# Copyright © 2008-2019 Björn Påhlsson
+#
+# This file is part of Mandos.
+#
+# Mandos 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.
+#
+# Mandos 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 Mandos. If not, see .
+#
+# Contact the authors at .
+#
+
+from __future__ import (division, absolute_import, print_function,
+ unicode_literals)
+
+try:
+ from future_builtins import *
+except ImportError:
+ pass
+
+import sys
+import argparse
+import locale
+import datetime
+import re
+import os
+import collections
+import json
+import unittest
+import logging
+import io
+import tempfile
+import contextlib
+
+if sys.version_info.major == 2:
+ __metaclass__ = type
+
+try:
+ import pydbus
+ import gi
+ dbus_python = None
+except ImportError:
+ import dbus as dbus_python
+ pydbus = None
+ class gi:
+ """Dummy gi module, for the tests"""
+ class repository:
+ class GLib:
+ class Error(Exception):
+ pass
+
+# Show warnings by default
+if not sys.warnoptions:
+ import warnings
+ warnings.simplefilter("default")
+
+log = logging.getLogger(sys.argv[0])
+logging.basicConfig(level="INFO", # Show info level messages
+ format="%(message)s") # Show basic log messages
+
+logging.captureWarnings(True) # Show warnings via the logging system
+
+if sys.version_info.major == 2:
+ str = unicode
+ import StringIO
+ io.StringIO = StringIO.StringIO
+
+locale.setlocale(locale.LC_ALL, "")
+
+version = "1.8.9"
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ add_command_line_options(parser)
+
+ options = parser.parse_args()
+ check_option_syntax(parser, options)
+
+ clientnames = options.client
+
+ if options.debug:
+ log.setLevel(logging.DEBUG)
+
+ if pydbus is not None:
+ bus = pydbus_adapter.CachingBus(pydbus)
+ else:
+ bus = dbus_python_adapter.CachingBus(dbus_python)
+
+ try:
+ all_clients = bus.get_clients_and_properties()
+ except dbus.ConnectFailed as e:
+ log.critical("Could not connect to Mandos server: %s", e)
+ sys.exit(1)
+ except dbus.Error as e:
+ log.critical(
+ "Failed to access Mandos server through D-Bus:\n%s", e)
+ sys.exit(1)
+
+ # Compile dict of (clientpath: properties) to process
+ if not clientnames:
+ clients = all_clients
+ else:
+ clients = {}
+ for name in clientnames:
+ for objpath, properties in all_clients.items():
+ if properties["Name"] == name:
+ clients[objpath] = properties
+ break
+ else:
+ log.critical("Client not found on server: %r", name)
+ sys.exit(1)
+
+ commands = commands_from_options(options)
+
+ for command in commands:
+ command.run(clients, bus)
+
+
+def add_command_line_options(parser):
+ parser.add_argument("--version", action="version",
+ version="%(prog)s {}".format(version),
+ help="show version number and exit")
+ parser.add_argument("-a", "--all", action="store_true",
+ help="Select all clients")
+ parser.add_argument("-v", "--verbose", action="store_true",
+ help="Print all fields")
+ parser.add_argument("-j", "--dump-json", dest="commands",
+ action="append_const", default=[],
+ const=command.DumpJSON(),
+ help="Dump client data in JSON format")
+ enable_disable = parser.add_mutually_exclusive_group()
+ enable_disable.add_argument("-e", "--enable", dest="commands",
+ action="append_const", default=[],
+ const=command.Enable(),
+ help="Enable client")
+ enable_disable.add_argument("-d", "--disable", dest="commands",
+ action="append_const", default=[],
+ const=command.Disable(),
+ help="disable client")
+ parser.add_argument("-b", "--bump-timeout", dest="commands",
+ action="append_const", default=[],
+ const=command.BumpTimeout(),
+ help="Bump timeout for client")
+ start_stop_checker = parser.add_mutually_exclusive_group()
+ start_stop_checker.add_argument("--start-checker",
+ dest="commands",
+ action="append_const", default=[],
+ const=command.StartChecker(),
+ help="Start checker for client")
+ start_stop_checker.add_argument("--stop-checker", dest="commands",
+ action="append_const", default=[],
+ const=command.StopChecker(),
+ help="Stop checker for client")
+ parser.add_argument("-V", "--is-enabled", dest="commands",
+ action="append_const", default=[],
+ const=command.IsEnabled(),
+ help="Check if client is enabled")
+ parser.add_argument("-r", "--remove", dest="commands",
+ action="append_const", default=[],
+ const=command.Remove(),
+ help="Remove client")
+ parser.add_argument("-c", "--checker", dest="commands",
+ action="append", default=[],
+ metavar="COMMAND", type=command.SetChecker,
+ help="Set checker command for client")
+ parser.add_argument(
+ "-t", "--timeout", dest="commands", action="append",
+ default=[], metavar="TIME",
+ type=command.SetTimeout.argparse(string_to_delta),
+ help="Set timeout for client")
+ parser.add_argument(
+ "--extended-timeout", dest="commands", action="append",
+ default=[], metavar="TIME",
+ type=command.SetExtendedTimeout.argparse(string_to_delta),
+ help="Set extended timeout for client")
+ parser.add_argument(
+ "-i", "--interval", dest="commands", action="append",
+ default=[], metavar="TIME",
+ type=command.SetInterval.argparse(string_to_delta),
+ help="Set checker interval for client")
+ approve_deny_default = parser.add_mutually_exclusive_group()
+ approve_deny_default.add_argument(
+ "--approve-by-default", dest="commands",
+ action="append_const", default=[],
+ const=command.ApproveByDefault(),
+ help="Set client to be approved by default")
+ approve_deny_default.add_argument(
+ "--deny-by-default", dest="commands",
+ action="append_const", default=[],
+ const=command.DenyByDefault(),
+ help="Set client to be denied by default")
+ parser.add_argument(
+ "--approval-delay", dest="commands", action="append",
+ default=[], metavar="TIME",
+ type=command.SetApprovalDelay.argparse(string_to_delta),
+ help="Set delay before client approve/deny")
+ parser.add_argument(
+ "--approval-duration", dest="commands", action="append",
+ default=[], metavar="TIME",
+ type=command.SetApprovalDuration.argparse(string_to_delta),
+ help="Set duration of one client approval")
+ parser.add_argument("-H", "--host", dest="commands",
+ action="append", default=[], metavar="STRING",
+ type=command.SetHost,
+ help="Set host for client")
+ parser.add_argument(
+ "-s", "--secret", dest="commands", action="append",
+ default=[], metavar="FILENAME",
+ type=command.SetSecret.argparse(argparse.FileType(mode="rb")),
+ help="Set password blob (file) for client")
+ approve_deny = parser.add_mutually_exclusive_group()
+ approve_deny.add_argument(
+ "-A", "--approve", dest="commands", action="append_const",
+ default=[], const=command.Approve(),
+ help="Approve any current client request")
+ approve_deny.add_argument("-D", "--deny", dest="commands",
+ action="append_const", default=[],
+ const=command.Deny(),
+ help="Deny any current client request")
+ parser.add_argument("--debug", action="store_true",
+ help="Debug mode (show D-Bus commands)")
+ parser.add_argument("--check", action="store_true",
+ help="Run self-test")
+ parser.add_argument("client", nargs="*", help="Client name")
+
+
+def string_to_delta(interval):
+ """Parse a string and return a datetime.timedelta"""
+
+ try:
+ return rfc3339_duration_to_delta(interval)
+ except ValueError as e:
+ log.warning("%s - Parsing as pre-1.6.1 interval instead",
+ ' '.join(e.args))
+ return parse_pre_1_6_1_interval(interval)
+
+
+def rfc3339_duration_to_delta(duration):
+ """Parse an RFC 3339 "duration" and return a datetime.timedelta
+
+ >>> rfc3339_duration_to_delta("P7D") == datetime.timedelta(7)
+ True
+ >>> rfc3339_duration_to_delta("PT60S") == datetime.timedelta(0, 60)
+ True
+ >>> rfc3339_duration_to_delta("PT60M") == datetime.timedelta(hours=1)
+ True
+ >>> # 60 months
+ >>> rfc3339_duration_to_delta("P60M") == datetime.timedelta(1680)
+ True
+ >>> rfc3339_duration_to_delta("PT24H") == datetime.timedelta(1)
+ True
+ >>> rfc3339_duration_to_delta("P1W") == datetime.timedelta(7)
+ True
+ >>> rfc3339_duration_to_delta("PT5M30S") == datetime.timedelta(0, 330)
+ True
+ >>> rfc3339_duration_to_delta("P1DT3M20S") == datetime.timedelta(1, 200)
+ True
+ >>> # Can not be empty:
+ >>> rfc3339_duration_to_delta("")
+ Traceback (most recent call last):
+ ...
+ ValueError: Invalid RFC 3339 duration: ""
+ >>> # Must start with "P":
+ >>> rfc3339_duration_to_delta("1D")
+ Traceback (most recent call last):
+ ...
+ ValueError: Invalid RFC 3339 duration: "1D"
+ >>> # Must use correct order
+ >>> rfc3339_duration_to_delta("PT1S2M")
+ Traceback (most recent call last):
+ ...
+ ValueError: Invalid RFC 3339 duration: "PT1S2M"
+ >>> # Time needs time marker
+ >>> rfc3339_duration_to_delta("P1H2S")
+ Traceback (most recent call last):
+ ...
+ ValueError: Invalid RFC 3339 duration: "P1H2S"
+ >>> # Weeks can not be combined with anything else
+ >>> rfc3339_duration_to_delta("P1D2W")
+ Traceback (most recent call last):
+ ...
+ ValueError: Invalid RFC 3339 duration: "P1D2W"
+ >>> rfc3339_duration_to_delta("P2W2H")
+ Traceback (most recent call last):
+ ...
+ ValueError: Invalid RFC 3339 duration: "P2W2H"
+ """
+
+ # Parsing an RFC 3339 duration with regular expressions is not
+ # possible - there would have to be multiple places for the same
+ # values, like seconds. The current code, while more esoteric, is
+ # cleaner without depending on a parsing library. If Python had a
+ # built-in library for parsing we would use it, but we'd like to
+ # avoid excessive use of external libraries.
+
+ # New type for defining tokens, syntax, and semantics all-in-one
+ Token = collections.namedtuple("Token", (
+ "regexp", # To match token; if "value" is not None, must have
+ # a "group" containing digits
+ "value", # datetime.timedelta or None
+ "followers")) # Tokens valid after this token
+ # RFC 3339 "duration" tokens, syntax, and semantics; taken from
+ # the "duration" ABNF definition in RFC 3339, Appendix A.
+ token_end = Token(re.compile(r"$"), None, frozenset())
+ token_second = Token(re.compile(r"(\d+)S"),
+ datetime.timedelta(seconds=1),
+ frozenset((token_end, )))
+ token_minute = Token(re.compile(r"(\d+)M"),
+ datetime.timedelta(minutes=1),
+ frozenset((token_second, token_end)))
+ token_hour = Token(re.compile(r"(\d+)H"),
+ datetime.timedelta(hours=1),
+ frozenset((token_minute, token_end)))
+ token_time = Token(re.compile(r"T"),
+ None,
+ frozenset((token_hour, token_minute,
+ token_second)))
+ token_day = Token(re.compile(r"(\d+)D"),
+ datetime.timedelta(days=1),
+ frozenset((token_time, token_end)))
+ token_month = Token(re.compile(r"(\d+)M"),
+ datetime.timedelta(weeks=4),
+ frozenset((token_day, token_end)))
+ token_year = Token(re.compile(r"(\d+)Y"),
+ datetime.timedelta(weeks=52),
+ frozenset((token_month, token_end)))
+ token_week = Token(re.compile(r"(\d+)W"),
+ datetime.timedelta(weeks=1),
+ frozenset((token_end, )))
+ token_duration = Token(re.compile(r"P"), None,
+ frozenset((token_year, token_month,
+ token_day, token_time,
+ token_week)))
+ # Define starting values:
+ # Value so far
+ value = datetime.timedelta()
+ found_token = None
+ # Following valid tokens
+ followers = frozenset((token_duration, ))
+ # String left to parse
+ s = duration
+ # Loop until end token is found
+ while found_token is not token_end:
+ # Search for any currently valid tokens
+ for token in followers:
+ match = token.regexp.match(s)
+ if match is not None:
+ # Token found
+ if token.value is not None:
+ # Value found, parse digits
+ factor = int(match.group(1), 10)
+ # Add to value so far
+ value += factor * token.value
+ # Strip token from string
+ s = token.regexp.sub("", s, 1)
+ # Go to found token
+ found_token = token
+ # Set valid next tokens
+ followers = found_token.followers
+ break
+ else:
+ # No currently valid tokens were found
+ raise ValueError("Invalid RFC 3339 duration: \"{}\""
+ .format(duration))
+ # End token found
+ return value
+
+
+def parse_pre_1_6_1_interval(interval):
+ """Parse an interval string as documented by Mandos before 1.6.1,
+ and return a datetime.timedelta
+
+ >>> parse_pre_1_6_1_interval('7d') == datetime.timedelta(days=7)
+ True
+ >>> parse_pre_1_6_1_interval('60s') == datetime.timedelta(0, 60)
+ True
+ >>> parse_pre_1_6_1_interval('60m') == datetime.timedelta(hours=1)
+ True
+ >>> parse_pre_1_6_1_interval('24h') == datetime.timedelta(days=1)
+ True
+ >>> parse_pre_1_6_1_interval('1w') == datetime.timedelta(days=7)
+ True
+ >>> parse_pre_1_6_1_interval('5m 30s') == datetime.timedelta(0, 330)
+ True
+ >>> parse_pre_1_6_1_interval('') == datetime.timedelta(0)
+ True
+ >>> # Ignore unknown characters, allow any order and repetitions
+ >>> parse_pre_1_6_1_interval('2dxy7zz11y3m5m') == datetime.timedelta(2, 480, 18000)
+ True
+
+ """
+
+ value = datetime.timedelta(0)
+ regexp = re.compile(r"(\d+)([dsmhw]?)")
+
+ for num, suffix in regexp.findall(interval):
+ if suffix == "d":
+ value += datetime.timedelta(int(num))
+ elif suffix == "s":
+ value += datetime.timedelta(0, int(num))
+ elif suffix == "m":
+ value += datetime.timedelta(0, 0, 0, 0, int(num))
+ elif suffix == "h":
+ value += datetime.timedelta(0, 0, 0, 0, 0, int(num))
+ elif suffix == "w":
+ value += datetime.timedelta(0, 0, 0, 0, 0, 0, int(num))
+ elif suffix == "":
+ value += datetime.timedelta(0, 0, 0, int(num))
+ return value
+
+
+def check_option_syntax(parser, options):
+ """Apply additional restrictions on options, not expressible in
+argparse"""
+
+ def has_commands(options, commands=None):
+ if commands is None:
+ commands = (command.Enable,
+ command.Disable,
+ command.BumpTimeout,
+ command.StartChecker,
+ command.StopChecker,
+ command.IsEnabled,
+ command.Remove,
+ command.SetChecker,
+ command.SetTimeout,
+ command.SetExtendedTimeout,
+ command.SetInterval,
+ command.ApproveByDefault,
+ command.DenyByDefault,
+ command.SetApprovalDelay,
+ command.SetApprovalDuration,
+ command.SetHost,
+ command.SetSecret,
+ command.Approve,
+ command.Deny)
+ return any(isinstance(cmd, commands)
+ for cmd in options.commands)
+
+ if has_commands(options) and not (options.client or options.all):
+ parser.error("Options require clients names or --all.")
+ if options.verbose and has_commands(options):
+ parser.error("--verbose can only be used alone.")
+ if (has_commands(options, (command.DumpJSON,))
+ and (options.verbose or len(options.commands) > 1)):
+ parser.error("--dump-json can only be used alone.")
+ if options.all and not has_commands(options):
+ parser.error("--all requires an action.")
+ if (has_commands(options, (command.IsEnabled,))
+ and len(options.client) > 1):
+ parser.error("--is-enabled requires exactly one client")
+ if (len(options.commands) > 1
+ and has_commands(options, (command.Remove,))
+ and not has_commands(options, (command.Deny,))):
+ parser.error("--remove can only be combined with --deny")
+
+
+class dbus:
+
+ class SystemBus:
+
+ object_manager_iface = "org.freedesktop.DBus.ObjectManager"
+ def get_managed_objects(self, busname, objectpath):
+ return self.call_method("GetManagedObjects", busname,
+ objectpath,
+ self.object_manager_iface)
+
+ properties_iface = "org.freedesktop.DBus.Properties"
+ def set_property(self, busname, objectpath, interface, key,
+ value):
+ self.call_method("Set", busname, objectpath,
+ self.properties_iface, interface, key,
+ value)
+
+
+ class MandosBus(SystemBus):
+ busname_domain = "se.recompile"
+ busname = busname_domain + ".Mandos"
+ server_path = "/"
+ server_interface = busname_domain + ".Mandos"
+ client_interface = busname_domain + ".Mandos.Client"
+ del busname_domain
+
+ def get_clients_and_properties(self):
+ managed_objects = self.get_managed_objects(
+ self.busname, self.server_path)
+ return {objpath: properties[self.client_interface]
+ for objpath, properties in managed_objects.items()
+ if self.client_interface in properties}
+
+ def set_client_property(self, objectpath, key, value):
+ return self.set_property(self.busname, objectpath,
+ self.client_interface, key,
+ value)
+
+ def call_client_method(self, objectpath, method, *args):
+ return self.call_method(method, self.busname, objectpath,
+ self.client_interface, *args)
+
+ def call_server_method(self, method, *args):
+ return self.call_method(method, self.busname,
+ self.server_path,
+ self.server_interface, *args)
+
+ class Error(Exception):
+ pass
+
+ class ConnectFailed(Error):
+ pass
+
+
+class dbus_python_adapter:
+
+ class SystemBus(dbus.MandosBus):
+ """Use dbus-python"""
+
+ def __init__(self, module=dbus_python):
+ self.dbus_python = module
+ self.bus = self.dbus_python.SystemBus()
+
+ @contextlib.contextmanager
+ def convert_exception(self, exception_class=dbus.Error):
+ try:
+ yield
+ except self.dbus_python.exceptions.DBusException as e:
+ # This does what "raise from" would do
+ exc = exception_class(*e.args)
+ exc.__cause__ = e
+ raise exc
+
+ def call_method(self, methodname, busname, objectpath,
+ interface, *args):
+ proxy_object = self.get_object(busname, objectpath)
+ log.debug("D-Bus: %s:%s:%s.%s(%s)", busname, objectpath,
+ interface, methodname,
+ ", ".join(repr(a) for a in args))
+ method = getattr(proxy_object, methodname)
+ with self.convert_exception():
+ with dbus_python_adapter.SilenceLogger(
+ "dbus.proxies"):
+ value = method(*args, dbus_interface=interface)
+ return self.type_filter(value)
+
+ def get_object(self, busname, objectpath):
+ log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
+ busname, objectpath)
+ with self.convert_exception(dbus.ConnectFailed):
+ return self.bus.get_object(busname, objectpath)
+
+ def type_filter(self, value):
+ """Convert the most bothersome types to Python types"""
+ if isinstance(value, self.dbus_python.Boolean):
+ return bool(value)
+ if isinstance(value, self.dbus_python.ObjectPath):
+ return str(value)
+ # Also recurse into dictionaries
+ if isinstance(value, self.dbus_python.Dictionary):
+ return {self.type_filter(key):
+ self.type_filter(subval)
+ for key, subval in value.items()}
+ return value
+
+ def set_client_property(self, objectpath, key, value):
+ if key == "Secret":
+ if not isinstance(value, bytes):
+ value = value.encode("utf-8")
+ value = self.dbus_python.ByteArray(value)
+ return self.set_property(self.busname, objectpath,
+ self.client_interface, key,
+ value)
+
+ class SilenceLogger:
+ "Simple context manager to silence a particular logger"
+ def __init__(self, loggername):
+ self.logger = logging.getLogger(loggername)
+
+ def __enter__(self):
+ self.logger.addFilter(self.nullfilter)
+
+ class NullFilter(logging.Filter):
+ def filter(self, record):
+ return False
+
+ nullfilter = NullFilter()
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.logger.removeFilter(self.nullfilter)
+
+
+ class CachingBus(SystemBus):
+ """A caching layer for dbus_python_adapter.SystemBus"""
+ def __init__(self, *args, **kwargs):
+ self.object_cache = {}
+ super(dbus_python_adapter.CachingBus,
+ self).__init__(*args, **kwargs)
+ def get_object(self, busname, objectpath):
+ try:
+ return self.object_cache[(busname, objectpath)]
+ except KeyError:
+ new_object = super(
+ dbus_python_adapter.CachingBus,
+ self).get_object(busname, objectpath)
+ self.object_cache[(busname, objectpath)] = new_object
+ return new_object
+
+
+class pydbus_adapter:
+ class SystemBus(dbus.MandosBus):
+ def __init__(self, module=pydbus):
+ self.pydbus = module
+ self.bus = self.pydbus.SystemBus()
+
+ @contextlib.contextmanager
+ def convert_exception(self, exception_class=dbus.Error):
+ try:
+ yield
+ except gi.repository.GLib.Error as e:
+ # This does what "raise from" would do
+ exc = exception_class(*e.args)
+ exc.__cause__ = e
+ raise exc
+
+ def call_method(self, methodname, busname, objectpath,
+ interface, *args):
+ proxy_object = self.get(busname, objectpath)
+ log.debug("D-Bus: %s:%s:%s.%s(%s)", busname, objectpath,
+ interface, methodname,
+ ", ".join(repr(a) for a in args))
+ method = getattr(proxy_object[interface], methodname)
+ with self.convert_exception():
+ return method(*args)
+
+ def get(self, busname, objectpath):
+ log.debug("D-Bus: Connect to: (busname=%r, path=%r)",
+ busname, objectpath)
+ with self.convert_exception(dbus.ConnectFailed):
+ if sys.version_info.major <= 2:
+ with warnings.catch_warnings():
+ warnings.filterwarnings(
+ "ignore", "", DeprecationWarning,
+ r"^xml\.etree\.ElementTree$")
+ return self.bus.get(busname, objectpath)
+ else:
+ return self.bus.get(busname, objectpath)
+
+ def set_property(self, busname, objectpath, interface, key,
+ value):
+ proxy_object = self.get(busname, objectpath)
+ log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", busname,
+ objectpath, self.properties_iface, interface,
+ key, value)
+ setattr(proxy_object[interface], key, value)
+
+ class CachingBus(SystemBus):
+ """A caching layer for pydbus_adapter.SystemBus"""
+ def __init__(self, *args, **kwargs):
+ self.object_cache = {}
+ super(pydbus_adapter.CachingBus,
+ self).__init__(*args, **kwargs)
+ def get(self, busname, objectpath):
+ try:
+ return self.object_cache[(busname, objectpath)]
+ except KeyError:
+ new_object = (super(pydbus_adapter.CachingBus, self)
+ .get(busname, objectpath))
+ self.object_cache[(busname, objectpath)] = new_object
+ return new_object
+
+
+def commands_from_options(options):
+
+ commands = list(options.commands)
+
+ def find_cmd(cmd, commands):
+ i = 0
+ for i, c in enumerate(commands):
+ if isinstance(c, cmd):
+ return i
+ return i+1
+
+ # If command.Remove is present, move any instances of command.Deny
+ # to occur ahead of command.Remove.
+ index_of_remove = find_cmd(command.Remove, commands)
+ before_remove = commands[:index_of_remove]
+ after_remove = commands[index_of_remove:]
+ cleaned_after = []
+ for cmd in after_remove:
+ if isinstance(cmd, command.Deny):
+ before_remove.append(cmd)
+ else:
+ cleaned_after.append(cmd)
+ if cleaned_after != after_remove:
+ commands = before_remove + cleaned_after
+
+ # If no command option has been given, show table of clients,
+ # optionally verbosely
+ if not commands:
+ commands.append(command.PrintTable(verbose=options.verbose))
+
+ return commands
+
+
+class command:
+ """A namespace for command classes"""
+
+ class Base:
+ """Abstract base class for commands"""
+ def run(self, clients, bus=None):
+ """Normal commands should implement run_on_one_client(),
+but commands which want to operate on all clients at the same time can
+override this run() method instead.
+"""
+ self.bus = bus
+ for client, properties in clients.items():
+ self.run_on_one_client(client, properties)
+
+
+ class IsEnabled(Base):
+ def run(self, clients, bus=None):
+ properties = next(iter(clients.values()))
+ if properties["Enabled"]:
+ sys.exit(0)
+ sys.exit(1)
+
+
+ class Approve(Base):
+ def run_on_one_client(self, client, properties):
+ self.bus.call_client_method(client, "Approve", True)
+
+
+ class Deny(Base):
+ def run_on_one_client(self, client, properties):
+ self.bus.call_client_method(client, "Approve", False)
+
+
+ class Remove(Base):
+ def run(self, clients, bus):
+ for clientpath in frozenset(clients.keys()):
+ bus.call_server_method("RemoveClient", clientpath)
+
+
+ class Output(Base):
+ """Abstract class for commands outputting client details"""
+ all_keywords = ("Name", "Enabled", "Timeout", "LastCheckedOK",
+ "Created", "Interval", "Host", "KeyID",
+ "Fingerprint", "CheckerRunning",
+ "LastEnabled", "ApprovalPending",
+ "ApprovedByDefault", "LastApprovalRequest",
+ "ApprovalDelay", "ApprovalDuration",
+ "Checker", "ExtendedTimeout", "Expires",
+ "LastCheckerStatus")
+
+
+ class DumpJSON(Output):
+ def run(self, clients, bus=None):
+ data = {properties["Name"]:
+ {key: properties[key]
+ for key in self.all_keywords}
+ for properties in clients.values()}
+ print(json.dumps(data, indent=4, separators=(',', ': ')))
+
+
+ class PrintTable(Output):
+ def __init__(self, verbose=False):
+ self.verbose = verbose
+
+ def run(self, clients, bus=None):
+ default_keywords = ("Name", "Enabled", "Timeout",
+ "LastCheckedOK")
+ keywords = default_keywords
+ if self.verbose:
+ keywords = self.all_keywords
+ print(self.TableOfClients(clients.values(), keywords))
+
+ class TableOfClients:
+ tableheaders = {
+ "Name": "Name",
+ "Enabled": "Enabled",
+ "Timeout": "Timeout",
+ "LastCheckedOK": "Last Successful Check",
+ "LastApprovalRequest": "Last Approval Request",
+ "Created": "Created",
+ "Interval": "Interval",
+ "Host": "Host",
+ "Fingerprint": "Fingerprint",
+ "KeyID": "Key ID",
+ "CheckerRunning": "Check Is Running",
+ "LastEnabled": "Last Enabled",
+ "ApprovalPending": "Approval Is Pending",
+ "ApprovedByDefault": "Approved By Default",
+ "ApprovalDelay": "Approval Delay",
+ "ApprovalDuration": "Approval Duration",
+ "Checker": "Checker",
+ "ExtendedTimeout": "Extended Timeout",
+ "Expires": "Expires",
+ "LastCheckerStatus": "Last Checker Status",
+ }
+
+ def __init__(self, clients, keywords):
+ self.clients = clients
+ self.keywords = keywords
+
+ def __str__(self):
+ return "\n".join(self.rows())
+
+ if sys.version_info.major == 2:
+ __unicode__ = __str__
+ def __str__(self):
+ return str(self).encode(
+ locale.getpreferredencoding())
+
+ def rows(self):
+ format_string = self.row_formatting_string()
+ rows = [self.header_line(format_string)]
+ rows.extend(self.client_line(client, format_string)
+ for client in self.clients)
+ return rows
+
+ def row_formatting_string(self):
+ "Format string used to format table rows"
+ return " ".join("{{{key}:{width}}}".format(
+ width=max(len(self.tableheaders[key]),
+ *(len(self.string_from_client(client,
+ key))
+ for client in self.clients)),
+ key=key)
+ for key in self.keywords)
+
+ def string_from_client(self, client, key):
+ return self.valuetostring(client[key], key)
+
+ @classmethod
+ def valuetostring(cls, value, keyword):
+ if isinstance(value, bool):
+ return "Yes" if value else "No"
+ if keyword in ("Timeout", "Interval", "ApprovalDelay",
+ "ApprovalDuration", "ExtendedTimeout"):
+ return cls.milliseconds_to_string(value)
+ return str(value)
+
+ def header_line(self, format_string):
+ return format_string.format(**self.tableheaders)
+
+ def client_line(self, client, format_string):
+ return format_string.format(
+ **{key: self.string_from_client(client, key)
+ for key in self.keywords})
+
+ @staticmethod
+ def milliseconds_to_string(ms):
+ td = datetime.timedelta(0, 0, 0, ms)
+ return ("{days}{hours:02}:{minutes:02}:{seconds:02}"
+ .format(days="{}T".format(td.days)
+ if td.days else "",
+ hours=td.seconds // 3600,
+ minutes=(td.seconds % 3600) // 60,
+ seconds=td.seconds % 60))
+
+
+ class PropertySetter(Base):
+ "Abstract class for Actions for setting one client property"
+
+ def run_on_one_client(self, client, properties=None):
+ """Set the Client's D-Bus property"""
+ self.bus.set_client_property(client, self.propname,
+ self.value_to_set)
+
+ @property
+ def propname(self):
+ raise NotImplementedError()
+
+
+ class Enable(PropertySetter):
+ propname = "Enabled"
+ value_to_set = True
+
+
+ class Disable(PropertySetter):
+ propname = "Enabled"
+ value_to_set = False
+
+
+ class BumpTimeout(PropertySetter):
+ propname = "LastCheckedOK"
+ value_to_set = ""
+
+
+ class StartChecker(PropertySetter):
+ propname = "CheckerRunning"
+ value_to_set = True
+
+
+ class StopChecker(PropertySetter):
+ propname = "CheckerRunning"
+ value_to_set = False
+
+
+ class ApproveByDefault(PropertySetter):
+ propname = "ApprovedByDefault"
+ value_to_set = True
+
+
+ class DenyByDefault(PropertySetter):
+ propname = "ApprovedByDefault"
+ value_to_set = False
+
+
+ class PropertySetterValue(PropertySetter):
+ """Abstract class for PropertySetter recieving a value as
+constructor argument instead of a class attribute."""
+ def __init__(self, value):
+ self.value_to_set = value
+
+ @classmethod
+ def argparse(cls, argtype):
+ def cmdtype(arg):
+ return cls(argtype(arg))
+ return cmdtype
+
+ class SetChecker(PropertySetterValue):
+ propname = "Checker"
+
+
+ class SetHost(PropertySetterValue):
+ propname = "Host"
+
+
+ class SetSecret(PropertySetterValue):
+ propname = "Secret"
+
+ @property
+ def value_to_set(self):
+ return self._vts
+
+ @value_to_set.setter
+ def value_to_set(self, value):
+ """When setting, read data from supplied file object"""
+ self._vts = value.read()
+ value.close()
+
+
+ class PropertySetterValueMilliseconds(PropertySetterValue):
+ """Abstract class for PropertySetterValue taking a value
+argument as a datetime.timedelta() but should store it as
+milliseconds."""
+
+ @property
+ def value_to_set(self):
+ return self._vts
+
+ @value_to_set.setter
+ def value_to_set(self, value):
+ "When setting, convert value from a datetime.timedelta"
+ self._vts = int(round(value.total_seconds() * 1000))
+
+
+ class SetTimeout(PropertySetterValueMilliseconds):
+ propname = "Timeout"
+
+
+ class SetExtendedTimeout(PropertySetterValueMilliseconds):
+ propname = "ExtendedTimeout"
+
+
+ class SetInterval(PropertySetterValueMilliseconds):
+ propname = "Interval"
+
+
+ class SetApprovalDelay(PropertySetterValueMilliseconds):
+ propname = "ApprovalDelay"
+
+
+ class SetApprovalDuration(PropertySetterValueMilliseconds):
+ propname = "ApprovalDuration"
+
+
+
+class TestCaseWithAssertLogs(unittest.TestCase):
+ """unittest.TestCase.assertLogs only exists in Python 3.4"""
+
+ if not hasattr(unittest.TestCase, "assertLogs"):
+ @contextlib.contextmanager
+ def assertLogs(self, logger, level=logging.INFO):
+ capturing_handler = self.CapturingLevelHandler(level)
+ old_level = logger.level
+ old_propagate = logger.propagate
+ logger.addHandler(capturing_handler)
+ logger.setLevel(level)
+ logger.propagate = False
+ try:
+ yield capturing_handler.watcher
+ finally:
+ logger.propagate = old_propagate
+ logger.removeHandler(capturing_handler)
+ logger.setLevel(old_level)
+ self.assertGreater(len(capturing_handler.watcher.records),
+ 0)
+
+ class CapturingLevelHandler(logging.Handler):
+ def __init__(self, level, *args, **kwargs):
+ logging.Handler.__init__(self, *args, **kwargs)
+ self.watcher = self.LoggingWatcher([], [])
+ def emit(self, record):
+ self.watcher.records.append(record)
+ self.watcher.output.append(self.format(record))
+
+ LoggingWatcher = collections.namedtuple("LoggingWatcher",
+ ("records",
+ "output"))
+
+
+class Unique:
+ """Class for objects which exist only to be unique objects, since
+unittest.mock.sentinel only exists in Python 3.3"""
+
+
+class Test_string_to_delta(TestCaseWithAssertLogs):
+ # Just test basic RFC 3339 functionality here, the doc string for
+ # rfc3339_duration_to_delta() already has more comprehensive
+ # tests, which are run by doctest.
+
+ def test_rfc3339_zero_seconds(self):
+ self.assertEqual(datetime.timedelta(),
+ string_to_delta("PT0S"))
+
+ def test_rfc3339_zero_days(self):
+ self.assertEqual(datetime.timedelta(), string_to_delta("P0D"))
+
+ def test_rfc3339_one_second(self):
+ self.assertEqual(datetime.timedelta(0, 1),
+ string_to_delta("PT1S"))
+
+ def test_rfc3339_two_hours(self):
+ self.assertEqual(datetime.timedelta(0, 7200),
+ string_to_delta("PT2H"))
+
+ def test_falls_back_to_pre_1_6_1_with_warning(self):
+ with self.assertLogs(log, logging.WARNING):
+ value = string_to_delta("2h")
+ self.assertEqual(datetime.timedelta(0, 7200), value)
+
+
+class Test_check_option_syntax(unittest.TestCase):
+ def setUp(self):
+ self.parser = argparse.ArgumentParser()
+ add_command_line_options(self.parser)
+
+ def test_actions_requires_client_or_all(self):
+ for action, value in self.actions.items():
+ args = self.actionargs(action, value)
+ with self.assertParseError():
+ self.parse_args(args)
+
+ # This mostly corresponds to the definition from has_commands() in
+ # check_option_syntax()
+ actions = {
+ "--enable": None,
+ "--disable": None,
+ "--bump-timeout": None,
+ "--start-checker": None,
+ "--stop-checker": None,
+ "--is-enabled": None,
+ "--remove": None,
+ "--checker": "x",
+ "--timeout": "PT0S",
+ "--extended-timeout": "PT0S",
+ "--interval": "PT0S",
+ "--approve-by-default": None,
+ "--deny-by-default": None,
+ "--approval-delay": "PT0S",
+ "--approval-duration": "PT0S",
+ "--host": "hostname",
+ "--secret": "/dev/null",
+ "--approve": None,
+ "--deny": None,
+ }
+
+ @staticmethod
+ def actionargs(action, value, *args):
+ if value is not None:
+ return [action, value] + list(args)
+ else:
+ return [action] + list(args)
+
+ @contextlib.contextmanager
+ def assertParseError(self):
+ with self.assertRaises(SystemExit) as e:
+ with self.redirect_stderr_to_devnull():
+ yield
+ # Exit code from argparse is guaranteed to be "2". Reference:
+ # https://docs.python.org/3/library
+ # /argparse.html#exiting-methods
+ self.assertEqual(2, e.exception.code)
+
+ def parse_args(self, args):
+ options = self.parser.parse_args(args)
+ check_option_syntax(self.parser, options)
+
+ @staticmethod
+ @contextlib.contextmanager
+ def redirect_stderr_to_devnull():
+ old_stderr = sys.stderr
+ with contextlib.closing(open(os.devnull, "w")) as null:
+ sys.stderr = null
+ try:
+ yield
+ finally:
+ sys.stderr = old_stderr
+
+ def check_option_syntax(self, options):
+ check_option_syntax(self.parser, options)
+
+ def test_actions_all_conflicts_with_verbose(self):
+ for action, value in self.actions.items():
+ args = self.actionargs(action, value, "--all",
+ "--verbose")
+ with self.assertParseError():
+ self.parse_args(args)
+
+ def test_actions_with_client_conflicts_with_verbose(self):
+ for action, value in self.actions.items():
+ args = self.actionargs(action, value, "--verbose",
+ "client")
+ with self.assertParseError():
+ self.parse_args(args)
+
+ def test_dump_json_conflicts_with_verbose(self):
+ args = ["--dump-json", "--verbose"]
+ with self.assertParseError():
+ self.parse_args(args)
+
+ def test_dump_json_conflicts_with_action(self):
+ for action, value in self.actions.items():
+ args = self.actionargs(action, value, "--dump-json")
+ with self.assertParseError():
+ self.parse_args(args)
+
+ def test_all_can_not_be_alone(self):
+ args = ["--all"]
+ with self.assertParseError():
+ self.parse_args(args)
+
+ def test_all_is_ok_with_any_action(self):
+ for action, value in self.actions.items():
+ args = self.actionargs(action, value, "--all")
+ self.parse_args(args)
+
+ def test_any_action_is_ok_with_one_client(self):
+ for action, value in self.actions.items():
+ args = self.actionargs(action, value, "client")
+ self.parse_args(args)
+
+ def test_one_client_with_all_actions_except_is_enabled(self):
+ for action, value in self.actions.items():
+ if action == "--is-enabled":
+ continue
+ args = self.actionargs(action, value, "client")
+ self.parse_args(args)
+
+ def test_two_clients_with_all_actions_except_is_enabled(self):
+ for action, value in self.actions.items():
+ if action == "--is-enabled":
+ continue
+ args = self.actionargs(action, value, "client1",
+ "client2")
+ self.parse_args(args)
+
+ def test_two_clients_are_ok_with_actions_except_is_enabled(self):
+ for action, value in self.actions.items():
+ if action == "--is-enabled":
+ continue
+ args = self.actionargs(action, value, "client1",
+ "client2")
+ self.parse_args(args)
+
+ def test_is_enabled_fails_without_client(self):
+ args = ["--is-enabled"]
+ with self.assertParseError():
+ self.parse_args(args)
+
+ def test_is_enabled_fails_with_two_clients(self):
+ args = ["--is-enabled", "client1", "client2"]
+ with self.assertParseError():
+ self.parse_args(args)
+
+ def test_remove_can_only_be_combined_with_action_deny(self):
+ for action, value in self.actions.items():
+ if action in {"--remove", "--deny"}:
+ continue
+ args = self.actionargs(action, value, "--all",
+ "--remove")
+ with self.assertParseError():
+ self.parse_args(args)
+
+
+class Test_dbus_exceptions(unittest.TestCase):
+
+ def test_dbus_ConnectFailed_is_Error(self):
+ with self.assertRaises(dbus.Error):
+ raise dbus.ConnectFailed()
+
+
+class Test_dbus_MandosBus(unittest.TestCase):
+
+ class MockMandosBus(dbus.MandosBus):
+ def __init__(self):
+ self._name = "se.recompile.Mandos"
+ self._server_path = "/"
+ self._server_interface = "se.recompile.Mandos"
+ self._client_interface = "se.recompile.Mandos.Client"
+ self.calls = []
+ self.call_method_return = Unique()
+
+ def call_method(self, methodname, busname, objectpath,
+ interface, *args):
+ self.calls.append((methodname, busname, objectpath,
+ interface, args))
+ return self.call_method_return
+
+ def setUp(self):
+ self.bus = self.MockMandosBus()
+
+ def test_set_client_property(self):
+ self.bus.set_client_property("objectpath", "key", "value")
+ expected_call = ("Set", self.bus._name, "objectpath",
+ "org.freedesktop.DBus.Properties",
+ (self.bus._client_interface, "key", "value"))
+ self.assertIn(expected_call, self.bus.calls)
+
+ def test_call_client_method(self):
+ ret = self.bus.call_client_method("objectpath", "methodname")
+ self.assertIs(self.bus.call_method_return, ret)
+ expected_call = ("methodname", self.bus._name, "objectpath",
+ self.bus._client_interface, ())
+ self.assertIn(expected_call, self.bus.calls)
+
+ def test_call_client_method_with_args(self):
+ args = (Unique(), Unique())
+ ret = self.bus.call_client_method("objectpath", "methodname",
+ *args)
+ self.assertIs(self.bus.call_method_return, ret)
+ expected_call = ("methodname", self.bus._name, "objectpath",
+ self.bus._client_interface,
+ (args[0], args[1]))
+ self.assertIn(expected_call, self.bus.calls)
+
+ def test_get_clients_and_properties(self):
+ managed_objects = {
+ "objectpath": {
+ self.bus._client_interface: {
+ "key": "value",
+ "bool": True,
+ },
+ "irrelevant_interface": {
+ "key": "othervalue",
+ "bool": False,
+ },
+ },
+ "other_objectpath": {
+ "other_irrelevant_interface": {
+ "key": "value 3",
+ "bool": None,
+ },
+ },
+ }
+ expected_clients_and_properties = {
+ "objectpath": {
+ "key": "value",
+ "bool": True,
+ }
+ }
+ self.bus.call_method_return = managed_objects
+ ret = self.bus.get_clients_and_properties()
+ self.assertDictEqual(expected_clients_and_properties, ret)
+ expected_call = ("GetManagedObjects", self.bus._name,
+ self.bus._server_path,
+ "org.freedesktop.DBus.ObjectManager", ())
+ self.assertIn(expected_call, self.bus.calls)
+
+ def test_call_server_method(self):
+ ret = self.bus.call_server_method("methodname")
+ self.assertIs(self.bus.call_method_return, ret)
+ expected_call = ("methodname", self.bus._name,
+ self.bus._server_path,
+ self.bus._server_interface, ())
+ self.assertIn(expected_call, self.bus.calls)
+
+ def test_call_server_method_with_args(self):
+ args = (Unique(), Unique())
+ ret = self.bus.call_server_method("methodname", *args)
+ self.assertIs(self.bus.call_method_return, ret)
+ expected_call = ("methodname", self.bus._name,
+ self.bus._server_path,
+ self.bus._server_interface,
+ (args[0], args[1]))
+ self.assertIn(expected_call, self.bus.calls)
+
+
+class Test_dbus_python_adapter_SystemBus(TestCaseWithAssertLogs):
+
+ def MockDBusPython_func(self, func):
+ class mock_dbus_python:
+ """mock dbus-python module"""
+ class exceptions:
+ """Pseudo-namespace"""
+ class DBusException(Exception):
+ pass
+ class SystemBus:
+ @staticmethod
+ def get_object(busname, objectpath):
+ DBusObject = collections.namedtuple(
+ "DBusObject", ("methodname", "Set"))
+ def method(*args, **kwargs):
+ self.assertEqual({"dbus_interface":
+ "interface"},
+ kwargs)
+ return func(*args)
+ def set_property(interface, key, value,
+ dbus_interface=None):
+ self.assertEqual(
+ "org.freedesktop.DBus.Properties",
+ dbus_interface)
+ self.assertEqual("Secret", key)
+ return func(interface, key, value,
+ dbus_interface=dbus_interface)
+ return DBusObject(methodname=method,
+ Set=set_property)
+ class Boolean:
+ def __init__(self, value):
+ self.value = bool(value)
+ def __bool__(self):
+ return self.value
+ if sys.version_info.major == 2:
+ __nonzero__ = __bool__
+ class ObjectPath(str):
+ pass
+ class Dictionary(dict):
+ pass
+ class ByteArray(bytes):
+ pass
+ return mock_dbus_python
+
+ def call_method(self, bus, methodname, busname, objectpath,
+ interface, *args):
+ with self.assertLogs(log, logging.DEBUG):
+ return bus.call_method(methodname, busname, objectpath,
+ interface, *args)
+
+ def test_call_method_returns(self):
+ expected_method_return = Unique()
+ method_args = (Unique(), Unique())
+ def func(*args):
+ self.assertEqual(len(method_args), len(args))
+ for marg, arg in zip(method_args, args):
+ self.assertIs(marg, arg)
+ return expected_method_return
+ mock_dbus_python = self.MockDBusPython_func(func)
+ bus = dbus_python_adapter.SystemBus(mock_dbus_python)
+ ret = self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface",
+ *method_args)
+ self.assertIs(ret, expected_method_return)
+
+ def test_call_method_filters_bool_true(self):
+ def func():
+ return method_return
+ mock_dbus_python = self.MockDBusPython_func(func)
+ bus = dbus_python_adapter.SystemBus(mock_dbus_python)
+ method_return = mock_dbus_python.Boolean(True)
+ ret = self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+ self.assertTrue(ret)
+ self.assertNotIsInstance(ret, mock_dbus_python.Boolean)
+
+ def test_call_method_filters_bool_false(self):
+ def func():
+ return method_return
+ mock_dbus_python = self.MockDBusPython_func(func)
+ bus = dbus_python_adapter.SystemBus(mock_dbus_python)
+ method_return = mock_dbus_python.Boolean(False)
+ ret = self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+ self.assertFalse(ret)
+ self.assertNotIsInstance(ret, mock_dbus_python.Boolean)
+
+ def test_call_method_filters_objectpath(self):
+ def func():
+ return method_return
+ mock_dbus_python = self.MockDBusPython_func(func)
+ bus = dbus_python_adapter.SystemBus(mock_dbus_python)
+ method_return = mock_dbus_python.ObjectPath("objectpath")
+ ret = self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+ self.assertEqual("objectpath", ret)
+ self.assertIsNot("objectpath", ret)
+ self.assertNotIsInstance(ret, mock_dbus_python.ObjectPath)
+
+ def test_call_method_filters_booleans_in_dict(self):
+ def func():
+ return method_return
+ mock_dbus_python = self.MockDBusPython_func(func)
+ bus = dbus_python_adapter.SystemBus(mock_dbus_python)
+ method_return = mock_dbus_python.Dictionary(
+ {mock_dbus_python.Boolean(True):
+ mock_dbus_python.Boolean(False),
+ mock_dbus_python.Boolean(False):
+ mock_dbus_python.Boolean(True)})
+ ret = self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+ expected_method_return = {True: False,
+ False: True}
+ self.assertEqual(expected_method_return, ret)
+ self.assertNotIsInstance(ret, mock_dbus_python.Dictionary)
+
+ def test_call_method_filters_objectpaths_in_dict(self):
+ def func():
+ return method_return
+ mock_dbus_python = self.MockDBusPython_func(func)
+ bus = dbus_python_adapter.SystemBus(mock_dbus_python)
+ method_return = mock_dbus_python.Dictionary(
+ {mock_dbus_python.ObjectPath("objectpath_key_1"):
+ mock_dbus_python.ObjectPath("objectpath_value_1"),
+ mock_dbus_python.ObjectPath("objectpath_key_2"):
+ mock_dbus_python.ObjectPath("objectpath_value_2")})
+ ret = self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+ expected_method_return = {str(key): str(value)
+ for key, value in
+ method_return.items()}
+ self.assertEqual(expected_method_return, ret)
+ self.assertIsInstance(ret, dict)
+ self.assertNotIsInstance(ret, mock_dbus_python.Dictionary)
+
+ def test_call_method_filters_dict_in_dict(self):
+ def func():
+ return method_return
+ mock_dbus_python = self.MockDBusPython_func(func)
+ bus = dbus_python_adapter.SystemBus(mock_dbus_python)
+ method_return = mock_dbus_python.Dictionary(
+ {"key1": mock_dbus_python.Dictionary({"key11": "value11",
+ "key12": "value12"}),
+ "key2": mock_dbus_python.Dictionary({"key21": "value21",
+ "key22": "value22"})})
+ ret = self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+ expected_method_return = {
+ "key1": {"key11": "value11",
+ "key12": "value12"},
+ "key2": {"key21": "value21",
+ "key22": "value22"},
+ }
+ self.assertEqual(expected_method_return, ret)
+ self.assertIsInstance(ret, dict)
+ self.assertNotIsInstance(ret, mock_dbus_python.Dictionary)
+ for key, value in ret.items():
+ self.assertIsInstance(value, dict)
+ self.assertEqual(expected_method_return[key], value)
+ self.assertNotIsInstance(value,
+ mock_dbus_python.Dictionary)
+
+ def test_call_method_filters_dict_three_deep(self):
+ def func():
+ return method_return
+ mock_dbus_python = self.MockDBusPython_func(func)
+ bus = dbus_python_adapter.SystemBus(mock_dbus_python)
+ method_return = mock_dbus_python.Dictionary(
+ {"key1":
+ mock_dbus_python.Dictionary(
+ {"key2":
+ mock_dbus_python.Dictionary(
+ {"key3":
+ mock_dbus_python.Boolean(True),
+ }),
+ }),
+ })
+ ret = self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+ expected_method_return = {"key1": {"key2": {"key3": True}}}
+ self.assertEqual(expected_method_return, ret)
+ self.assertIsInstance(ret, dict)
+ self.assertNotIsInstance(ret, mock_dbus_python.Dictionary)
+ self.assertIsInstance(ret["key1"], dict)
+ self.assertNotIsInstance(ret["key1"],
+ mock_dbus_python.Dictionary)
+ self.assertIsInstance(ret["key1"]["key2"], dict)
+ self.assertNotIsInstance(ret["key1"]["key2"],
+ mock_dbus_python.Dictionary)
+ self.assertTrue(ret["key1"]["key2"]["key3"])
+ self.assertNotIsInstance(ret["key1"]["key2"]["key3"],
+ mock_dbus_python.Boolean)
+
+ def test_call_method_handles_exception(self):
+ dbus_logger = logging.getLogger("dbus.proxies")
+
+ def func():
+ dbus_logger.error("Test")
+ raise mock_dbus_python.exceptions.DBusException()
+
+ mock_dbus_python = self.MockDBusPython_func(func)
+ bus = dbus_python_adapter.SystemBus(mock_dbus_python)
+
+ class CountingHandler(logging.Handler):
+ count = 0
+ def emit(self, record):
+ self.count += 1
+
+ counting_handler = CountingHandler()
+
+ dbus_logger.addHandler(counting_handler)
+
+ try:
+ with self.assertRaises(dbus.Error) as e:
+ self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+ finally:
+ dbus_logger.removeFilter(counting_handler)
+
+ self.assertNotIsInstance(e, dbus.ConnectFailed)
+
+ # Make sure the dbus logger was suppressed
+ self.assertEqual(0, counting_handler.count)
+
+ def test_Set_Secret_sends_bytearray(self):
+ ret = [None]
+ def func(*args, **kwargs):
+ ret[0] = (args, kwargs)
+ mock_dbus_python = self.MockDBusPython_func(func)
+ bus = dbus_python_adapter.SystemBus(mock_dbus_python)
+ bus.set_client_property("objectpath", "Secret", "value")
+ expected_call = (("se.recompile.Mandos.Client", "Secret",
+ mock_dbus_python.ByteArray(b"value")),
+ {"dbus_interface":
+ "org.freedesktop.DBus.Properties"})
+ self.assertEqual(expected_call, ret[0])
+ if sys.version_info.major == 2:
+ self.assertIsInstance(ret[0][0][-1],
+ mock_dbus_python.ByteArray)
+
+ def test_get_object_converts_to_correct_exception(self):
+ bus = dbus_python_adapter.SystemBus(
+ self.fake_dbus_python_raises_exception_on_connect)
+ with self.assertRaises(dbus.ConnectFailed):
+ self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+
+ class fake_dbus_python_raises_exception_on_connect:
+ """fake dbus-python module"""
+ class exceptions:
+ """Pseudo-namespace"""
+ class DBusException(Exception):
+ pass
+
+ @classmethod
+ def SystemBus(cls):
+ def get_object(busname, objectpath):
+ raise cls.exceptions.DBusException()
+ Bus = collections.namedtuple("Bus", ["get_object"])
+ return Bus(get_object=get_object)
+
+
+class Test_dbus_python_adapter_CachingBus(unittest.TestCase):
+ class mock_dbus_python:
+ """mock dbus-python modules"""
+ class SystemBus:
+ @staticmethod
+ def get_object(busname, objectpath):
+ return Unique()
+
+ def setUp(self):
+ self.bus = dbus_python_adapter.CachingBus(
+ self.mock_dbus_python)
+
+ def test_returns_distinct_objectpaths(self):
+ obj1 = self.bus.get_object("busname", "objectpath1")
+ self.assertIsInstance(obj1, Unique)
+ obj2 = self.bus.get_object("busname", "objectpath2")
+ self.assertIsInstance(obj2, Unique)
+ self.assertIsNot(obj1, obj2)
+
+ def test_returns_distinct_busnames(self):
+ obj1 = self.bus.get_object("busname1", "objectpath")
+ self.assertIsInstance(obj1, Unique)
+ obj2 = self.bus.get_object("busname2", "objectpath")
+ self.assertIsInstance(obj2, Unique)
+ self.assertIsNot(obj1, obj2)
+
+ def test_returns_distinct_both(self):
+ obj1 = self.bus.get_object("busname1", "objectpath")
+ self.assertIsInstance(obj1, Unique)
+ obj2 = self.bus.get_object("busname2", "objectpath")
+ self.assertIsInstance(obj2, Unique)
+ self.assertIsNot(obj1, obj2)
+
+ def test_returns_same(self):
+ obj1 = self.bus.get_object("busname", "objectpath")
+ self.assertIsInstance(obj1, Unique)
+ obj2 = self.bus.get_object("busname", "objectpath")
+ self.assertIsInstance(obj2, Unique)
+ self.assertIs(obj1, obj2)
+
+ def test_returns_same_old(self):
+ obj1 = self.bus.get_object("busname1", "objectpath1")
+ self.assertIsInstance(obj1, Unique)
+ obj2 = self.bus.get_object("busname2", "objectpath2")
+ self.assertIsInstance(obj2, Unique)
+ obj1b = self.bus.get_object("busname1", "objectpath1")
+ self.assertIsInstance(obj1b, Unique)
+ self.assertIsNot(obj1, obj2)
+ self.assertIsNot(obj2, obj1b)
+ self.assertIs(obj1, obj1b)
+
+
+class Test_pydbus_adapter_SystemBus(TestCaseWithAssertLogs):
+
+ def Stub_pydbus_func(self, func):
+ class stub_pydbus:
+ """stub pydbus module"""
+ class SystemBus:
+ @staticmethod
+ def get(busname, objectpath):
+ DBusObject = collections.namedtuple(
+ "DBusObject", ("methodname",))
+ return {"interface":
+ DBusObject(methodname=func)}
+ return stub_pydbus
+
+ def call_method(self, bus, methodname, busname, objectpath,
+ interface, *args):
+ with self.assertLogs(log, logging.DEBUG):
+ return bus.call_method(methodname, busname, objectpath,
+ interface, *args)
+
+ def test_call_method_returns(self):
+ expected_method_return = Unique()
+ method_args = (Unique(), Unique())
+ def func(*args):
+ self.assertEqual(len(method_args), len(args))
+ for marg, arg in zip(method_args, args):
+ self.assertIs(marg, arg)
+ return expected_method_return
+ stub_pydbus = self.Stub_pydbus_func(func)
+ bus = pydbus_adapter.SystemBus(stub_pydbus)
+ ret = self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface",
+ *method_args)
+ self.assertIs(ret, expected_method_return)
+
+ def test_call_method_handles_exception(self):
+ dbus_logger = logging.getLogger("dbus.proxies")
+
+ def func():
+ raise gi.repository.GLib.Error()
+
+ stub_pydbus = self.Stub_pydbus_func(func)
+ bus = pydbus_adapter.SystemBus(stub_pydbus)
+
+ with self.assertRaises(dbus.Error) as e:
+ self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+
+ self.assertNotIsInstance(e, dbus.ConnectFailed)
+
+ def test_get_converts_to_correct_exception(self):
+ bus = pydbus_adapter.SystemBus(
+ self.fake_pydbus_raises_exception_on_connect)
+ with self.assertRaises(dbus.ConnectFailed):
+ self.call_method(bus, "methodname", "busname",
+ "objectpath", "interface")
+
+ class fake_pydbus_raises_exception_on_connect:
+ """fake dbus-python module"""
+ @classmethod
+ def SystemBus(cls):
+ def get(busname, objectpath):
+ raise gi.repository.GLib.Error()
+ Bus = collections.namedtuple("Bus", ["get"])
+ return Bus(get=get)
+
+ def test_set_property_uses_setattr(self):
+ class Object:
+ pass
+ obj = Object()
+ class pydbus_spy:
+ class SystemBus:
+ @staticmethod
+ def get(busname, objectpath):
+ return {"interface": obj}
+ bus = pydbus_adapter.SystemBus(pydbus_spy)
+ value = Unique()
+ bus.set_property("busname", "objectpath", "interface", "key",
+ value)
+ self.assertIs(value, obj.key)
+
+ def test_get_suppresses_xml_deprecation_warning(self):
+ if sys.version_info.major >= 3:
+ return
+ class stub_pydbus_get:
+ class SystemBus:
+ @staticmethod
+ def get(busname, objectpath):
+ warnings.warn_explicit(
+ "deprecated", DeprecationWarning,
+ "xml.etree.ElementTree", 0)
+ bus = pydbus_adapter.SystemBus(stub_pydbus_get)
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ bus.get("busname", "objectpath")
+ self.assertEqual(0, len(w))
+
+
+class Test_pydbus_adapter_CachingBus(unittest.TestCase):
+ class stub_pydbus:
+ """stub pydbus module"""
+ class SystemBus:
+ @staticmethod
+ def get(busname, objectpath):
+ return Unique()
+
+ def setUp(self):
+ self.bus = pydbus_adapter.CachingBus(self.stub_pydbus)
+
+ def test_returns_distinct_objectpaths(self):
+ obj1 = self.bus.get("busname", "objectpath1")
+ self.assertIsInstance(obj1, Unique)
+ obj2 = self.bus.get("busname", "objectpath2")
+ self.assertIsInstance(obj2, Unique)
+ self.assertIsNot(obj1, obj2)
+
+ def test_returns_distinct_busnames(self):
+ obj1 = self.bus.get("busname1", "objectpath")
+ self.assertIsInstance(obj1, Unique)
+ obj2 = self.bus.get("busname2", "objectpath")
+ self.assertIsInstance(obj2, Unique)
+ self.assertIsNot(obj1, obj2)
+
+ def test_returns_distinct_both(self):
+ obj1 = self.bus.get("busname1", "objectpath")
+ self.assertIsInstance(obj1, Unique)
+ obj2 = self.bus.get("busname2", "objectpath")
+ self.assertIsInstance(obj2, Unique)
+ self.assertIsNot(obj1, obj2)
+
+ def test_returns_same(self):
+ obj1 = self.bus.get("busname", "objectpath")
+ self.assertIsInstance(obj1, Unique)
+ obj2 = self.bus.get("busname", "objectpath")
+ self.assertIsInstance(obj2, Unique)
+ self.assertIs(obj1, obj2)
+
+ def test_returns_same_old(self):
+ obj1 = self.bus.get("busname1", "objectpath1")
+ self.assertIsInstance(obj1, Unique)
+ obj2 = self.bus.get("busname2", "objectpath2")
+ self.assertIsInstance(obj2, Unique)
+ obj1b = self.bus.get("busname1", "objectpath1")
+ self.assertIsInstance(obj1b, Unique)
+ self.assertIsNot(obj1, obj2)
+ self.assertIsNot(obj2, obj1b)
+ self.assertIs(obj1, obj1b)
+
+
+class Test_commands_from_options(unittest.TestCase):
+
+ def setUp(self):
+ self.parser = argparse.ArgumentParser()
+ add_command_line_options(self.parser)
+
+ def test_is_enabled(self):
+ self.assert_command_from_args(["--is-enabled", "client"],
+ command.IsEnabled)
+
+ def assert_command_from_args(self, args, command_cls, length=1,
+ clients=None, **cmd_attrs):
+ """Assert that parsing ARGS should result in an instance of
+COMMAND_CLS with (optionally) all supplied attributes (CMD_ATTRS)."""
+ options = self.parser.parse_args(args)
+ check_option_syntax(self.parser, options)
+ commands = commands_from_options(options)
+ self.assertEqual(length, len(commands))
+ for command in commands:
+ if isinstance(command, command_cls):
+ break
+ else:
+ self.assertIsInstance(command, command_cls)
+ if clients is not None:
+ self.assertEqual(clients, options.client)
+ for key, value in cmd_attrs.items():
+ self.assertEqual(value, getattr(command, key))
+
+ def assert_commands_from_args(self, args, commands, clients=None):
+ for cmd in commands:
+ self.assert_command_from_args(args, cmd,
+ length=len(commands),
+ clients=clients)
+
+ def test_is_enabled_short(self):
+ self.assert_command_from_args(["-V", "client"],
+ command.IsEnabled)
+
+ def test_approve(self):
+ self.assert_command_from_args(["--approve", "client"],
+ command.Approve)
+
+ def test_approve_short(self):
+ self.assert_command_from_args(["-A", "client"],
+ command.Approve)
+
+ def test_deny(self):
+ self.assert_command_from_args(["--deny", "client"],
+ command.Deny)
+
+ def test_deny_short(self):
+ self.assert_command_from_args(["-D", "client"], command.Deny)
+
+ def test_remove(self):
+ self.assert_command_from_args(["--remove", "client"],
+ command.Remove)
+
+ def test_deny_before_remove(self):
+ options = self.parser.parse_args(["--deny", "--remove",
+ "client"])
+ check_option_syntax(self.parser, options)
+ commands = commands_from_options(options)
+ self.assertEqual(2, len(commands))
+ self.assertIsInstance(commands[0], command.Deny)
+ self.assertIsInstance(commands[1], command.Remove)
+
+ def test_deny_before_remove_reversed(self):
+ options = self.parser.parse_args(["--remove", "--deny",
+ "--all"])
+ check_option_syntax(self.parser, options)
+ commands = commands_from_options(options)
+ self.assertEqual(2, len(commands))
+ self.assertIsInstance(commands[0], command.Deny)
+ self.assertIsInstance(commands[1], command.Remove)
+
+ def test_remove_short(self):
+ self.assert_command_from_args(["-r", "client"],
+ command.Remove)
+
+ def test_dump_json(self):
+ self.assert_command_from_args(["--dump-json"],
+ command.DumpJSON)
+
+ def test_enable(self):
+ self.assert_command_from_args(["--enable", "client"],
+ command.Enable)
+
+ def test_enable_short(self):
+ self.assert_command_from_args(["-e", "client"],
+ command.Enable)
+
+ def test_disable(self):
+ self.assert_command_from_args(["--disable", "client"],
+ command.Disable)
+
+ def test_disable_short(self):
+ self.assert_command_from_args(["-d", "client"],
+ command.Disable)
+
+ def test_bump_timeout(self):
+ self.assert_command_from_args(["--bump-timeout", "client"],
+ command.BumpTimeout)
+
+ def test_bump_timeout_short(self):
+ self.assert_command_from_args(["-b", "client"],
+ command.BumpTimeout)
+
+ def test_start_checker(self):
+ self.assert_command_from_args(["--start-checker", "client"],
+ command.StartChecker)
+
+ def test_stop_checker(self):
+ self.assert_command_from_args(["--stop-checker", "client"],
+ command.StopChecker)
+
+ def test_approve_by_default(self):
+ self.assert_command_from_args(["--approve-by-default",
+ "client"],
+ command.ApproveByDefault)
+
+ def test_deny_by_default(self):
+ self.assert_command_from_args(["--deny-by-default", "client"],
+ command.DenyByDefault)
+
+ def test_checker(self):
+ self.assert_command_from_args(["--checker", ":", "client"],
+ command.SetChecker,
+ value_to_set=":")
+
+ def test_checker_empty(self):
+ self.assert_command_from_args(["--checker", "", "client"],
+ command.SetChecker,
+ value_to_set="")
+
+ def test_checker_short(self):
+ self.assert_command_from_args(["-c", ":", "client"],
+ command.SetChecker,
+ value_to_set=":")
+
+ def test_host(self):
+ self.assert_command_from_args(
+ ["--host", "client.example.org", "client"],
+ command.SetHost, value_to_set="client.example.org")
+
+ def test_host_short(self):
+ self.assert_command_from_args(
+ ["-H", "client.example.org", "client"], command.SetHost,
+ value_to_set="client.example.org")
+
+ def test_secret_devnull(self):
+ self.assert_command_from_args(["--secret", os.path.devnull,
+ "client"], command.SetSecret,
+ value_to_set=b"")
+
+ def test_secret_tempfile(self):
+ with tempfile.NamedTemporaryFile(mode="r+b") as f:
+ value = b"secret\0xyzzy\nbar"
+ f.write(value)
+ f.seek(0)
+ self.assert_command_from_args(["--secret", f.name,
+ "client"],
+ command.SetSecret,
+ value_to_set=value)
+
+ def test_secret_devnull_short(self):
+ self.assert_command_from_args(["-s", os.path.devnull,
+ "client"], command.SetSecret,
+ value_to_set=b"")
+
+ def test_secret_tempfile_short(self):
+ with tempfile.NamedTemporaryFile(mode="r+b") as f:
+ value = b"secret\0xyzzy\nbar"
+ f.write(value)
+ f.seek(0)
+ self.assert_command_from_args(["-s", f.name, "client"],
+ command.SetSecret,
+ value_to_set=value)
+
+ def test_timeout(self):
+ self.assert_command_from_args(["--timeout", "PT5M", "client"],
+ command.SetTimeout,
+ value_to_set=300000)
+
+ def test_timeout_short(self):
+ self.assert_command_from_args(["-t", "PT5M", "client"],
+ command.SetTimeout,
+ value_to_set=300000)
+
+ def test_extended_timeout(self):
+ self.assert_command_from_args(["--extended-timeout", "PT15M",
+ "client"],
+ command.SetExtendedTimeout,
+ value_to_set=900000)
+
+ def test_interval(self):
+ self.assert_command_from_args(["--interval", "PT2M",
+ "client"], command.SetInterval,
+ value_to_set=120000)
+
+ def test_interval_short(self):
+ self.assert_command_from_args(["-i", "PT2M", "client"],
+ command.SetInterval,
+ value_to_set=120000)
+
+ def test_approval_delay(self):
+ self.assert_command_from_args(["--approval-delay", "PT30S",
+ "client"],
+ command.SetApprovalDelay,
+ value_to_set=30000)
+
+ def test_approval_duration(self):
+ self.assert_command_from_args(["--approval-duration", "PT1S",
+ "client"],
+ command.SetApprovalDuration,
+ value_to_set=1000)
+
+ def test_print_table(self):
+ self.assert_command_from_args([], command.PrintTable,
+ verbose=False)
+
+ def test_print_table_verbose(self):
+ self.assert_command_from_args(["--verbose"],
+ command.PrintTable,
+ verbose=True)
+
+ def test_print_table_verbose_short(self):
+ self.assert_command_from_args(["-v"], command.PrintTable,
+ verbose=True)
+
+
+ def test_manual_page_example_1(self):
+ self.assert_command_from_args("",
+ command.PrintTable,
+ clients=[],
+ verbose=False)
+
+ def test_manual_page_example_2(self):
+ self.assert_command_from_args(
+ "--verbose foo1.example.org foo2.example.org".split(),
+ command.PrintTable, clients=["foo1.example.org",
+ "foo2.example.org"],
+ verbose=True)
+
+ def test_manual_page_example_3(self):
+ self.assert_command_from_args("--enable --all".split(),
+ command.Enable,
+ clients=[])
+
+ def test_manual_page_example_4(self):
+ self.assert_commands_from_args(
+ ("--timeout=PT5M --interval=PT1M foo1.example.org"
+ " foo2.example.org").split(),
+ [command.SetTimeout, command.SetInterval],
+ clients=["foo1.example.org", "foo2.example.org"])
+
+ def test_manual_page_example_5(self):
+ self.assert_command_from_args("--approve --all".split(),
+ command.Approve,
+ clients=[])
+
+
+class TestCommand(unittest.TestCase):
+ """Abstract class for tests of command classes"""
+
+ class FakeMandosBus(dbus.MandosBus):
+ def __init__(self, testcase):
+ self.client_properties = {
+ "Name": "foo",
+ "KeyID": ("92ed150794387c03ce684574b1139a65"
+ "94a34f895daaaf09fd8ea90a27cddb12"),
+ "Secret": b"secret",
+ "Host": "foo.example.org",
+ "Enabled": True,
+ "Timeout": 300000,
+ "LastCheckedOK": "2019-02-03T00:00:00",
+ "Created": "2019-01-02T00:00:00",
+ "Interval": 120000,
+ "Fingerprint": ("778827225BA7DE539C5A"
+ "7CFA59CFF7CDBD9A5920"),
+ "CheckerRunning": False,
+ "LastEnabled": "2019-01-03T00:00:00",
+ "ApprovalPending": False,
+ "ApprovedByDefault": True,
+ "LastApprovalRequest": "",
+ "ApprovalDelay": 0,
+ "ApprovalDuration": 1000,
+ "Checker": "fping -q -- %(host)s",
+ "ExtendedTimeout": 900000,
+ "Expires": "2019-02-04T00:00:00",
+ "LastCheckerStatus": 0,
+ }
+ self.other_client_properties = {
+ "Name": "barbar",
+ "KeyID": ("0558568eedd67d622f5c83b35a115f79"
+ "6ab612cff5ad227247e46c2b020f441c"),
+ "Secret": b"secretbar",
+ "Host": "192.0.2.3",
+ "Enabled": True,
+ "Timeout": 300000,
+ "LastCheckedOK": "2019-02-04T00:00:00",
+ "Created": "2019-01-03T00:00:00",
+ "Interval": 120000,
+ "Fingerprint": ("3E393AEAEFB84C7E89E2"
+ "F547B3A107558FCA3A27"),
+ "CheckerRunning": True,
+ "LastEnabled": "2019-01-04T00:00:00",
+ "ApprovalPending": False,
+ "ApprovedByDefault": False,
+ "LastApprovalRequest": "2019-01-03T00:00:00",
+ "ApprovalDelay": 30000,
+ "ApprovalDuration": 93785000,
+ "Checker": ":",
+ "ExtendedTimeout": 900000,
+ "Expires": "2019-02-05T00:00:00",
+ "LastCheckerStatus": -2,
+ }
+ self.clients = collections.OrderedDict(
+ [
+ ("client_objectpath", self.client_properties),
+ ("other_client_objectpath",
+ self.other_client_properties),
+ ])
+ self.one_client = {"client_objectpath":
+ self.client_properties}
+ self.testcase = testcase
+ self.calls = []
+
+ def call_method(self, methodname, busname, objectpath,
+ interface, *args):
+ self.testcase.assertEqual("se.recompile.Mandos", busname)
+ self.calls.append((methodname, busname, objectpath,
+ interface, args))
+ if interface == "org.freedesktop.DBus.Properties":
+ if methodname == "Set":
+ self.testcase.assertEqual(3, len(args))
+ interface, key, value = args
+ self.testcase.assertEqual(
+ "se.recompile.Mandos.Client", interface)
+ self.clients[objectpath][key] = value
+ return
+ elif interface == "se.recompile.Mandos":
+ self.testcase.assertEqual("RemoveClient", methodname)
+ self.testcase.assertEqual(1, len(args))
+ clientpath = args[0]
+ del self.clients[clientpath]
+ return
+ elif interface == "se.recompile.Mandos.Client":
+ if methodname == "Approve":
+ self.testcase.assertEqual(1, len(args))
+ return
+ raise ValueError()
+
+ def setUp(self):
+ self.bus = self.FakeMandosBus(self)
+
+
+class TestBaseCommands(TestCommand):
+
+ def test_IsEnabled_exits_successfully(self):
+ with self.assertRaises(SystemExit) as e:
+ command.IsEnabled().run(self.bus.one_client)
+ if e.exception.code is not None:
+ self.assertEqual(0, e.exception.code)
+ else:
+ self.assertIsNone(e.exception.code)
+
+ def test_IsEnabled_exits_with_failure(self):
+ self.bus.client_properties["Enabled"] = False
+ with self.assertRaises(SystemExit) as e:
+ command.IsEnabled().run(self.bus.one_client)
+ if isinstance(e.exception.code, int):
+ self.assertNotEqual(0, e.exception.code)
+ else:
+ self.assertIsNotNone(e.exception.code)
+
+ def test_Approve(self):
+ busname = "se.recompile.Mandos"
+ client_interface = "se.recompile.Mandos.Client"
+ command.Approve().run(self.bus.clients, self.bus)
+ for clientpath in self.bus.clients:
+ self.assertIn(("Approve", busname, clientpath,
+ client_interface, (True,)), self.bus.calls)
+
+ def test_Deny(self):
+ busname = "se.recompile.Mandos"
+ client_interface = "se.recompile.Mandos.Client"
+ command.Deny().run(self.bus.clients, self.bus)
+ for clientpath in self.bus.clients:
+ self.assertIn(("Approve", busname, clientpath,
+ client_interface, (False,)),
+ self.bus.calls)
+
+ def test_Remove(self):
+ command.Remove().run(self.bus.clients, self.bus)
+ for clientpath in self.bus.clients:
+ self.assertIn(("RemoveClient", dbus_busname,
+ dbus_server_path, dbus_server_interface,
+ (clientpath,)), self.bus.calls)
+
+ expected_json = {
+ "foo": {
+ "Name": "foo",
+ "KeyID": ("92ed150794387c03ce684574b1139a65"
+ "94a34f895daaaf09fd8ea90a27cddb12"),
+ "Host": "foo.example.org",
+ "Enabled": True,
+ "Timeout": 300000,
+ "LastCheckedOK": "2019-02-03T00:00:00",
+ "Created": "2019-01-02T00:00:00",
+ "Interval": 120000,
+ "Fingerprint": ("778827225BA7DE539C5A"
+ "7CFA59CFF7CDBD9A5920"),
+ "CheckerRunning": False,
+ "LastEnabled": "2019-01-03T00:00:00",
+ "ApprovalPending": False,
+ "ApprovedByDefault": True,
+ "LastApprovalRequest": "",
+ "ApprovalDelay": 0,
+ "ApprovalDuration": 1000,
+ "Checker": "fping -q -- %(host)s",
+ "ExtendedTimeout": 900000,
+ "Expires": "2019-02-04T00:00:00",
+ "LastCheckerStatus": 0,
+ },
+ "barbar": {
+ "Name": "barbar",
+ "KeyID": ("0558568eedd67d622f5c83b35a115f79"
+ "6ab612cff5ad227247e46c2b020f441c"),
+ "Host": "192.0.2.3",
+ "Enabled": True,
+ "Timeout": 300000,
+ "LastCheckedOK": "2019-02-04T00:00:00",
+ "Created": "2019-01-03T00:00:00",
+ "Interval": 120000,
+ "Fingerprint": ("3E393AEAEFB84C7E89E2"
+ "F547B3A107558FCA3A27"),
+ "CheckerRunning": True,
+ "LastEnabled": "2019-01-04T00:00:00",
+ "ApprovalPending": False,
+ "ApprovedByDefault": False,
+ "LastApprovalRequest": "2019-01-03T00:00:00",
+ "ApprovalDelay": 30000,
+ "ApprovalDuration": 93785000,
+ "Checker": ":",
+ "ExtendedTimeout": 900000,
+ "Expires": "2019-02-05T00:00:00",
+ "LastCheckerStatus": -2,
+ },
+ }
+
+ def test_DumpJSON_normal(self):
+ with self.capture_stdout_to_buffer() as buffer:
+ command.DumpJSON().run(self.bus.clients)
+ json_data = json.loads(buffer.getvalue())
+ self.assertDictEqual(self.expected_json, json_data)
+
+ @staticmethod
+ @contextlib.contextmanager
+ def capture_stdout_to_buffer():
+ capture_buffer = io.StringIO()
+ old_stdout = sys.stdout
+ sys.stdout = capture_buffer
+ try:
+ yield capture_buffer
+ finally:
+ sys.stdout = old_stdout
+
+ def test_DumpJSON_one_client(self):
+ with self.capture_stdout_to_buffer() as buffer:
+ command.DumpJSON().run(self.bus.one_client)
+ json_data = json.loads(buffer.getvalue())
+ expected_json = {"foo": self.expected_json["foo"]}
+ self.assertDictEqual(expected_json, json_data)
+
+ def test_PrintTable_normal(self):
+ with self.capture_stdout_to_buffer() as buffer:
+ command.PrintTable().run(self.bus.clients)
+ expected_output = "\n".join((
+ "Name Enabled Timeout Last Successful Check",
+ "foo Yes 00:05:00 2019-02-03T00:00:00 ",
+ "barbar Yes 00:05:00 2019-02-04T00:00:00 ",
+ )) + "\n"
+ self.assertEqual(expected_output, buffer.getvalue())
+
+ def test_PrintTable_verbose(self):
+ with self.capture_stdout_to_buffer() as buffer:
+ command.PrintTable(verbose=True).run(self.bus.clients)
+ columns = (
+ (
+ "Name ",
+ "foo ",
+ "barbar ",
+ ),(
+ "Enabled ",
+ "Yes ",
+ "Yes ",
+ ),(
+ "Timeout ",
+ "00:05:00 ",
+ "00:05:00 ",
+ ),(
+ "Last Successful Check ",
+ "2019-02-03T00:00:00 ",
+ "2019-02-04T00:00:00 ",
+ ),(
+ "Created ",
+ "2019-01-02T00:00:00 ",
+ "2019-01-03T00:00:00 ",
+ ),(
+ "Interval ",
+ "00:02:00 ",
+ "00:02:00 ",
+ ),(
+ "Host ",
+ "foo.example.org ",
+ "192.0.2.3 ",
+ ),(
+ ("Key ID "
+ " "),
+ ("92ed150794387c03ce684574b1139a6594a34f895daaaf09fd8"
+ "ea90a27cddb12 "),
+ ("0558568eedd67d622f5c83b35a115f796ab612cff5ad227247e"
+ "46c2b020f441c "),
+ ),(
+ "Fingerprint ",
+ "778827225BA7DE539C5A7CFA59CFF7CDBD9A5920 ",
+ "3E393AEAEFB84C7E89E2F547B3A107558FCA3A27 ",
+ ),(
+ "Check Is Running ",
+ "No ",
+ "Yes ",
+ ),(
+ "Last Enabled ",
+ "2019-01-03T00:00:00 ",
+ "2019-01-04T00:00:00 ",
+ ),(
+ "Approval Is Pending ",
+ "No ",
+ "No ",
+ ),(
+ "Approved By Default ",
+ "Yes ",
+ "No ",
+ ),(
+ "Last Approval Request ",
+ " ",
+ "2019-01-03T00:00:00 ",
+ ),(
+ "Approval Delay ",
+ "00:00:00 ",
+ "00:00:30 ",
+ ),(
+ "Approval Duration ",
+ "00:00:01 ",
+ "1T02:03:05 ",
+ ),(
+ "Checker ",
+ "fping -q -- %(host)s ",
+ ": ",
+ ),(
+ "Extended Timeout ",
+ "00:15:00 ",
+ "00:15:00 ",
+ ),(
+ "Expires ",
+ "2019-02-04T00:00:00 ",
+ "2019-02-05T00:00:00 ",
+ ),(
+ "Last Checker Status",
+ "0 ",
+ "-2 ",
+ )
+ )
+ num_lines = max(len(rows) for rows in columns)
+ expected_output = ("\n".join("".join(rows[line]
+ for rows in columns)
+ for line in range(num_lines))
+ + "\n")
+ self.assertEqual(expected_output, buffer.getvalue())
+
+ def test_PrintTable_one_client(self):
+ with self.capture_stdout_to_buffer() as buffer:
+ command.PrintTable().run(self.bus.one_client)
+ expected_output = "\n".join((
+ "Name Enabled Timeout Last Successful Check",
+ "foo Yes 00:05:00 2019-02-03T00:00:00 ",
+ )) + "\n"
+ self.assertEqual(expected_output, buffer.getvalue())
+
+
+class TestPropertySetterCmd(TestCommand):
+ """Abstract class for tests of command.PropertySetter classes"""
+
+ def runTest(self):
+ if not hasattr(self, "command"):
+ return # Abstract TestCase class
+
+ if hasattr(self, "values_to_set"):
+ cmd_args = [(value,) for value in self.values_to_set]
+ values_to_get = getattr(self, "values_to_get",
+ self.values_to_set)
+ else:
+ cmd_args = [() for x in range(len(self.values_to_get))]
+ values_to_get = self.values_to_get
+ for value_to_get, cmd_arg in zip(values_to_get, cmd_args):
+ for clientpath in self.bus.clients:
+ self.bus.clients[clientpath][self.propname] = (
+ Unique())
+ self.command(*cmd_arg).run(self.bus.clients, self.bus)
+ for clientpath in self.bus.clients:
+ value = (self.bus.clients[clientpath]
+ [self.propname])
+ self.assertNotIsInstance(value, Unique)
+ self.assertEqual(value_to_get, value)
+
+
+class TestEnableCmd(TestPropertySetterCmd):
+ command = command.Enable
+ propname = "Enabled"
+ values_to_get = [True]
+
+
+class TestDisableCmd(TestPropertySetterCmd):
+ command = command.Disable
+ propname = "Enabled"
+ values_to_get = [False]
+
+
+class TestBumpTimeoutCmd(TestPropertySetterCmd):
+ command = command.BumpTimeout
+ propname = "LastCheckedOK"
+ values_to_get = [""]
+
+
+class TestStartCheckerCmd(TestPropertySetterCmd):
+ command = command.StartChecker
+ propname = "CheckerRunning"
+ values_to_get = [True]
+
+
+class TestStopCheckerCmd(TestPropertySetterCmd):
+ command = command.StopChecker
+ propname = "CheckerRunning"
+ values_to_get = [False]
+
+
+class TestApproveByDefaultCmd(TestPropertySetterCmd):
+ command = command.ApproveByDefault
+ propname = "ApprovedByDefault"
+ values_to_get = [True]
+
+
+class TestDenyByDefaultCmd(TestPropertySetterCmd):
+ command = command.DenyByDefault
+ propname = "ApprovedByDefault"
+ values_to_get = [False]
+
+
+class TestSetCheckerCmd(TestPropertySetterCmd):
+ command = command.SetChecker
+ propname = "Checker"
+ values_to_set = ["", ":", "fping -q -- %s"]
+
+
+class TestSetHostCmd(TestPropertySetterCmd):
+ command = command.SetHost
+ propname = "Host"
+ values_to_set = ["192.0.2.3", "client.example.org"]
+
+
+class TestSetSecretCmd(TestPropertySetterCmd):
+ command = command.SetSecret
+ propname = "Secret"
+ values_to_set = [io.BytesIO(b""),
+ io.BytesIO(b"secret\0xyzzy\nbar")]
+ values_to_get = [f.getvalue() for f in values_to_set]
+
+
+class TestSetTimeoutCmd(TestPropertySetterCmd):
+ command = command.SetTimeout
+ propname = "Timeout"
+ values_to_set = [datetime.timedelta(),
+ datetime.timedelta(minutes=5),
+ datetime.timedelta(seconds=1),
+ datetime.timedelta(weeks=1),
+ datetime.timedelta(weeks=52)]
+ values_to_get = [dt.total_seconds()*1000 for dt in values_to_set]
+
+
+class TestSetExtendedTimeoutCmd(TestPropertySetterCmd):
+ command = command.SetExtendedTimeout
+ propname = "ExtendedTimeout"
+ values_to_set = [datetime.timedelta(),
+ datetime.timedelta(minutes=5),
+ datetime.timedelta(seconds=1),
+ datetime.timedelta(weeks=1),
+ datetime.timedelta(weeks=52)]
+ values_to_get = [dt.total_seconds()*1000 for dt in values_to_set]
+
+
+class TestSetIntervalCmd(TestPropertySetterCmd):
+ command = command.SetInterval
+ propname = "Interval"
+ values_to_set = [datetime.timedelta(),
+ datetime.timedelta(minutes=5),
+ datetime.timedelta(seconds=1),
+ datetime.timedelta(weeks=1),
+ datetime.timedelta(weeks=52)]
+ values_to_get = [dt.total_seconds()*1000 for dt in values_to_set]
+
+
+class TestSetApprovalDelayCmd(TestPropertySetterCmd):
+ command = command.SetApprovalDelay
+ propname = "ApprovalDelay"
+ values_to_set = [datetime.timedelta(),
+ datetime.timedelta(minutes=5),
+ datetime.timedelta(seconds=1),
+ datetime.timedelta(weeks=1),
+ datetime.timedelta(weeks=52)]
+ values_to_get = [dt.total_seconds()*1000 for dt in values_to_set]
+
+
+class TestSetApprovalDurationCmd(TestPropertySetterCmd):
+ command = command.SetApprovalDuration
+ propname = "ApprovalDuration"
+ values_to_set = [datetime.timedelta(),
+ datetime.timedelta(minutes=5),
+ datetime.timedelta(seconds=1),
+ datetime.timedelta(weeks=1),
+ datetime.timedelta(weeks=52)]
+ values_to_get = [dt.total_seconds()*1000 for dt in values_to_set]
+
+
+
+def should_only_run_tests():
+ parser = argparse.ArgumentParser(add_help=False)
+ parser.add_argument("--check", action='store_true')
+ args, unknown_args = parser.parse_known_args()
+ run_tests = args.check
+ if run_tests:
+ # Remove --check argument from sys.argv
+ sys.argv[1:] = unknown_args
+ return run_tests
+
+# Add all tests from doctest strings
+def load_tests(loader, tests, none):
+ import doctest
+ tests.addTests(doctest.DocTestSuite())
+ return tests
+
+if __name__ == "__main__":
+ try:
+ if should_only_run_tests():
+ # Call using ./tdd-python-script --check [--verbose]
+ unittest.main()
+ else:
+ main()
+ finally:
+ logging.shutdown()
=== added file 'mandos-ctl.xml'
--- mandos-ctl.xml 1970-01-01 00:00:00 +0000
+++ mandos-ctl.xml 2019-07-29 16:35:53 +0000
@@ -0,0 +1,652 @@
+
+
+
+
+%common;
+]>
+
+
+
+ Mandos Manual
+
+ Mandos
+ &version;
+ &TIMESTAMP;
+
+
+ Björn
+ Påhlsson
+
+ belorn@recompile.se
+
+
+
+ Teddy
+ Hogeborn
+
+ teddy@recompile.se
+
+
+
+
+ 2010
+ 2011
+ 2012
+ 2013
+ 2014
+ 2015
+ 2016
+ 2017
+ 2018
+ 2019
+ Teddy Hogeborn
+ Björn Påhlsson
+
+
+
+
+
+ &COMMANDNAME;
+ 8
+
+
+
+ &COMMANDNAME;
+
+ Control or query the operation of the Mandos server
+
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+
+
+
+
+ CLIENT
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CLIENT
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CLIENT
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+ CLIENT
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+ DESCRIPTION
+
+ &COMMANDNAME; is a program to control or
+ query 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 extended_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.
+
+
+
+
+
+
+
+
+
+ Dump client settings as JSON to standard output.
+
+
+
+
+
+
+
+
+
+ Check if a single client is enabled or not, and exit with
+ a successful exit status only if the client is enabled.
+
+
+
+
+
+
+
+
+ Show debug output; currently, this means show D-Bus calls.
+
+
+
+
+
+
+
+
+ Run self-tests. This includes any unit tests, etc.
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+ BUGS
+
+
+
+
+ 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=PT5M --interval=PT1M foo1.example.org foo2.example.org
+
+
+
+
+
+
+
+ To approve all clients currently waiting for approval:
+
+
+ &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
+
+ intro
+ 8mandos,
+ mandos
+ 8,
+ mandos-clients.conf
+ 5,
+ mandos-monitor
+ 8
+
+
+
+
+
+
+
+
+
=== added file 'mandos-keygen'
--- mandos-keygen 1970-01-01 00:00:00 +0000
+++ mandos-keygen 2019-09-03 19:06:41 +0000
@@ -0,0 +1,451 @@
+#!/bin/sh -e
+#
+# Mandos key generator - create new keys for a Mandos client
+#
+# Copyright © 2008-2019 Teddy Hogeborn
+# Copyright © 2008-2019 Björn Påhlsson
+#
+# This file is part of Mandos.
+#
+# Mandos 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.
+#
+# Mandos 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 Mandos. If not, see .
+#
+# Contact the authors at .
+#
+
+VERSION="1.8.9"
+
+KEYDIR="/etc/keys/mandos"
+KEYTYPE=RSA
+KEYLENGTH=4096
+SUBKEYTYPE=RSA
+SUBKEYLENGTH=4096
+KEYNAME="`hostname --fqdn 2>/dev/null || hostname`"
+KEYEMAIL=""
+KEYCOMMENT=""
+KEYEXPIRE=0
+TLS_KEYTYPE=ed25519
+FORCE=no
+SSH=yes
+KEYCOMMENT_ORIG="$KEYCOMMENT"
+mode=keygen
+
+if [ ! -d "$KEYDIR" ]; then
+ KEYDIR="/etc/mandos/keys"
+fi
+
+# Parse options
+TEMP=`getopt --options vhpF:d:t:l:s:L:n:e:c:x:T:fS \
+ --longoptions version,help,password,passfile:,dir:,type:,length:,subtype:,sublength:,name:,email:,comment:,expire:,tls-keytype:,force,no-ssh \
+ --name "$0" -- "$@"`
+
+help(){
+basename="`basename "$0"`"
+cat <&2; exit 1;;
+ esac
+done
+if [ "$#" -gt 0 ]; then
+ echo "Unknown arguments: '$*'" >&2
+ exit 1
+fi
+
+SECKEYFILE="$KEYDIR/seckey.txt"
+PUBKEYFILE="$KEYDIR/pubkey.txt"
+TLS_PRIVKEYFILE="$KEYDIR/tls-privkey.pem"
+TLS_PUBKEYFILE="$KEYDIR/tls-pubkey.pem"
+
+# Check for some invalid values
+if [ ! -d "$KEYDIR" ]; then
+ echo "$KEYDIR not a directory" >&2
+ exit 1
+fi
+if [ ! -r "$KEYDIR" ]; then
+ echo "Directory $KEYDIR not readable" >&2
+ exit 1
+fi
+
+if [ "$mode" = keygen ]; then
+ if [ ! -w "$KEYDIR" ]; then
+ echo "Directory $KEYDIR not writeable" >&2
+ exit 1
+ fi
+ if [ -z "$KEYTYPE" ]; then
+ echo "Empty key type" >&2
+ exit 1
+ fi
+
+ if [ -z "$KEYNAME" ]; then
+ echo "Empty key name" >&2
+ exit 1
+ fi
+
+ if [ -z "$KEYLENGTH" ] || [ "$KEYLENGTH" -lt 512 ]; then
+ echo "Invalid key length" >&2
+ exit 1
+ fi
+
+ if [ -z "$KEYEXPIRE" ]; then
+ echo "Empty key expiration" >&2
+ exit 1
+ fi
+
+ # Make FORCE be 0 or 1
+ case "$FORCE" in
+ [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]) FORCE=1;;
+ [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) FORCE=0;;
+ esac
+
+ if { [ -e "$SECKEYFILE" ] || [ -e "$PUBKEYFILE" ] \
+ || [ -e "$TLS_PRIVKEYFILE" ] \
+ || [ -e "$TLS_PUBKEYFILE" ]; } \
+ && [ "$FORCE" -eq 0 ]; then
+ echo "Refusing to overwrite old key files; use --force" >&2
+ exit 1
+ fi
+
+ # Set lines for GnuPG batch file
+ if [ -n "$KEYCOMMENT" ]; then
+ KEYCOMMENTLINE="Name-Comment: $KEYCOMMENT"
+ fi
+ if [ -n "$KEYEMAIL" ]; then
+ KEYEMAILLINE="Name-Email: $KEYEMAIL"
+ fi
+
+ # Create temporary gpg batch file
+ BATCHFILE="`mktemp -t mandos-keygen-batch.XXXXXXXXXX`"
+ TLS_PRIVKEYTMP="`mktemp -t mandos-keygen-privkey.XXXXXXXXXX`"
+fi
+
+if [ "$mode" = password ]; then
+ # Create temporary encrypted password file
+ SECFILE="`mktemp -t mandos-keygen-secfile.XXXXXXXXXX`"
+fi
+
+# Create temporary key ring directory
+RINGDIR="`mktemp -d -t mandos-keygen-keyrings.XXXXXXXXXX`"
+
+# Remove temporary files on exit
+trap "
+set +e; \
+test -n \"$SECFILE\" && shred --remove \"$SECFILE\"; \
+test -n \"$TLS_PRIVKEYTMP\" && shred --remove \"$TLS_PRIVKEYTMP\"; \
+shred --remove \"$RINGDIR\"/sec* 2>/dev/null;
+test -n \"$BATCHFILE\" && rm --force \"$BATCHFILE\"; \
+rm --recursive --force \"$RINGDIR\";
+tty --quiet && stty echo; \
+" EXIT
+
+set -e
+
+umask 077
+
+if [ "$mode" = keygen ]; then
+ # Create batch file for GnuPG
+ cat >"$BATCHFILE" <<-EOF
+ Key-Type: $KEYTYPE
+ Key-Length: $KEYLENGTH
+ Key-Usage: sign,auth
+ Subkey-Type: $SUBKEYTYPE
+ Subkey-Length: $SUBKEYLENGTH
+ Subkey-Usage: encrypt
+ Name-Real: $KEYNAME
+ $KEYCOMMENTLINE
+ $KEYEMAILLINE
+ Expire-Date: $KEYEXPIRE
+ #Preferences:
+ #Handle:
+ #%pubring pubring.gpg
+ #%secring secring.gpg
+ %no-protection
+ %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 TLS private key
+ if certtool --generate-privkey --password='' \
+ --outfile "$TLS_PRIVKEYTMP" --sec-param ultra \
+ --key-type="$TLS_KEYTYPE" --pkcs8 --no-text 2>/dev/null; then
+
+ # Backup any old key files
+ if cp --backup=numbered --force "$TLS_PRIVKEYFILE" "$TLS_PRIVKEYFILE" \
+ 2>/dev/null; then
+ shred --remove "$TLS_PRIVKEYFILE" 2>/dev/null || :
+ fi
+ if cp --backup=numbered --force "$TLS_PUBKEYFILE" "$TLS_PUBKEYFILE" \
+ 2>/dev/null; then
+ rm --force "$TLS_PUBKEYFILE"
+ fi
+ cp --archive "$TLS_PRIVKEYTMP" "$TLS_PRIVKEYFILE"
+ shred --remove "$TLS_PRIVKEYTMP" 2>/dev/null || :
+
+ ## TLS public key
+
+ # First try certtool from GnuTLS
+ if ! certtool --password='' --load-privkey="$TLS_PRIVKEYFILE" \
+ --outfile="$TLS_PUBKEYFILE" --pubkey-info --no-text \
+ 2>/dev/null; then
+ # Otherwise try OpenSSL
+ if ! openssl pkey -in "$TLS_PRIVKEYFILE" \
+ -out "$TLS_PUBKEYFILE" -pubout; then
+ rm --force "$TLS_PUBKEYFILE"
+ # None of the commands succeded; give up
+ return 1
+ fi
+ fi
+ fi
+
+ # Make sure trustdb.gpg exists;
+ # this is a workaround for Debian bug #737128
+ gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
+ --homedir "$RINGDIR" \
+ --import-ownertrust < /dev/null
+ # Generate a new key in the key rings
+ gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
+ --homedir "$RINGDIR" --trust-model always \
+ --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
+ shred --remove "$SECKEYFILE" 2>/dev/null || :
+ fi
+ if cp --backup=numbered --force "$PUBKEYFILE" "$PUBKEYFILE" \
+ 2>/dev/null; then
+ rm --force "$PUBKEYFILE"
+ fi
+
+ FILECOMMENT="Mandos client key for $KEYNAME"
+ if [ "$KEYCOMMENT" != "$KEYCOMMENT_ORIG" ]; then
+ FILECOMMENT="$FILECOMMENT ($KEYCOMMENT)"
+ fi
+
+ if [ -n "$KEYEMAIL" ]; then
+ FILECOMMENT="$FILECOMMENT <$KEYEMAIL>"
+ fi
+
+ # Export key from key rings to key files
+ gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
+ --homedir "$RINGDIR" --armor --export-options export-minimal \
+ --comment "$FILECOMMENT" --output "$SECKEYFILE" \
+ --export-secret-keys
+ gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
+ --homedir "$RINGDIR" --armor --export-options export-minimal \
+ --comment "$FILECOMMENT" --output "$PUBKEYFILE" --export
+fi
+
+if [ "$mode" = password ]; then
+
+ # Make SSH be 0 or 1
+ case "$SSH" in
+ [Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]) SSH=1;;
+ [Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|*) SSH=0;;
+ esac
+
+ if [ $SSH -eq 1 ]; then
+ for ssh_keytype in ecdsa-sha2-nistp256 ed25519 rsa; do
+ set +e
+ ssh_fingerprint="`ssh-keyscan -t $ssh_keytype localhost 2>/dev/null`"
+ err=$?
+ set -e
+ if [ $err -ne 0 ]; then
+ ssh_fingerprint=""
+ continue
+ fi
+ if [ -n "$ssh_fingerprint" ]; then
+ ssh_fingerprint="${ssh_fingerprint#localhost }"
+ break
+ fi
+ done
+ fi
+
+ # Import key into temporary key rings
+ gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
+ --homedir "$RINGDIR" --trust-model always --armor \
+ --import "$SECKEYFILE"
+ gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
+ --homedir "$RINGDIR" --trust-model always --armor \
+ --import "$PUBKEYFILE"
+
+ # Get fingerprint of key
+ FINGERPRINT="`gpg --quiet --batch --no-tty --no-options \
+ --enable-dsa2 --homedir "$RINGDIR" --trust-model always \
+ --fingerprint --with-colons \
+ | sed --quiet \
+ --expression='/^fpr:/{s/^fpr:.*:\\([0-9A-Z]*\\):\$/\\1/p;q}'`"
+
+ test -n "$FINGERPRINT"
+
+ if [ -r "$TLS_PUBKEYFILE" ]; then
+ KEY_ID="$(certtool --key-id --hash=sha256 \
+ --infile="$TLS_PUBKEYFILE" 2>/dev/null || :)"
+
+ if [ -z "$KEY_ID" ]; then
+ KEY_ID=$(openssl pkey -pubin -in "$TLS_PUBKEYFILE" \
+ -outform der \
+ | openssl sha256 \
+ | sed --expression='s/^.*[^[:xdigit:]]//')
+ fi
+ test -n "$KEY_ID"
+ fi
+
+ FILECOMMENT="Encrypted password for a Mandos client"
+
+ while [ ! -s "$SECFILE" ]; do
+ if [ -n "$PASSFILE" ]; then
+ cat -- "$PASSFILE"
+ else
+ tty --quiet && stty -echo
+ echo -n "Enter passphrase: " >/dev/tty
+ read -r first
+ tty --quiet && echo >&2
+ echo -n "Repeat passphrase: " >/dev/tty
+ read -r second
+ if tty --quiet; then
+ echo >&2
+ stty echo
+ fi
+ if [ "$first" != "$second" ]; then
+ echo "Passphrase mismatch" >&2
+ touch "$RINGDIR"/mismatch
+ else
+ echo -n "$first"
+ fi
+ fi | gpg --quiet --batch --no-tty --no-options --enable-dsa2 \
+ --homedir "$RINGDIR" --trust-model always --armor \
+ --encrypt --sign --recipient "$FINGERPRINT" --comment \
+ "$FILECOMMENT" > "$SECFILE"
+ if [ -e "$RINGDIR"/mismatch ]; then
+ rm --force "$RINGDIR"/mismatch
+ if tty --quiet; then
+ > "$SECFILE"
+ else
+ exit 1
+ fi
+ fi
+ done
+
+ cat <<-EOF
+ [$KEYNAME]
+ host = $KEYNAME
+ EOF
+ if [ -n "$KEY_ID" ]; then
+ echo "key_id = $KEY_ID"
+ fi
+ cat <<-EOF
+ fingerprint = $FINGERPRINT
+ secret =
+ EOF
+ sed --quiet --expression='
+ /^-----BEGIN PGP MESSAGE-----$/,/^-----END PGP MESSAGE-----$/{
+ /^$/,${
+ # Remove 24-bit Radix-64 checksum
+ s/=....$//
+ # Indent four spaces
+ /^[^-]/s/^/ /p
+ }
+ }' < "$SECFILE"
+ if [ -n "$ssh_fingerprint" ]; then
+ echo 'checker = ssh-keyscan -t '"$ssh_keytype"' %%(host)s 2>/dev/null | grep --fixed-strings --line-regexp --quiet --regexp=%%(host)s" %(ssh_fingerprint)s"'
+ echo "ssh_fingerprint = ${ssh_fingerprint}"
+ fi
+fi
+
+trap - EXIT
+
+set +e
+# Remove the password file, if any
+if [ -n "$SECFILE" ]; then
+ shred --remove "$SECFILE" 2>/dev/null
+fi
+# Remove the key rings
+shred --remove "$RINGDIR"/sec* 2>/dev/null
+rm --recursive --force "$RINGDIR"
=== added file 'mandos-keygen.xml'
--- mandos-keygen.xml 1970-01-01 00:00:00 +0000
+++ mandos-keygen.xml 2019-07-18 00:02:43 +0000
@@ -0,0 +1,590 @@
+
+
+
+
+%common;
+]>
+
+
+
+ Mandos Manual
+
+ Mandos
+ &version;
+ &TIMESTAMP;
+
+
+ Björn
+ Påhlsson
+
+ belorn@recompile.se
+
+
+
+ Teddy
+ Hogeborn
+
+ teddy@recompile.se
+
+
+
+
+ 2008
+ 2009
+ 2010
+ 2011
+ 2012
+ 2013
+ 2014
+ 2015
+ 2016
+ 2017
+ 2018
+ 2019
+ Teddy Hogeborn
+ Björn Påhlsson
+
+
+
+
+
+ &COMMANDNAME;
+ 8
+
+
+
+ &COMMANDNAME;
+
+ Generate key and password for Mandos client and server.
+
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+ FILE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+ &COMMANDNAME;
+
+
+
+
+
+
+
+
+ DESCRIPTION
+
+ &COMMANDNAME; is a program to generate the
+ TLS and OpenPGP keys used by
+ mandos-client
+ 8mandos. The keys are
+ normally written to /etc/keys/mandos for later installation into
+ the initrd image, but this, and most other things, can be
+ changed with command line options.
+
+
+ This program can also be used with the
+ or
+ options to generate a ready-made section for
+ clients.conf (see
+ mandos-clients.conf
+ 5).
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+ Target directory for key files. Default is /etc/keys/mandos.
+
+
+
+
+
+
+
+
+
+ OpenPGP key type. Default is RSA
.
+
+
+
+
+
+
+
+
+
+ OpenPGP key length in bits. Default is 4096.
+
+
+
+
+
+
+
+
+
+ OpenPGP subkey type. Default is RSA
+
+
+
+
+
+
+
+
+
+ OpenPGP subkey length in bits. Default is 4096.
+
+
+
+
+
+
+
+
+
+ Email address of key. Default is empty.
+
+
+
+
+
+
+
+
+
+ Comment field for key. Default is empty.
+
+
+
+
+
+
+
+
+
+ Key expire time. Default is no expiration. See
+ gpg
+ 1 for syntax.
+
+
+
+
+
+
+
+
+
+ TLS key type. Default is ed25519
+
+
+
+
+
+
+
+
+
+ Force overwriting old key.
+
+
+
+
+
+
+
+
+ Prompt for a password and encrypt it with the key already
+ present in either /etc/keys/mandos or
+ the directory specified with the
+ option. Outputs, on standard output, a section suitable
+ for inclusion in mandos-clients.conf8. The host name or the name
+ specified with the option is used
+ for the section header. All other options are ignored,
+ and no key is created. Note: white space is stripped from
+ the beginning and from the end of the password; See .
+
+
+
+
+
+
+
+
+ The same as , but read from
+ FILE, not the terminal, and
+ white space is not stripped from the password in any way.
+
+
+
+
+
+
+
+
+ When or
+ is given, this option will
+ prevent &COMMANDNAME; from calling
+ ssh-keyscan to get an SSH fingerprint
+ for this host and, if successful, output suitable config
+ options to use this fingerprint as a
+ option in the output. This is
+ otherwise the default behavior.
+
+
+
+
+
+
+
+ OVERVIEW
+
+
+ This program is a small utility to generate new TLS and OpenPGP
+ keys for new Mandos clients, and to generate sections for
+ inclusion in clients.conf on the server.
+
+
+
+
+ EXIT STATUS
+
+ The exit status will be 0 if a new key (or password, if the
+ option was used) was successfully
+ created, otherwise not.
+
+
+
+
+ ENVIRONMENT
+
+
+ TMPDIR
+
+
+ If set, temporary files will be created here. See
+ mktemp
+ 1.
+
+
+
+
+
+
+
+ FILES
+
+ Use the option to change where
+ &COMMANDNAME; will write the key files. The
+ default file names are shown here.
+
+
+
+ /etc/keys/mandos/seckey.txt
+
+
+ OpenPGP secret key file which will be created or
+ overwritten.
+
+
+
+
+ /etc/keys/mandos/pubkey.txt
+
+
+ OpenPGP public key file which will be created or
+ overwritten.
+
+
+
+
+ /etc/keys/mandos/tls-privkey.pem
+
+
+ Private key file which will be created or overwritten.
+
+
+
+
+ /etc/keys/mandos/tls-pubkey.pem
+
+
+ Public key file which will be created or overwritten.
+
+
+
+
+ /tmp
+
+
+ Temporary files will be written here if
+ TMPDIR is not set.
+
+
+
+
+
+
+
+ BUGS
+
+ The / option
+ strips white space from the start and from the end of the
+ password before using it. If this is a problem, use the
+ option instead, which does not do
+ this.
+
+
+
+
+
+ EXAMPLE
+
+
+ Normal invocation needs no options:
+
+
+ &COMMANDNAME;
+
+
+
+
+ Create key in another directory and of another type. Force
+ overwriting old key files:
+
+
+
+
+&COMMANDNAME; --dir ~/keydir --type RSA --force
+
+
+
+
+
+ Prompt for a password, encrypt it with the keys in /etc/keys/mandos and output a
+ section suitable for clients.conf.
+
+
+ &COMMANDNAME; --password
+
+
+
+
+ Prompt for a password, encrypt it with the keys in the
+ client-key directory and output a section
+ suitable for clients.conf.
+
+
+
+
+&COMMANDNAME; --password --dir client-key
+
+
+
+
+
+
+ SECURITY
+
+ The , ,
+ , and
+ options can be used to create keys of low security. If in
+ doubt, leave them to the default values.
+
+
+ The key expire time is not guaranteed to be
+ honored by mandos
+ 8.
+
+
+
+
+ SEE ALSO
+
+ intro
+ 8mandos,
+ gpg
+ 1,
+ mandos-clients.conf
+ 5,
+ mandos
+ 8,
+ mandos-client
+ 8mandos,
+ ssh-keyscan
+ 1
+
+
+
+
+
+
+
+
+
=== added file 'mandos-monitor'
--- mandos-monitor 1970-01-01 00:00:00 +0000
+++ mandos-monitor 2019-11-03 19:09:41 +0000
@@ -0,0 +1,757 @@
+#!/usr/bin/python3 -bbI
+# -*- mode: python; coding: utf-8 -*-
+#
+# Mandos Monitor - Control and monitor the Mandos server
+#
+# Copyright © 2009-2019 Teddy Hogeborn
+# Copyright © 2009-2019 Björn Påhlsson
+#
+# This file is part of Mandos.
+#
+# Mandos 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.
+#
+# Mandos 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 Mandos. If not, see .
+#
+# Contact the authors at .
+#
+
+from __future__ import (division, absolute_import, print_function,
+ unicode_literals)
+try:
+ from future_builtins import *
+except ImportError:
+ pass
+
+import sys
+import os
+import warnings
+import datetime
+import locale
+import logging
+
+import urwid.curses_display
+import urwid
+
+from dbus.mainloop.glib import DBusGMainLoop
+from gi.repository import GLib
+
+import dbus
+
+if sys.version_info.major == 2:
+ str = unicode
+
+log = logging.getLogger(os.path.basename(sys.argv[0]))
+logging.basicConfig(level="NOTSET", # Show all messages
+ format="%(message)s") # Show basic log messages
+
+logging.captureWarnings(True) # Show warnings via the logging system
+
+locale.setlocale(locale.LC_ALL, "")
+
+logging.getLogger("dbus.proxies").setLevel(logging.CRITICAL)
+
+# Some useful constants
+domain = "se.recompile"
+server_interface = domain + ".Mandos"
+client_interface = domain + ".Mandos.Client"
+version = "1.8.9"
+
+try:
+ dbus.OBJECT_MANAGER_IFACE
+except AttributeError:
+ dbus.OBJECT_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
+
+
+def isoformat_to_datetime(iso):
+ "Parse an ISO 8601 date string to a datetime.datetime()"
+ if not iso:
+ return None
+ d, t = iso.split("T", 1)
+ year, month, day = d.split("-", 2)
+ hour, minute, second = t.split(":", 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, properties=None, **kwargs):
+ self.proxy = proxy_object # Mandos Client proxy object
+ self.properties = dict() if properties is None else properties
+ self.property_changed_match = (
+ self.proxy.connect_to_signal("PropertiesChanged",
+ self.properties_changed,
+ dbus.PROPERTIES_IFACE,
+ byte_arrays=True))
+
+ if properties is None:
+ self.properties.update(self.proxy.GetAll(
+ client_interface,
+ dbus_interface=dbus.PROPERTIES_IFACE))
+
+ super(MandosClientPropertyCache, self).__init__(**kwargs)
+
+ def properties_changed(self, interface, properties, invalidated):
+ """This is called whenever we get a PropertiesChanged signal
+ It updates the changed properties in the "properties" dict.
+ """
+ # Update properties dict with new value
+ if interface == client_interface:
+ self.properties.update(properties)
+
+ def delete(self):
+ self.property_changed_match.remove()
+
+
+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, **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
+
+ self._update_timer_callback_tag = None
+
+ # The widget shown normally
+ self._text_widget = urwid.Text("")
+ # The widget shown when we have focus
+ self._focus_text_widget = urwid.Text("")
+ super(MandosClientWidget, self).__init__(**kwargs)
+ self.update()
+ self.opened = False
+
+ self.match_objects = (
+ self.proxy.connect_to_signal("CheckerCompleted",
+ self.checker_completed,
+ client_interface,
+ byte_arrays=True),
+ self.proxy.connect_to_signal("CheckerStarted",
+ self.checker_started,
+ client_interface,
+ byte_arrays=True),
+ self.proxy.connect_to_signal("GotSecret",
+ self.got_secret,
+ client_interface,
+ byte_arrays=True),
+ self.proxy.connect_to_signal("NeedApproval",
+ self.need_approval,
+ client_interface,
+ byte_arrays=True),
+ self.proxy.connect_to_signal("Rejected",
+ self.rejected,
+ client_interface,
+ byte_arrays=True))
+ log.debug("Created client %s", self.properties["Name"])
+
+ def using_timer(self, flag):
+ """Call this method with True or False when timer should be
+ activated or deactivated.
+ """
+ if flag and self._update_timer_callback_tag is None:
+ # Will update the shown timer value every second
+ self._update_timer_callback_tag = (
+ GLib.timeout_add(1000,
+ glib_safely(self.update_timer)))
+ elif not (flag or self._update_timer_callback_tag is None):
+ GLib.source_remove(self._update_timer_callback_tag)
+ self._update_timer_callback_tag = None
+
+ def checker_completed(self, exitstatus, condition, command):
+ if exitstatus == 0:
+ log.debug('Checker for client %s (command "%s")'
+ " succeeded", self.properties["Name"], command)
+ self.update()
+ return
+ # Checker failed
+ if os.WIFEXITED(condition):
+ log.info('Checker for client %s (command "%s") failed'
+ " with exit code %d", self.properties["Name"],
+ command, os.WEXITSTATUS(condition))
+ elif os.WIFSIGNALED(condition):
+ log.info('Checker for client %s (command "%s") was'
+ " killed by signal %d", self.properties["Name"],
+ command, os.WTERMSIG(condition))
+ self.update()
+
+ def checker_started(self, command):
+ """Server signals that a checker started."""
+ log.debug('Client %s started checker "%s"',
+ self.properties["Name"], command)
+
+ def got_secret(self):
+ log.info("Client %s received its secret",
+ self.properties["Name"])
+
+ def need_approval(self, timeout, default):
+ if not default:
+ message = "Client %s needs approval within %f seconds"
+ else:
+ message = "Client %s will get its secret in %f seconds"
+ log.info(message, self.properties["Name"], timeout/1000)
+
+ def rejected(self, reason):
+ log.info("Client %s was rejected; reason: %s",
+ self.properties["Name"], reason)
+
+ def selectable(self):
+ """Make this a "selectable" widget.
+ This overrides the method from urwid.FlowWidget."""
+ return True
+
+ def rows(self, maxcolrow, 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(maxcolrow, 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 = {"normal": "standout",
+ "bold": "bold-standout",
+ "underline-blink":
+ "underline-blink-standout",
+ "bold-underline-blink":
+ "bold-underline-blink-standout",
+ }
+
+ # Rebuild focus and non-focus widgets using current properties
+
+ # Base part of a client. Name!
+ base = "{name}: ".format(name=self.properties["Name"])
+ if not self.properties["Enabled"]:
+ message = "DISABLED"
+ self.using_timer(False)
+ elif self.properties["ApprovalPending"]:
+ timeout = datetime.timedelta(
+ milliseconds=self.properties["ApprovalDelay"])
+ last_approval_request = isoformat_to_datetime(
+ self.properties["LastApprovalRequest"])
+ if last_approval_request is not None:
+ timer = max(timeout - (datetime.datetime.utcnow()
+ - last_approval_request),
+ datetime.timedelta())
+ else:
+ timer = datetime.timedelta()
+ if self.properties["ApprovedByDefault"]:
+ message = "Approval in {}. (d)eny?"
+ else:
+ message = "Denial in {}. (a)pprove?"
+ message = message.format(str(timer).rsplit(".", 1)[0])
+ self.using_timer(True)
+ elif self.properties["LastCheckerStatus"] != 0:
+ # When checker has failed, show timer until client expires
+ expires = self.properties["Expires"]
+ if expires == "":
+ timer = datetime.timedelta(0)
+ else:
+ expires = (datetime.datetime.strptime
+ (expires, "%Y-%m-%dT%H:%M:%S.%f"))
+ timer = max(expires - datetime.datetime.utcnow(),
+ datetime.timedelta())
+ message = ("A checker has failed! Time until client"
+ " gets disabled: {}"
+ .format(str(timer).rsplit(".", 1)[0]))
+ self.using_timer(True)
+ else:
+ message = "enabled"
+ self.using_timer(False)
+ self._text = "{}{}".format(base, message)
+
+ if not urwid.supports_unicode():
+ self._text = self._text.encode("ascii", "replace")
+ textlist = [("normal", self._text)]
+ 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 GLib. Will indefinitely loop until
+ GLib.source_remove() on tag is called
+ """
+ self.update()
+ return True # Keep calling this
+
+ def delete(self, **kwargs):
+ if self._update_timer_callback_tag is not None:
+ GLib.source_remove(self._update_timer_callback_tag)
+ self._update_timer_callback_tag = None
+ for match in self.match_objects:
+ match.remove()
+ self.match_objects = ()
+ if self.delete_hook is not None:
+ self.delete_hook(self)
+ return super(MandosClientWidget, self).delete(**kwargs)
+
+ def render(self, maxcolrow, focus=False):
+ """Render differently if we have focus.
+ This overrides the method from urwid.FlowWidget"""
+ return self.current_widget(focus).render(maxcolrow,
+ focus=focus)
+
+ def keypress(self, maxcolrow, key):
+ """Handle keys.
+ This overrides the method from urwid.FlowWidget"""
+ if key == "+":
+ self.proxy.Set(client_interface, "Enabled",
+ dbus.Boolean(True), ignore_reply=True,
+ dbus_interface=dbus.PROPERTIES_IFACE)
+ elif key == "-":
+ self.proxy.Set(client_interface, "Enabled", False,
+ ignore_reply=True,
+ dbus_interface=dbus.PROPERTIES_IFACE)
+ elif key == "a":
+ self.proxy.Approve(dbus.Boolean(True, variant_level=1),
+ dbus_interface=client_interface,
+ ignore_reply=True)
+ elif key == "d":
+ self.proxy.Approve(dbus.Boolean(False, variant_level=1),
+ dbus_interface=client_interface,
+ ignore_reply=True)
+ elif key == "R" or key == "_" or key == "ctrl k":
+ self.server_proxy_object.RemoveClient(self.proxy
+ .object_path,
+ ignore_reply=True)
+ elif key == "s":
+ self.proxy.Set(client_interface, "CheckerRunning",
+ dbus.Boolean(True), ignore_reply=True,
+ dbus_interface=dbus.PROPERTIES_IFACE)
+ elif key == "S":
+ self.proxy.Set(client_interface, "CheckerRunning",
+ dbus.Boolean(False), ignore_reply=True,
+ dbus_interface=dbus.PROPERTIES_IFACE)
+ elif key == "C":
+ self.proxy.CheckedOK(dbus_interface=client_interface,
+ ignore_reply=True)
+ # xxx
+# elif key == "p" or key == "=":
+# self.proxy.pause()
+# elif key == "u" or key == ":":
+# self.proxy.unpause()
+# elif key == "RET":
+# self.open()
+ else:
+ return key
+
+ def properties_changed(self, interface, properties, invalidated):
+ """Call self.update() if any properties changed.
+ This overrides the method from MandosClientPropertyCache"""
+ old_values = {key: self.properties.get(key)
+ for key in properties.keys()}
+ super(MandosClientWidget, self).properties_changed(
+ interface, properties, invalidated)
+ if any(old_values[key] != self.properties.get(key)
+ for key in old_values):
+ self.update()
+
+
+def glib_safely(func, retval=True):
+ def safe_func(*args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except Exception:
+ log.exception("")
+ return retval
+ return safe_func
+
+
+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, *args, **kwargs):
+ ret = (super(ConstrainedListBox, self)
+ .keypress(*args, **kwargs))
+ if ret in ("up", "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((
+ ("normal",
+ "default", "default", None),
+ ("bold",
+ "bold", "default", "bold"),
+ ("underline-blink",
+ "underline,blink", "default", "underline,blink"),
+ ("standout",
+ "standout", "default", "standout"),
+ ("bold-underline-blink",
+ "bold,underline,blink", "default",
+ "bold,underline,blink"),
+ ("bold-standout",
+ "bold,standout", "default", "bold,standout"),
+ ("underline-blink-standout",
+ "underline,blink,standout", "default",
+ "underline,blink,standout"),
+ ("bold-underline-blink-standout",
+ "bold,underline,blink,standout", "default",
+ "bold,underline,blink,standout"),
+ ))
+
+ if urwid.supports_unicode():
+ self.divider = "─" # \u2500
+ else:
+ self.divider = "_" # \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 = urwid.SimpleListWalker([])
+ 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 = "any"
+
+ self.loghandler = UILogHandler(self)
+
+ self.rebuild()
+ self.add_log_line(("bold",
+ "Mandos Monitor version " + version))
+ self.add_log_line(("bold", "q: Quit ?: Help"))
+
+ self.busname = domain + ".Mandos"
+ self.main_loop = GLib.MainLoop()
+
+ def client_not_found(self, key_id, address):
+ log.info("Client with address %s and key ID %s could"
+ " not be found", address, key_id)
+
+ 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)
+ self.topwidget = urwid.Pile(self.uilist)
+
+ def add_log_line(self, markup):
+ self.log.append(urwid.Text(markup, wrap=self.log_wrap))
+ if self.max_log_length:
+ if len(self.log) > self.max_log_length:
+ del self.log[0:(len(self.log) - self.max_log_length)]
+ self.logbox.set_focus(len(self.logbox.body.contents)-1,
+ coming_from="above")
+ self.refresh()
+
+ def toggle_log_display(self):
+ """Toggle visibility of the log buffer."""
+ self.log_visible = not self.log_visible
+ self.rebuild()
+ log.debug("Log visibility changed to: %s", self.log_visible)
+
+ def change_log_display(self):
+ """Change type of log display.
+ Currently, this toggles wrapping of text lines."""
+ if self.log_wrap == "clip":
+ self.log_wrap = "any"
+ else:
+ self.log_wrap = "clip"
+ for textwidget in self.log:
+ textwidget.set_wrap_mode(self.log_wrap)
+ log.debug("Wrap mode: %s", self.log_wrap)
+
+ def find_and_remove_client(self, path, interfaces):
+ """Find a client by its object path and remove it.
+
+ This is connected to the InterfacesRemoved signal from the
+ Mandos server object."""
+ if client_interface not in interfaces:
+ # Not a Mandos client object; ignore
+ return
+ try:
+ client = self.clients_dict[path]
+ except KeyError:
+ # not found?
+ log.warning("Unknown client %s removed", path)
+ return
+ client.delete()
+
+ def add_new_client(self, path, ifs_and_props):
+ """Find a client by its object path and remove it.
+
+ This is connected to the InterfacesAdded signal from the
+ Mandos server object.
+ """
+ if client_interface not in ifs_and_props:
+ # Not a Mandos client object; ignore
+ return
+ client_proxy_object = self.bus.get_object(self.busname, path)
+ self.add_client(MandosClientWidget(
+ server_proxy_object=self.mandos_serv,
+ proxy_object=client_proxy_object,
+ update_hook=self.refresh,
+ delete_hook=self.remove_client,
+ properties=dict(ifs_and_props[client_interface])),
+ 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(key=lambda c: c.properties["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]
+ 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."""
+ log.addHandler(self.loghandler)
+ self.orig_log_propagate = log.propagate
+ log.propagate = False
+ self.orig_log_level = log.level
+ log.setLevel("INFO")
+ self.bus = dbus.SystemBus()
+ mandos_dbus_objc = self.bus.get_object(
+ self.busname, "/", follow_name_owner_changes=True)
+ self.mandos_serv = dbus.Interface(
+ mandos_dbus_objc, dbus_interface=server_interface)
+ try:
+ mandos_clients = (self.mandos_serv
+ .GetAllClientsWithProperties())
+ if not mandos_clients:
+ log.warning("Note: Server has no clients.")
+ except dbus.exceptions.DBusException:
+ log.warning("Note: No Mandos server running.")
+ mandos_clients = dbus.Dictionary()
+
+ (self.mandos_serv
+ .connect_to_signal("InterfacesRemoved",
+ self.find_and_remove_client,
+ dbus_interface=dbus.OBJECT_MANAGER_IFACE,
+ byte_arrays=True))
+ (self.mandos_serv
+ .connect_to_signal("InterfacesAdded",
+ self.add_new_client,
+ dbus_interface=dbus.OBJECT_MANAGER_IFACE,
+ byte_arrays=True))
+ (self.mandos_serv
+ .connect_to_signal("ClientNotFound",
+ self.client_not_found,
+ dbus_interface=server_interface,
+ byte_arrays=True))
+ for path, client in mandos_clients.items():
+ client_proxy_object = self.bus.get_object(self.busname,
+ path)
+ self.add_client(MandosClientWidget(
+ server_proxy_object=self.mandos_serv,
+ proxy_object=client_proxy_object,
+ properties=client,
+ update_hook=self.refresh,
+ delete_hook=self.remove_client),
+ path=path)
+
+ self.refresh()
+ self._input_callback_tag = (
+ GLib.io_add_watch(
+ GLib.IOChannel.unix_new(sys.stdin.fileno()),
+ GLib.PRIORITY_DEFAULT, GLib.IO_IN,
+ glib_safely(self.process_input)))
+ self.main_loop.run()
+ # Main loop has finished, we should close everything now
+ GLib.source_remove(self._input_callback_tag)
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", BytesWarning)
+ self.screen.stop()
+
+ def stop(self):
+ self.main_loop.quit()
+ log.removeHandler(self.loghandler)
+ log.propagate = self.orig_log_propagate
+
+ def process_input(self, source, condition):
+ keys = self.screen.get_input()
+ translations = {"ctrl n": "down", # Emacs
+ "ctrl p": "up", # Emacs
+ "ctrl v": "page down", # Emacs
+ "meta v": "page up", # Emacs
+ " ": "page down", # less
+ "f": "page down", # less
+ "b": "page up", # less
+ "j": "down", # vi
+ "k": "up", # vi
+ }
+ for key in keys:
+ try:
+ key = translations[key]
+ except KeyError: # :-)
+ pass
+
+ if key == "q" or key == "Q":
+ self.stop()
+ break
+ elif key == "window resize":
+ self.size = self.screen.get_cols_rows()
+ self.refresh()
+ elif key == "ctrl l":
+ self.screen.clear()
+ self.refresh()
+ elif key == "l" or key == "D":
+ self.toggle_log_display()
+ self.refresh()
+ elif key == "w" or key == "i":
+ self.change_log_display()
+ self.refresh()
+ elif key == "?" or key == "f1" or key == "esc":
+ if not self.log_visible:
+ self.log_visible = True
+ self.rebuild()
+ self.add_log_line(("bold",
+ " ".join(("q: Quit",
+ "?: Help",
+ "l: Log window toggle",
+ "TAB: Switch window",
+ "w: Wrap (log lines)",
+ "v: Toggle verbose log",
+ ))))
+ self.add_log_line(("bold",
+ " ".join(("Clients:",
+ "+: Enable",
+ "-: Disable",
+ "R: Remove",
+ "s: Start new checker",
+ "S: Stop checker",
+ "C: Checker OK",
+ "a: Approve",
+ "d: Deny",
+ ))))
+ self.refresh()
+ elif key == "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 == "v":
+ if log.level < logging.INFO:
+ log.setLevel(logging.INFO)
+ log.info("Verbose mode: Off")
+ else:
+ log.setLevel(logging.NOTSET)
+ log.info("Verbose mode: On")
+ # elif (key == "end" or key == "meta >" or key == "G"
+ # or key == ">"):
+ # pass # xxx end-of-buffer
+ # elif (key == "home" or key == "meta <" or key == "g"
+ # or key == "<"):
+ # pass # xxx beginning-of-buffer
+ # elif key == "ctrl e" or key == "$":
+ # pass # xxx move-end-of-line
+ # elif key == "ctrl a" or key == "^":
+ # pass # xxx move-beginning-of-line
+ # elif key == "ctrl b" or key == "meta (" or key == "h":
+ # pass # xxx left
+ # elif key == "ctrl f" or key == "meta )" or key == "l":
+ # pass # xxx right
+ # elif key == "a":
+ # pass # scroll up log
+ # elif key == "z":
+ # pass # scroll down log
+ elif self.topwidget.selectable():
+ self.topwidget.keypress(self.size, key)
+ self.refresh()
+ return True
+
+
+class UILogHandler(logging.Handler):
+ def __init__(self, ui, *args, **kwargs):
+ self.ui = ui
+ super(UILogHandler, self).__init__(*args, **kwargs)
+ self.setFormatter(
+ logging.Formatter("%(asctime)s: %(message)s"))
+ def emit(self, record):
+ msg = self.format(record)
+ if record.levelno > logging.INFO:
+ msg = ("bold", msg)
+ self.ui.add_log_line(msg)
+
+
+ui = UserInterface()
+try:
+ ui.run()
+except KeyboardInterrupt:
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", "", BytesWarning)
+ ui.screen.stop()
+except Exception:
+ with warnings.catch_warnings():
+ warnings.filterwarnings("ignore", "", BytesWarning)
+ ui.screen.stop()
+ raise
=== added file 'mandos-monitor.xml'
--- mandos-monitor.xml 1970-01-01 00:00:00 +0000
+++ mandos-monitor.xml 2019-02-10 04:20:26 +0000
@@ -0,0 +1,249 @@
+
+
+
+
+%common;
+]>
+
+
+
+ Mandos Manual
+
+ Mandos
+ &version;
+ &TIMESTAMP;
+
+
+ Björn
+ Påhlsson
+
+ belorn@recompile.se
+
+
+
+ Teddy
+ Hogeborn
+
+ teddy@recompile.se
+
+
+
+
+ 2010
+ 2011
+ 2012
+ 2013
+ 2014
+ 2015
+ 2016
+ 2017
+ 2018
+ 2019
+ 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
+
+
+ v
+ Toggle verbose logging
+
+
+ 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 bus name of
+ se.recompile.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
+
+ intro
+ 8mandos,
+ mandos
+ 8,
+ mandos-ctl
+ 8
+
+
+
+
+
+
+
+
+
=== added file 'mandos-options.xml'
--- mandos-options.xml 1970-01-01 00:00:00 +0000
+++ mandos-options.xml 2019-07-25 21:42:40 +0000
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+ If this is specified, the server will only announce the service
+ and listen to requests on the specified network interface.
+ Default is to use all available interfaces. Note: a failure to bind to the specified
+ interface is not considered critical, and the server will not
+ exit, but instead continue normally.
+
+
+
+ If this option is used, the server will only listen to the
+ specified IPv6 address. If a link-local address is specified, an
+ interface should be set, since a link-local address is only valid
+ on a single interface. By default, the server will listen to all
+ available addresses. If set, this must normally be an IPv6
+ address; an IPv4 address can only be specified using IPv4-mapped
+ IPv6 address syntax: ::FFFF:192.0.2.3
. (Only if IPv6 usage is
+ disabled (see below) must this be an IPv4
+ address.)
+
+
+
+ If this option is used, the server will bind to that port. By
+ default, the server will listen to an arbitrary port given by the
+ operating system.
+
+
+
+ If the server is run in debug mode, it will run in the foreground
+ and print a lot of debugging information. The default is to
+ not run in debug mode.
+
+
+
+ GnuTLS priority string for the TLS handshake.
+ The default is
+
+ SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA:!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA
+ when using raw public keys in TLS, and
+ SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA:+SIGN-DSA-SHA256
+ when using OpenPGP keys in TLS,. See gnutls_priority_init
+ 3 for the syntax.
+ Warning: changing this may make the
+ TLS handshake fail, making server-client
+ communication impossible. Changing this option may also make the
+ network traffic decryptable by an attacker.
+
+
+
+ Zeroconf service name. The default is
+ Mandos
. This only needs to be
+ changed if for some reason is would be necessary to run more than
+ one server on the same host. This would not
+ normally be useful. If there are name collisions on the same
+ network, the newer server will automatically
+ rename itself to Mandos #2
, and
+ so on; therefore, this option is not needed in that case.
+
+
+
+ This option controls whether the server will provide a D-Bus
+ system bus interface. The default is to provide such an
+ interface.
+
+
+
+ This option controls whether the server will use IPv6 sockets and
+ addresses. The default is to use IPv6. This option should
+ never normally be turned off, even in
+ IPv4-only environments. This is because
+ mandos-client
+ 8mandos will normally use
+ IPv6 link-local addresses, and will not be able to find or connect
+ to the server if this option is turned off. Only
+ advanced users should consider changing this option.
+
+
+
+ This option controls whether the server will restore its state
+ from the last time it ran. Default is to restore last state.
+
+
+
+ Directory to save (and restore) state in. Default is
+ /var/lib/mandos
.
+
+
+
+ If this option is used, the server will not create a new network
+ socket, but will instead use the supplied file descriptor. By
+ default, the server will create a new network socket.
+
+
+
+ This option will make the server run in the foreground and not
+ write a PID file. The default is to not run
+ in the foreground, except in mode, which
+ implies this option.
+
+
+
+ This option controls whether the server will announce its
+ existence using Zeroconf. Default is to use Zeroconf. If
+ Zeroconf is not used, a number or a
+ is required.
+
+
+
=== added file 'mandos-to-cryptroot-unlock'
--- mandos-to-cryptroot-unlock 1970-01-01 00:00:00 +0000
+++ mandos-to-cryptroot-unlock 2019-07-24 11:02:24 +0000
@@ -0,0 +1,82 @@
+#!/bin/sh
+#
+# Script to get password from plugin-runner to cryptroot-unlock
+#
+# Copyright © 2018 Teddy Hogeborn
+# Copyright © 2018 Björn Påhlsson
+#
+# This file is part of Mandos.
+#
+# Mandos 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.
+#
+# Mandos 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 Mandos. If not, see .
+#
+# Contact the authors at .
+
+# This script is made to run in the initramfs, and must not be run in
+# the normal system environment.
+
+# Temporary file for the password
+passfile=$(mktemp -p /run -t mandos.XXXXXX)
+trap "rm -f -- $passfile 2>/dev/null" EXIT
+
+# Disable the plugins which conflict with "askpass" as distributed by
+# cryptsetup.
+cat <<-EOF >>/conf/conf.d/mandos/plugin-runner.conf
+
+ --disable=askpass-fifo
+ --disable=password-prompt
+ --disable=plymouth
+EOF
+
+# In case a password is retrieved by other means than by plugin-runner
+# (such as typing it on the console into the prompt given by the
+# "askpass" program), this dummy plugin will be made to exit
+# successfully, thereby forcing plugin-runner to stop all its plugins
+# and also exit itself.
+cat <<-EOF > /lib/mandos/plugins.d/dummy
+ #!/bin/sh
+
+ while [ -e /run/mandos-keep-running ]; do
+ sleep 1
+ done
+
+ # exit successfully to force plugin-runner to finish
+ exit 0
+EOF
+chmod u=rwx,go=rx /lib/mandos/plugins.d/dummy
+
+# This file is the flag which keeps the dummy plugin running
+touch /run/mandos-keep-running
+
+# Keep running plugin-runner and trying any password, until either a
+# password is accepted by cryptroot-unlock, or plugin-runner fails, or
+# the file /run/mandos-keep-running has been removed.
+while command -v cryptroot-unlock >/dev/null 2>&1; do
+ /lib/mandos/plugin-runner > "$passfile" &
+ echo $! > /run/mandos-plugin-runner.pid
+ wait %% || break
+
+ # Try this password ten times (or ten seconds)
+ for loop in 1 2 3 4 5 6 7 8 9 10; do
+ if [ -e /run/mandos-keep-running ]; then
+ cryptroot-unlock < "$passfile" >/dev/null 2>&1 && break 2
+ sleep 1
+ else
+ break 2
+ fi
+ done
+done
+
+exec >/dev/null 2>&1
+
+rm -f /run/mandos-plugin-runner.pid /run/mandos-keep-running
=== modified file 'mandos.conf'
--- mandos.conf 2008-08-09 01:39:09 +0000
+++ mandos.conf 2015-07-20 03:03:33 +0000
@@ -1,38 +1,53 @@
-# This file must have exactly one section named "server".
-[server]
+# This file must have exactly one section named "DEFAULT".
+[DEFAULT]
# These are the default values for the server, uncomment and change
# them if needed.
-
# If "interface" is set, the server will only listen to a specific
# network interface.
;interface =
-
# If "address" is set, the server will only listen to a specific
# address. This must currently be an IPv6 address; an IPv4 address
# can be specified using the "::FFFF:192.0.2.3" syntax. Also, if this
# is a link-local address, an interface should be set above.
;address =
-
# If "port" is set, the server to bind to that port. By default, the
# server will listen to an arbitrary port.
;port =
-
# If "debug" is true, the server will run in the foreground and print
# a lot of debugging information.
;debug = False
-
# GnuTLS priority for the TLS handshake. See gnutls_priority_init(3).
-;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP
-
+;priority = SECURE256:!CTYPE-X.509:+CTYPE-OPENPGP:!RSA:+SIGN-DSA-SHA256
# Zeroconf service name. You need to change this if you for some
# reason want to run more than one server on the same *host*.
# If there are name collisions on the same *network*, the server will
# rename itself to "Mandos #2", etc.
;servicename = Mandos
+
+# Whether to provide a D-Bus system bus interface or not
+;use_dbus = True
+
+# Whether to use IPv6. (Changing this is NOT recommended.)
+;use_ipv6 = True
+
+# Whether to restore saved state on startup
+;restore = True
+
+# The directory where state is saved
+;statedir = /var/lib/mandos
+
+# Whether to run in the foreground
+;foreground = False
+
+# File descriptor number to use for network socket
+;socket =
+
+# Whether to use ZeroConf; if false, requires port or socket
+;zeroconf = True
=== modified file 'mandos.conf.xml'
--- mandos.conf.xml 2008-08-08 01:31:58 +0000
+++ mandos.conf.xml 2019-06-20 18:54:10 +0000
@@ -1,64 +1,55 @@
-
-
+
/etc/mandos/mandos.conf">
+
+
+%common;
]>
-
+
- &CONFNAME;
-
- &CONFNAME;
- &VERSION;
+ Mandos Manual
+
+ Mandos
+ &version;
+ &TIMESTAMP;
Björn
Påhlsson
- belorn@fukt.bsnet.se
+ belorn@recompile.se
Teddy
Hogeborn
- teddy@fukt.bsnet.se
+ teddy@recompile.se
2008
- Teddy Hogeborn & Björn Påhlsson
+ 2009
+ 2010
+ 2011
+ 2012
+ 2013
+ 2014
+ 2015
+ 2016
+ 2017
+ 2018
+ 2019
+ Teddy Hogeborn
+ Björn Påhlsson
-
-
- This manual page 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 manual page 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
- .
-
-
+
-
+
&CONFNAME;
5
@@ -67,118 +58,140 @@
&CONFNAME;
- Configuration file for Mandos
+ Configuration file for the Mandos server
-
+
-
- &CONFPATH;
-
+ &CONFPATH;
-
+
DESCRIPTION
- The file &CONFPATH; is a simple configuration file for mandos
- and is looked on at startup of the service. The configuration
- file must start with [server]. The format for
- the rest is a simple VAR = VALUE pair. Values may not be empty.
-
-
-
- The paramters are:
-
-
+ The file &CONFPATH; is a simple configuration file for
+ mandos
+ 8, and is read by it at
+ startup. The configuration file starts with [DEFAULT]
on a line by itself, followed by
+ any number of option=value
entries,
+ with continuations in the style of RFC 822. option: value
is also accepted. Note that
+ leading whitespace is removed from values. Lines beginning with
+ #
or ;
are ignored and may be used
+ to provide comments.
+
+
+
+
+ OPTIONS
+
- interface
-
-
- This option allows you to override the default network
- interfaces. By default mandos will not bind to any
- specific interface but instead use default avahi-server
- behaviour.
-
-
-
-
-
- address
-
-
- This option allows you to override the default network
- address. By default mandos will not bind to any
- specific address but instead use default avahi-server
- behaviour.
-
-
-
-
-
- port
-
-
- This option allows you to override the default port to
- listen on. By default mandos will not specify any specific
- port and instead use a random port given by the OS from
- the use of INADDR_ANY.
-
-
-
-
-
- debug
-
-
- This option allows you to modify debug mode with a true/false
- boolean value. By default is debug set to false.
-
-
-
-
-
- priority
-
-
- This option allows you to override the default gnutls
- priority that will be used in gnutls session. See
-