././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1732462094.522593
mandos-1.8.18/ 0000775 0001750 0001750 00000000000 14720643017 012144 5 ustar 00teddy teddy ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/COPYING 0000664 0001750 0001750 00000104374 14720643017 013210 0 ustar 00teddy teddy
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
.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/DBUS-API 0000664 0001750 0001750 00000013664 14720643017 013245 0 ustar 00teddy teddy -*- 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 |
|-------------------------+------+------------+---------------------|
| ApprovalDelay (a) | t | Read/Write | approval_delay |
| ApprovalDuration (a) | t | Read/Write | approval_duration |
| ApprovalPending (b) | b | Read | N/A |
| ApprovedByDefault | b | Read/Write | approved_by_default |
| 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 |
| Host | s | Read/Write | host |
| Interval (a) | t | Read/Write | interval |
| KeyID | s | Read | key_id |
| 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-2020 Teddy Hogeborn
Copyright © 2010-2020 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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/INSTALL 0000664 0001750 0001750 00000013610 14720643017 013176 0 ustar 00teddy teddy -*- 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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/Makefile 0000664 0001750 0001750 00000055332 14720643017 013614 0 ustar 00teddy teddy 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:=-fstack-protector-all -fPIC
CPPFLAGS+=-D_FORTIFY_SOURCE=3
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 -fno-strict-aliasing
LANGUAGE:=-std=gnu11
CPPFLAGS+=-D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64
htmldir:=man
version:=1.8.18
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
# BINDIR:=$(PREFIX)/sbin
# 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
# DBUSPOLICYDIR:=$(DESTDIR)/etc/dbus-1/system.d
##
## These settings are for a package-type install
PREFIX:=$(DESTDIR)/usr
BINDIR:=$(PREFIX)/sbin
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)
DBUSPOLICYDIR:=$(DESTDIR)/usr/share/dbus-1/system.d
##
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 $(PKG_CONFIG) --cflags-only-I gpgme 2>/dev/null \
|| gpgme-config --cflags; getconf LFS_CFLAGS)
GPGME_LIBS:=$(shell $(PKG_CONFIG) --libs gpgme 2>/dev/null \
|| 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) -DVERSION='"$(version)"'
LDFLAGS+=-Xlinker --as-needed $(COVERAGE) $(LINK_FORTIFY) $(strip \
) $(foreach flag,$(LINK_FORTIFY_LD),-Xlinker $(flag))
# 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 \
/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))
.PHONY: all
all: $(PROGS) mandos.lsm
.PHONY: doc
doc: $(DOCS)
.PHONY: html
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/' \
$@)
# Does the linker support the --no-warn-execstack option?
ifeq ($(shell echo 'int main(){}'|$(CC) --language=c /dev/stdin -o /dev/null -Xlinker --no-warn-execstack >/dev/null 2>&1 && echo yes),yes)
# These programs use nested functions, which uses an executable stack
plugin-runner: LDFLAGS += -Xlinker --no-warn-execstack
dracut-module/password-agent: LDFLAGS += -Xlinker --no-warn-execstack
plugins.d/password-prompt: LDFLAGS += -Xlinker --no-warn-execstack
plugins.d/mandos-client: LDFLAGS += -Xlinker --no-warn-execstack
plugins.d/plymouth: LDFLAGS += -Xlinker --no-warn-execstack
endif
# Need to add the GnuTLS, Avahi and GPGME libraries
plugins.d/mandos-client: CFLAGS += $(GNUTLS_CFLAGS) $(strip \
) $(AVAHI_CFLAGS) $(GPGME_CFLAGS)
plugins.d/mandos-client: LDLIBS += $(GNUTLS_LIBS) $(strip \
) $(AVAHI_LIBS) $(GPGME_LIBS)
# Need to add the libnl-route library
plugin-helpers/mandos-client-iprouteadddel: CFLAGS += $(LIBNL3_CFLAGS)
plugin-helpers/mandos-client-iprouteadddel: LDLIBS += $(LIBNL3_LIBS)
# Need to add the GLib and pthread libraries
dracut-module/password-agent: CFLAGS += $(GLIB_CFLAGS)
# Note: -lpthread is unnecessary with the GNU C library 2.34 or later
dracut-module/password-agent: LDLIBS += $(GLIB_LIBS) -lpthread
.PHONY: clean
clean:
-rm --force $(CPROGS) $(objects) $(htmldocs) $(DOCS) core
.PHONY: distclean
distclean: clean
.PHONY: mostlyclean
mostlyclean: clean
.PHONY: maintainer-clean
maintainer-clean: clean
-rm --force --recursive keydir confdir statedir
.PHONY: check
check: all
./mandos --check
./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
.PHONY: run-client
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
if ! [ -e keydir/tls-privkey.pem ]; then \
install --mode=u=rw /dev/null keydir/tls-privkey.pem; \
fi
if ! [ -e keydir/tls-pubkey.pem ]; then \
install --mode=u=rw /dev/null keydir/tls-pubkey.pem; \
fi
# Run the server with a local config
.PHONY: run-server
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 -D --mode=u=rw,go=r $^ $@
confdir/clients.conf: clients.conf keydir/seckey.txt keydir/tls-pubkey.pem
install -D --mode=u=rw $< $@
# Add a client password
./mandos-keygen --dir keydir --password --no-ssh >> $@
statedir:
install --directory statedir
.PHONY: install
install: install-server install-client-nokey
.PHONY: install-html
install-html: html
install -D --mode=u=rw,go=r --target-directory=$(htmldir) \
$(htmldocs)
.PHONY: install-server
install-server: doc
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)" ]; then \
install -D --mode=u=rw,go=r tmpfiles.d-mandos.conf \
$(TMPFILES)/mandos.conf; \
fi
if [ "$(SYSUSERS)" != "$(DESTDIR)" ]; then \
install -D --mode=u=rw,go=r sysusers.d-mandos.conf \
$(SYSUSERS)/mandos.conf; \
fi
install --directory $(BINDIR)
install --mode=u=rwx,go=rx --target-directory=$(BINDIR) mandos
install --mode=u=rwx,go=rx --target-directory=$(BINDIR) \
mandos-ctl
install --mode=u=rwx,go=rx --target-directory=$(BINDIR) \
mandos-monitor
install --directory $(CONFDIR)
install --mode=u=rw,go=r --target-directory=$(CONFDIR) \
mandos.conf
install --mode=u=rw --target-directory=$(CONFDIR) \
clients.conf
install -D --mode=u=rw,go=r dbus-mandos.conf \
$(DBUSPOLICYDIR)/mandos.conf
install -D --mode=u=rwx,go=rx init.d-mandos \
$(DESTDIR)/etc/init.d/mandos
if [ "$(SYSTEMD)" != "$(DESTDIR)" ]; then \
install -D --mode=u=rw,go=r mandos.service \
$(SYSTEMD); \
fi
install -D --mode=u=rw,go=r default-mandos \
$(DESTDIR)/etc/default/mandos
if [ -z $(DESTDIR) ]; then \
update-rc.d mandos defaults 25 15;\
fi
install --directory $(MANDIR)/man8 $(MANDIR)/man5
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
.PHONY: install-client-nokey
install-client-nokey: all doc
install --directory --mode=u=rwx $(KEYDIR) \
$(LIBDIR)/mandos/plugins.d \
$(LIBDIR)/mandos/plugin-helpers
if [ "$(SYSUSERS)" != "$(DESTDIR)" ]; then \
install -D --mode=u=rw,go=r sysusers.d-mandos.conf \
$(SYSUSERS)/mandos-client.conf; \
fi
if [ "$(CONFDIR)" != "$(LIBDIR)/mandos" ]; then \
install --directory \
--mode=u=rwx "$(CONFDIR)/plugins.d" \
"$(CONFDIR)/plugin-helpers"; \
fi
install --directory --mode=u=rwx,go=rx \
"$(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 --directory $(BINDIR)
install --mode=u=rwx,go=rx --target-directory=$(BINDIR) \
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 -D initramfs-tools-hook \
$(INITRAMFSTOOLS)/hooks/mandos
install -D --mode=u=rw,go=r initramfs-tools-conf \
$(INITRAMFSTOOLS)/conf.d/mandos-conf
install -D --mode=u=rw,go=r initramfs-tools-conf-hook \
$(INITRAMFSTOOLS)/conf-hooks.d/zz-mandos
install -D initramfs-tools-script \
$(INITRAMFSTOOLS)/scripts/init-premount/mandos
install -D initramfs-tools-script-stop \
$(INITRAMFSTOOLS)/scripts/local-premount/mandos
install -D --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)
install --directory $(MANDIR)/man8
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
.PHONY: install-client
install-client: install-client-nokey
# Post-installation stuff
-$(BINDIR)/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)"
.PHONY: uninstall
uninstall: uninstall-server uninstall-client
.PHONY: uninstall-server
uninstall-server:
-rm --force $(BINDIR)/mandos \
$(BINDIR)/mandos-ctl \
$(BINDIR)/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)
.PHONY: uninstall-client
uninstall-client:
# Refuse to uninstall client if /etc/crypttab is explicitly configured
# to use it.
! grep --regexp='^ *[^ #].*keyscript=[^,=]*/mandos/' \
$(DESTDIR)/etc/crypttab
-rm --force $(BINDIR)/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
.PHONY: purge
purge: purge-server purge-client
.PHONY: purge-server
purge-server: uninstall-server
-rm --force $(CONFDIR)/mandos.conf $(CONFDIR)/clients.conf \
$(DESTDIR)/etc/dbus-1/system.d/mandos.conf
$(DESTDIR)/etc/default/mandos \
$(DESTDIR)/etc/init.d/mandos \
$(DESTDIR)/run/mandos.pid \
$(DESTDIR)/var/run/mandos.pid
if [ "$(SYSTEMD)" != "$(DESTDIR)" -a -d "$(SYSTEMD)" ]; then \
-rm --force -- $(SYSTEMD)/mandos.service; \
fi
-rmdir $(CONFDIR)
.PHONY: purge-client
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)
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/NEWS 0000664 0001750 0001750 00000056075 14720643017 012660 0 ustar 00teddy teddy This NEWS file records noteworthy changes, very tersely.
See the manual for detailed information.
Version 1.8.18 (2024-11-24)
* Client
** Detect GPGME version correctly when building initramfs.
* Server
** Only connect D-Bus when necessary.
** If a network socket is externally configured, avoid closing it, and
also make it non-inheritable.
** mandos-keygen: Support ssh-keyscan from OpenSSH 9.8 or later.
** mandos-monitor: Avoid debug messages and deprecation warnings from
Urwid.
Version 1.8.17 (2024-09-12)
* Improve documentation slightly, especially how to add extra options
to mandos-client when using dracut with systemd.
* Make life easier for distribution packagers by making sure that
"make install" creates all required directories.
* Server
** When seeing clients.conf entries lacking both fingerprint and
key_id, show a warning and ignore them instead of crashing,
* Client
** Suppress most spurious compiler warnings.
** Use 64-bit time.
** In initramfs-tools-hook, be aware of new name of GPGME library,
libgpgme11t64.
** In password-agent(8mandos), look for a Plymouth process in
alphabetical order, not in reverse alphabetical order. This is
technically a user-visible change, but should never matter in
practice.
Version 1.8.16 (2023-02-08)
* Server
** Bug fix: Start client checkers after a random delay
Version 1.8.15 (2022-04-25)
* Server
** Bug fix: When running "mandos-keygen --password" to read a password
interactively (to save in a section in the clients.conf file),
backslashes in the password are no longer interpreted as backslash
escapes.
** GnuTLS debug output no longer has a "b'" prefix.
Version 1.8.14 (2021-02-03)
* Client
** Create /dev/fd symlink (if necessary) in plugin-runner(8mandos) and
mandos-client(8mandos) (Workaround for Debian bug #981302)
Version 1.8.13 (2020-11-30)
* Client
** Fix unreliable test in password-agent(8mandos).
Version 1.8.12 (2020-07-04)
* Client
** Fix compatibility with the GNU C Library version 2.31.
** In initramfs-tools boots, only use setsid(1) when available.
Version 1.8.11 (2020-04-08)
* Client
** Fix file descriptor leak when adding or removing local routes to
unreachable hosts on the local network.
Version 1.8.10 (2020-03-21)
* Server
** Fix bug when setting a client's D-Bus "Secret" property
** Start client checkers after a random delay
** When using systemd, allow easier modification of server options
** Better log messages in mandos-monitor
* Client
** When using dracut & systemd, allow easier modification of options
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.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/README 0000664 0001750 0001750 00000000632 14720643017 013025 0 ustar 00teddy teddy 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.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/TODO 0000664 0001750 0001750 00000012041 14720643017 012632 0 ustar 00teddy teddy -*- org -*-
* Testing
** python-nemu
* mandos-applet
** [[https://www.freedesktop.org/software/polkit/docs/latest/polkit-apps.html][Writing polkit applications]]
* mandos-client
** TODO A ~--server~ option which only adds to the server list.
(Unlike ~--connect~, which implicitly disables ZeroConf.)
** TODO [#B] Use [[man:capabilities][capabilities]] instead of [[info:libc#Setting%20User%20ID][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~ [[man:bridge-utils-interfaces][bridge-utils-interfaces(5)]]
** TODO [#A] Detect partial writes to stdout and exit with ~EX_TEMPFAIL~
* splashy
** TODO [#B] use [[info:libc#Scanning%20Directory%20Content][scandir(3)]] instead of [[info:libc#Reading/Closing%20Directory][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 [[info:libc#Scanning%20Directory%20Content][scandir(3)]] instead of [[info:libc#Reading/Closing%20Directory][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 [[info:libc#File%20Locks][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] 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 [[man:run-parts][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("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 [[https://standards.freedesktop.org/secret-service/][Secret Service]] API?
** 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:
* 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 [[man:run-parts][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
#+FILETAGS: :mandos:
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/bugs.xml 0000664 0001750 0001750 00000001033 14720643017 013623 0 ustar 00teddy teddy
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).
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/clients.conf 0000664 0001750 0001750 00000007073 14720643017 014463 0 ustar 00teddy teddy # Default settings for all clients. These values are the default
# values, so uncomment and change them if you want different ones.
[DEFAULT]
# How long until a client is disabled and not be allowed to get the
# data this server holds.
# (RFC 3339 duration syntax)
;timeout = PT5M
# How often to run the checker to confirm that a client is still up.
# Note: a new checker will not be started if an old one is still
# running. The server will wait for a checker to complete until the
# above "timeout" occurs, at which time the client will be disabled,
# and any running checker killed.
# (RFC 3339 duration syntax)
;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.
# (RFC 3339 duration syntax)
;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.
# (RFC 3339 duration syntax)
;approval_delay = PT0S
# How long one approval will last.
# (RFC 3339 duration syntax)
;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
;
;# This is base64-encoded binary data. It will be decoded and sent to
;# the client matching the above key_id (for GnuTLS 3.6.6 or later) or
;# the above fingerprint (for GnuTLS before 3.6.0). This should, of
;# course, be OpenPGP encrypted data, decryptable only by the client.
;secret =
; hQIOA6QdEjBs2L/HEAf/TCyrDe5Xnm9esa+Pb/vWF9CUqfn4srzVgSu234
; REJMVv7lBSrPE2132Lmd2gqF1HeLKDJRSVxJpt6xoWOChGHg+TMyXDxK+N
; Xl89vGvdU1XfhKkVm9MDLOgT5ECDPysDGHFPDhqHOSu3Kaw2DWMV/iH9vz
; 3Z20erVNbdcvyBnuojcoWO/6yfB5EQO0BXp7kcyy00USA3CjD5FGZdoQGI
; Tb8A/ar0tVA5crSQmaSotm6KmNLhrFnZ5BxX+TiE+eTUTqSloWRY6VAvqW
; QHC7OASxK5E6RXPBuFH5IohUA2Qbk5AHt99pYvsIPX88j2rWauOokoiKZo
; t/9leJ8VxO5l3wf/U64IH8bkPIoWmWZfd/nqh4uwGNbCgKMyT+AnvH7kMJ
; 3i7DivfWl2mKLV0PyPHUNva0VQxX6yYjcOhj1R6fCr/at8/NSLe2OhLchz
; dC+Ls9h+kvJXgF8Sisv+Wk/1RadPLFmraRlqvJwt6Ww21LpiXqXHV2mIgq
; WnR98YgSvUi3TJHrUQiNc9YyBzuRo0AjgG2C9qiE3FM+Y28+iQ/sR3+bFs
; zYuZKVTObqiIslwXu7imO0cvvFRgJF/6u3HNFQ4LUTGhiM3FQmC6NNlF3/
; vJM2hwRDMcJqDd54Twx90Wh+tYz0z7QMsK4ANXWHHWHR0JchnLWmenzbtW
; 5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm
; 4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O
; QlnHIvPzEArRQLo=
;
;# Host name; used only by the checker, not used by the server itself.
;host = foo.example.org
;####
;####
;# 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/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 = PT1M
;
;# This client requires manual approval before it receives its secret.
;approved_by_default = False
;# Require approval within 30 seconds.
;approval_delay = PT30S
;####
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/common.ent 0000664 0001750 0001750 00000000136 14720643017 014144 0 ustar 00teddy teddy
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/dbus-mandos.conf 0000664 0001750 0001750 00000001464 14720643017 015234 0 ustar 00teddy teddy
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/default-mandos 0000664 0001750 0001750 00000000256 14720643017 014775 0 ustar 00teddy teddy # Directory where configuration files are located. Default is
# "/etc/mandos".
#
#CONFIGDIR=/etc/mandos
# Additional options that are passed to the Daemon.
DAEMON_ARGS=""
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1732462094.518593
mandos-1.8.18/dracut-module/ 0000775 0001750 0001750 00000000000 14720643017 014711 5 ustar 00teddy teddy ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/dracut-module/ask-password-mandos.path 0000664 0001750 0001750 00000003532 14720643017 021467 0 ustar 00teddy teddy # -*- 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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/dracut-module/ask-password-mandos.service 0000664 0001750 0001750 00000005732 14720643017 022177 0 ustar 00teddy teddy # -*- systemd -*-
#
# Copyright © 2019-2023 Teddy Hogeborn
# Copyright © 2019-2023 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 $PASSWORD_AGENT_OPTIONS -- /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 $MANDOS_CLIENT_OPTIONS
#
# Please keep the above line intact, exactly as it is! To add extra
# options to mandos-client, instead create an override file (e.g. with
# the command "systemctl edit --force ask-password-mandos.service"),
# and, in that file, put something like the following:
#
# [Service]
# Environment=MANDOS_CLIENT_OPTIONS=--debug
#
# Rebuild the initramfs using this command:
#
# dpkg-reconfigure dracut
#
# Once the system has booted (possibly by typing in the password
# manually), you can see the log using this command:
#
# journalctl --unit=ask-password-mandos.service
#
# Lastly, to remove the override file with extra options, run:
#
# systemctl revert ask-password-mandos.service
#
# And rebuild the initramfs again, as above.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/dracut-module/cmdline-mandos.sh 0000775 0001750 0001750 00000005547 14720643017 020155 0 ustar 00teddy teddy #!/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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/dracut-module/module-setup.sh 0000775 0001750 0001750 00000017420 14720643017 017677 0 ustar 00teddy teddy #!/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 [ -d /etc/systemd/system/ask-password-mandos.service.d ]; then
inst /etc/systemd/system/ask-password-mandos.service.d
inst_multiple -o /etc/systemd/system/ask-password-mandos.service.d/*.conf
fi
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/ \$MANDOS_CLIENT_OPTIONS/ --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(){
:
}
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/dracut-module/password-agent.c 0000664 0001750 0001750 00001025405 14720643017 020022 0 ustar 00teddy teddy /* -*- coding: utf-8; lexical-binding: t -*- */
/*
* Mandos password agent - Simple password agent to run Mandos client
*
* Copyright © 2019-2022 Teddy Hogeborn
* Copyright © 2019-2022 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 /* pipe2(), O_CLOEXEC, setresgid(),
setresuid(), asprintf(), getline(),
basename() */
#include /* uintmax_t, strtoumax(), PRIuMAX,
PRIdMAX, intmax_t, uint32_t,
SCNx32, SCNuMAX, SCNxMAX */
#include /* size_t, NULL */
#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 /* 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 /* EXIT_SUCCESS, EXIT_FAILURE,
malloc(), free(), 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,
ENOMEM, EEXIST, ECHILD, EPERM,
EAGAIN, EINTR, ENOBUFS, EADDRINUSE,
ECONNREFUSED, ECONNRESET,
ETOOMANYREFS, EMSGSIZE, EBADF,
EINVAL */
#include /* strdup(), memcpy(),
explicit_bzero(), memset(),
strcmp(), strlen(), strncpy(),
memcmp(), basename(), strerror() */
#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 /* SIZE_MAX, uint32_t */
#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, stderr, fopen(),
fclose(), getline(), sscanf(),
feof(), ferror(), 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_message(), 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){
if((queue->length + 1) > (SIZE_MAX / sizeof(task_context))){
/* overflow */
error(0, ENOMEM, "Failed to allocate %" PRIuMAX
" tasks for queue->tasks", (uintmax_t)(queue->length + 1));
errno = ENOMEM;
return false;
}
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(pid == -1){
error(0, errno, "Failed to fork()");
close(pipefds[0]);
return false;
}
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;
#if defined(__GNUC__) and __GNUC__ >= 7
#pragma GCC diagnostic push
/* ievent is pointing into a struct which is of sufficient size */
#pragma GCC diagnostic ignored "-Wstringop-overflow"
#endif
const ssize_t read_length = read(fd, ievent, ievent_size);
#if defined(__GNUC__) and __GNUC__ >= 7
#pragma GCC diagnostic pop
#endif
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 defined(__GNUC__) and __GNUC__ >= 5
#pragma GCC diagnostic push
/* mlock() does not access the memory */
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
if(mlock(send_buffer, send_buffer_length) != 0){
#if defined(__GNUC__) and __GNUC__ >= 5
#pragma GCC diagnostic pop
#endif
/* 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.
— (Tue, 15 Sep 2020
14:24:20 GMT)
*/
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 = (ssret < 0) ? errno : 0;
#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, saved_errno, "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 %s: %"
PRIuMAX " out of %" PRIuMAX " bytes sent", filename,
(uintmax_t)ssret, (uintmax_t)send_buffer_length);
}
#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 test_add_to_queue_overflow(__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(queue->length == 0);
queue->length = SIZE_MAX / sizeof(task_context); /* fake max size */
FILE *real_stderr = stderr;
FILE *devnull = fopen("/dev/null", "we");
g_assert_nonnull(devnull);
stderr = devnull;
const bool ret = add_to_queue(queue,
(task_context){ .func=dummy_func });
g_assert_true(errno == ENOMEM);
g_assert_false(ret);
stderr = real_stderr;
g_assert_cmpint(fclose(devnull), ==, 0);
queue->length = 0; /* Restore real size */
}
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);
}
if(pid == -1){
error(EXIT_FAILURE, errno, "Failed to fork()");
}
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",
"printf %s \"${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));
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic push
/* ievent is pointing into a struct which is of sufficient size */
#pragma GCC diagnostic ignored "-Wstringop-overread"
#endif
g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
==, ievent_size);
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic pop
#endif
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));
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic push
/* ievent is pointing into a struct which is of sufficient size */
#pragma GCC diagnostic ignored "-Wstringop-overread"
#endif
g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
==, ievent_size);
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic pop
#endif
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));
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic push
/* ievent is pointing into a struct which is of sufficient size */
#pragma GCC diagnostic ignored "-Wstringop-overread"
#endif
g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
==, ievent_size);
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic pop
#endif
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));
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic push
/* ievent is pointing into a struct which is of sufficient size */
#pragma GCC diagnostic ignored "-Wstringop-overread"
#endif
g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
==, ievent_size);
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic pop
#endif
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));
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic push
/* ievent is pointing into a struct which is of sufficient size */
#pragma GCC diagnostic ignored "-Wstringop-overread"
#endif
g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
==, ievent_size);
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic pop
#endif
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));
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic push
/* ievent is pointing into a struct which is of sufficient size */
#pragma GCC diagnostic ignored "-Wstringop-overread"
#endif
g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
==, ievent_size);
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic pop
#endif
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));
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic push
/* ievent is pointing into a struct which is of sufficient size */
#pragma GCC diagnostic ignored "-Wstringop-overread"
#endif
g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
==, ievent_size);
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic pop
#endif
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));
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic push
/* ievent is pointing into a struct which is of sufficient size */
#pragma GCC diagnostic ignored "-Wstringop-overread"
#endif
g_assert_cmpint(write(pipefds[1], (char *)ievent, ievent_size),
==, ievent_size);
#if defined(__GNUC__) and __GNUC__ >= 11
#pragma GCC diagnostic pop
#endif
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 protocol 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 = {};
int socketfds[2];
/* Find a message size which triggers EMSGSIZE */
__attribute__((cleanup(cleanup_string)))
char *message_buffer = NULL;
size_t message_size = PIPE_BUF + 1;
for(ssize_t ssret = 0; ssret >= 0; message_size += 1024){
if(message_size >= 1024*1024*1024){ /* 1 GiB */
g_test_skip("Skipping EMSGSIZE test: Will not try 1GiB");
return;
}
message_buffer = realloc(message_buffer, message_size);
if(message_buffer == NULL){
g_test_skip("Skipping EMSGSIZE test");
g_test_message("Failed to malloc() %" PRIuMAX " bytes",
(uintmax_t)message_size);
return;
}
/* Fill buffer with 'x' */
memset(message_buffer, 'x', message_size);
/* Create a new socketpair for each message size to avoid having
to empty the pipe by reading the message to a separate buffer
*/
g_assert_cmpint(socketpair(PF_LOCAL, SOCK_DGRAM
| SOCK_NONBLOCK | SOCK_CLOEXEC, 0,
socketfds), ==, 0);
ssret = send(socketfds[1], message_buffer, message_size,
MSG_NOSIGNAL);
error_t saved_errno = errno;
g_assert_cmpint(close(socketfds[0]), ==, 0);
g_assert_cmpint(close(socketfds[1]), ==, 0);
if(ssret < 0){
if(saved_errno != EMSGSIZE) {
g_test_skip("Skipping EMSGSIZE test");
g_test_message("Error on send(%" PRIuMAX " bytes): %s",
(uintmax_t)message_size,
strerror(saved_errno));
return;
}
break;
} else if(ssret != (ssize_t)message_size){
g_test_skip("Skipping EMSGSIZE test");
g_test_message("Partial send(): %" PRIuMAX " of %" PRIdMAX
" bytes", (uintmax_t)ssret,
(intmax_t)message_size);
return;
}
}
g_test_message("EMSGSIZE triggered by %" PRIdMAX " bytes",
(intmax_t)message_size);
buffer password = {
.data=message_buffer,
.length=message_size - 2, /* Compensate for added '+' and NUL */
.allocated=message_size,
};
if(mlock(password.data, password.allocated) != 0){
g_assert_true(errno == EPERM or errno == ENOMEM);
}
__attribute__((cleanup(cleanup_queue)))
task_queue *queue = create_queue();
g_assert_nonnull(queue);
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/add/overflow", test_add_to_queue_overflow);
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 should_run_tests = FALSE;
GOptionEntry entries[] = {
{ "test", 0, 0, G_OPTION_ARG_NONE,
&should_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 should_run_tests != FALSE;
}
/*
Local Variables:
run-tests:
(lambda ()
(if (not (funcall run-tests-in-test-buffer default-directory))
(funcall show-test-buffer-in-test-window)
(funcall remove-test-window)))
run-tests-in-test-buffer:
(lambda (dir)
(with-current-buffer (get-buffer-create "*Test*")
(setq buffer-read-only nil
default-directory dir)
(erase-buffer)
(compilation-mode))
(let ((process-result
(let ((inhibit-read-only t))
(process-file-shell-command
(funcall get-command-line) nil "*Test*"))))
(and (numberp process-result)
(= process-result 0))))
get-command-line:
(lambda ()
(let*
((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))
(qbdir (shell-quote-argument local-build-directory))
(qcmd (shell-quote-argument command)))
(format (concat "cd %s && CFLAGS=-Werror make --silent %s"
" && %s --test --verbose") qbdir qcmd qcmd)))
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)))))
show-test-buffer-in-test-window:
(lambda ()
(when (not (get-buffer-window-list "*Test*"))
(setq next-error-last-buffer (get-buffer "*Test*"))
(let* ((side (if (>= (window-width) 146) 'right 'bottom))
(display-buffer-overriding-action
`((display-buffer-in-side-window) (side . ,side)
(window-height . fit-window-to-buffer)
(window-width . fit-window-to-buffer))))
(display-buffer "*Test*"))))
remove-test-window:
(lambda ()
(let ((test-window (get-buffer-window "*Test*")))
(if test-window (delete-window test-window))))
eval: (add-hook 'after-save-hook run-tests 90 t)
End:
*/
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/dracut-module/password-agent.xml 0000664 0001750 0001750 00000035145 14720643017 020401 0 ustar 00teddy teddy
%common;
]>
Mandos Manual
Mandos
&version;
&TIMESTAMP;
Björn
Påhlsson
belorn@recompile.se
Teddy
Hogeborn
teddy@recompile.se
2019
2020
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/plugins.d/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.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/init.d-mandos 0000664 0001750 0001750 00000010377 14720643017 014543 0 ustar 00teddy teddy #! /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
:
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/initramfs-tools-conf 0000664 0001750 0001750 00000001267 14720643017 016152 0 ustar 00teddy teddy # -*- 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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/initramfs-tools-conf-hook 0000664 0001750 0001750 00000001207 14720643017 017102 0 ustar 00teddy teddy # -*- 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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/initramfs-tools-hook 0000775 0001750 0001750 00000017332 14720643017 016170 0 ustar 00teddy teddy #!/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}\n' --show libgpgme11t64 libgpgme11 2>/dev/null | sed --quiet --expression='/./{p;q}'`"
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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/initramfs-tools-script 0000775 0001750 0001750 00000011232 14720643017 016525 0 ustar 00teddy teddy #!/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
# Use setsid if available
if command -v setsid >/dev/null 2>&1; then
setsid /lib/mandos/mandos-to-cryptroot-unlock &
else
/lib/mandos/mandos-to-cryptroot-unlock &
fi
fi
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/initramfs-tools-script-stop 0000775 0001750 0001750 00000003564 14720643017 017521 0 ustar 00teddy teddy #!/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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/initramfs-unpack 0000775 0001750 0001750 00000005623 14720643017 015353 0 ustar 00teddy teddy #!/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"
elif { $catimg 2>/dev/null | zstd --test >/dev/null 2>&1;
[ ${PIPESTATUS[-1]} -eq 0 ]; }; then
decomp="zstdcat --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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/intro.xml 0000664 0001750 0001750 00000043745 14720643017 014036 0 ustar 00teddy teddy
%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
2020
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.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/legalnotice.xml 0000664 0001750 0001750 00000002043 14720643017 015153 0 ustar 00teddy teddy
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/.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos 0000775 0001750 0001750 00000451723 14720643017 013367 0 ustar 00teddy teddy #!/usr/bin/python3 -bI
# -*- coding: utf-8; lexical-binding: t -*-
#
# 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", "remove", "server_state_changed",
# "entry_group_state_changed", "cleanup", and "activate" in the
# "AvahiService" class, and some lines in "main".
#
# Everything else is
# Copyright © 2008-2022 Teddy Hogeborn
# Copyright © 2008-2022 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 unittest
import argparse
import logging
import os
try:
import SocketServer as socketserver
except ImportError:
import socketserver
import socket
import datetime
import errno
try:
import ConfigParser as configparser
except ImportError:
import configparser
import re
import signal
import subprocess
import atexit
import stat
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 random
import shlex
import dbus
import dbus.service
import gi
from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop
import ctypes
import ctypes.util
import xml.dom.minidom
import inspect
if sys.version_info.major == 2:
__metaclass__ = type
str = unicode
input = raw_input
# Add collections.abc.Callable if it does not exist
try:
collections.abc.Callable
except AttributeError:
class abc:
Callable = collections.Callable
collections.abc = abc
del abc
# Add shlex.quote if it does not exist
try:
shlex.quote
except AttributeError:
shlex.quote = re.escape
# Add os.set_inheritable if it does not exist
try:
os.set_inheritable
except AttributeError:
def set_inheritable(fd, inheritable):
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
if inheritable and ((flags & fcntl.FD_CLOEXEC) != 0):
fcntl.fcntl(fd, fcntl.F_SETFL, flags & ~fcntl.FD_CLOEXEC)
elif (not inheritable) and ((flags & fcntl.FD_CLOEXEC) == 0):
fcntl.fcntl(fd, fcntl.F_SETFL, flags | fcntl.FD_CLOEXEC)
os.set_inheritable = set_inheritable
del set_inheritable
# 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 < (3, 2):
configparser.Configparser = configparser.SafeConfigParser
version = "1.8.18"
stored_state_file = "clients.pickle"
log = logging.getLogger(os.path.basename(sys.argv[0]))
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"))
log.addHandler(syslogger)
if debug:
console = logging.StreamHandler()
console.setFormatter(logging.Formatter("%(asctime)s %(name)s"
" [%(process)d]:"
" %(levelname)s:"
" %(message)s"))
log.addHandler(console)
log.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, *args, **kwargs):
self.value = value
return super(AvahiError, self).__init__(value, *args,
**kwargs)
class AvahiServiceError(AvahiError):
pass
class AvahiGroupError(AvahiError):
pass
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
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.
host: string; Host to publish records for, default is localhost
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,
servicetype=None,
port=None,
TXT=None,
domain="",
host="",
max_renames=32768,
protocol=avahi.PROTO_UNSPEC,
bus=None):
self.interface = interface
self.name = name
self.type = servicetype
self.port = port
self.TXT = TXT if TXT is not None else []
self.domain = domain
self.host = host
self.rename_count = 0
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:
log.critical("No suitable Zeroconf service name found"
" after %i retries, exiting.",
self.rename_count)
raise AvahiServiceError("Too many renames")
self.name = str(
self.server.GetAlternativeServiceName(self.name))
self.rename_count += 1
log.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"):
log.info("Local Zeroconf service name collision.")
return self.rename(remove=False)
else:
log.critical("D-Bus Exception", exc_info=error)
self.cleanup()
os._exit(1)
def remove(self):
"""Derived from the Avahi example code"""
if self.entry_group_state_changed_match is not None:
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"""
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))
log.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"""
log.debug("Avahi entry group state change: %i", state)
if state == avahi.ENTRY_GROUP_ESTABLISHED:
log.debug("Zeroconf service established.")
elif state == avahi.ENTRY_GROUP_COLLISION:
log.info("Zeroconf service name collision.")
self.rename()
elif state == avahi.ENTRY_GROUP_FAILURE:
log.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"""
log.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:
log.error(bad_states[state])
else:
log.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"):
log.info("Local Zeroconf service name collision.")
return self.rename(remove=False)
else:
log.critical("D-Bus Exception", exc_info=error)
self.cleanup()
os._exit(1)
else:
if error is None:
log.debug("Unknown state: %r", state)
else:
log.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).decode(
"utf-8", errors="replace")
return super(gnutls.Error, self).__init__(
message, *args)
class CertificateSecurityError(Error):
pass
class PointerTo:
def __init__(self, cls):
self.cls = cls
def from_param(self, obj):
if not isinstance(obj, self.cls):
raise TypeError("Not of type {}: {!r}"
.format(self.cls.__name__, obj))
return ctypes.byref(obj.from_param(obj))
class CastToVoidPointer:
def __init__(self, cls):
self.cls = cls
def from_param(self, obj):
if not isinstance(obj, self.cls):
raise TypeError("Not of type {}: {!r}"
.format(self.cls.__name__, obj))
return ctypes.cast(obj.from_param(obj), ctypes.c_void_p)
class With_from_param:
@classmethod
def from_param(cls, obj):
return obj._as_parameter_
# Classes
class Credentials(With_from_param):
def __init__(self):
self._as_parameter_ = gnutls.certificate_credentials_t()
gnutls.certificate_allocate_credentials(self)
self.type = gnutls.CRD_CERTIFICATE
def __del__(self):
gnutls.certificate_free_credentials(self)
class ClientSession(With_from_param):
def __init__(self, socket, credentials=None):
self._as_parameter_ = 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(self, gnutls_flags)
del gnutls_flags
gnutls.set_default_priority(self)
gnutls.transport_set_ptr(self, socket.fileno())
gnutls.handshake_set_private_extensions(self, True)
self.socket = socket
if credentials is None:
credentials = gnutls.Credentials()
gnutls.credentials_set(self, credentials.type,
credentials)
self.credentials = credentials
def __del__(self):
gnutls.deinit(self)
def handshake(self):
return gnutls.handshake(self)
def send(self, data):
data = bytes(data)
data_len = len(data)
while data_len > 0:
data_len -= gnutls.record_send(self, data[-data_len:],
data_len)
def bye(self):
return gnutls.bye(self, 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 >= gnutls.E_SUCCESS:
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,
_error_code=_error_code):
"""A function to retry on some errors, suitable
for the "errcheck" attribute on ctypes functions"""
while result < gnutls.E_SUCCESS:
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 = [ClientSession, ctypes.c_char_p,
ctypes.POINTER(ctypes.c_char_p)]
priority_set_direct.restype = _error_code
init = _library.gnutls_init
init.argtypes = [PointerTo(ClientSession), ctypes.c_int]
init.restype = _error_code
set_default_priority = _library.gnutls_set_default_priority
set_default_priority.argtypes = [ClientSession]
set_default_priority.restype = _error_code
record_send = _library.gnutls_record_send
record_send.argtypes = [ClientSession, 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 = [
PointerTo(Credentials)]
certificate_allocate_credentials.restype = _error_code
certificate_free_credentials = (
_library.gnutls_certificate_free_credentials)
certificate_free_credentials.argtypes = [Credentials]
certificate_free_credentials.restype = None
handshake_set_private_extensions = (
_library.gnutls_handshake_set_private_extensions)
handshake_set_private_extensions.argtypes = [ClientSession,
ctypes.c_int]
handshake_set_private_extensions.restype = None
credentials_set = _library.gnutls_credentials_set
credentials_set.argtypes = [ClientSession, credentials_type_t,
CastToVoidPointer(Credentials)]
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 = [ClientSession]
certificate_type_get.restype = _error_code
certificate_get_peers = _library.gnutls_certificate_get_peers
certificate_get_peers.argtypes = [ClientSession,
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 = [ClientSession]
deinit.restype = None
handshake = _library.gnutls_handshake
handshake.argtypes = [ClientSession]
handshake.restype = ctypes.c_int
handshake.errcheck = _retry_on_error
transport_set_ptr = _library.gnutls_transport_set_ptr
transport_set_ptr.argtypes = [ClientSession, transport_ptr_t]
transport_set_ptr.restype = None
bye = _library.gnutls_bye
bye.argtypes = [ClientSession, close_request_t]
bye.restype = ctypes.c_int
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 = [ClientSession, 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:
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.
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()
"""
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.get("fingerprint",
"").upper()
.replace(" ", ""))
if not (client["key_id"] or client["fingerprint"]):
log.error("Skipping client %s without key_id or"
" fingerprint", client_name)
del settings[client_name]
continue
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
if server_settings is None:
server_settings = {}
self.server_settings = server_settings
# adding all client settings
for setting, value in settings.items():
setattr(self, setting, value)
if self.enabled:
if not hasattr(self, "last_enabled"):
self.last_enabled = datetime.datetime.utcnow()
if not hasattr(self, "expires"):
self.expires = (datetime.datetime.utcnow()
+ self.timeout)
else:
self.last_enabled = None
self.expires = None
log.debug("Creating client %r", self.name)
log.debug(" Key ID: %s", self.key_id)
log.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.disable_initiator_tag = None
self.checker_callback_tag = None
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.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:
log.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, randomize_start=False):
# Schedule a new checker to be started a randomly selected
# time (a fraction of 'interval') from now. This spreads out
# the startup of checkers over time when the server is
# started.
if self.checker_initiator_tag is not None:
GLib.source_remove(self.checker_initiator_tag)
interval_milliseconds = int(self.interval.total_seconds()
* 1000)
if randomize_start:
delay_milliseconds = random.randrange(
interval_milliseconds + 1)
else:
delay_milliseconds = interval_milliseconds
self.checker_initiator_tag = GLib.timeout_add(
delay_milliseconds, self.start_checker, randomize_start)
delay = datetime.timedelta(0, 0, 0, delay_milliseconds)
# A checker might take up to an 'interval' of time, so we can
# expire at the soonest one interval after a checker was
# started. Since the initial checker is delayed, the expire
# time might have to be extended.
now = datetime.datetime.utcnow()
self.expires = now + delay + self.interval
# Schedule a disable() at expire time
if self.disable_initiator_tag is not None:
GLib.source_remove(self.disable_initiator_tag)
self.disable_initiator_tag = GLib.timeout_add(
int((self.expires - now).total_seconds() * 1000),
self.disable)
def checker_callback(self, source, condition, connection,
command):
"""The checker has completed, so take appropriate actions."""
# 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 returncode >= 0:
self.last_checker_status = returncode
self.last_checker_signal = None
if self.last_checker_status == 0:
log.info("Checker for %(name)s succeeded", vars(self))
self.checked_ok()
else:
log.info("Checker for %(name)s failed", vars(self))
else:
self.last_checker_status = -1
self.last_checker_signal = -returncode
log.warning("Checker for %(name)s crashed?", vars(self))
return False
def checked_ok(self):
"""Assert that the client has been seen, alive and well."""
self.last_checked_ok = datetime.datetime.utcnow()
self.last_checker_status = 0
self.last_checker_signal = None
self.bump_timeout()
def bump_timeout(self, timeout=None):
"""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_was_randomized=False):
"""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, and if a checker (for some reason) started running
# slowly and taking more than 'interval' time, then the client
# would inevitably timeout, since no checker would get a
# chance to run to completion. If we instead leave running
# checkers alone, the checker would have to take more time
# than 'timeout' for the client to be disabled, which is as it
# should be.
if self.checker is not None and not self.checker.is_alive():
log.warning("Checker was not alive; joining")
self.checker.join()
self.checker = None
# Start a new checker if needed
if self.checker is None:
# Escape attributes for the shell
escaped_attrs = {
attr: shlex.quote(str(getattr(self, attr)))
for attr in self.runtime_expansions}
try:
command = self.checker_command % escaped_attrs
except TypeError as error:
log.error('Could not format string "%s"',
self.checker_command, exc_info=error)
return True # Try again later
self.current_checker_command = command
log.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)
if start_was_randomized:
# We were started after a random delay; Schedule a new
# checker to be started an 'interval' from now, and every
# interval from then on.
now = datetime.datetime.utcnow()
self.checker_initiator_tag = GLib.timeout_add(
int(self.interval.total_seconds() * 1000),
self.start_checker)
self.expires = max(self.expires, now + self.interval)
# Don't start a new checker again after same random delay
return False
# 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:
GLib.source_remove(self.checker_callback_tag)
self.checker_callback_tag = None
if getattr(self, "checker", None) is None:
return
log.debug("Stopping checker for %(name)s", vars(self))
self.checker.terminate()
self.checker = None
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:
log.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(bytes(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:
log.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:
log.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):
with contextlib.closing(self.server.child_pipe) as child_pipe:
log.info("TCP connection from: %s",
str(self.client_address))
log.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,
priority.encode("utf-8"), None)
# Start communication using the Mandos protocol
# Get protocol number
line = self.request.makefile().readline()
log.debug("Protocol version: %r", line)
try:
if int(line.strip().split()[0]) > 1:
raise RuntimeError(line)
except (ValueError, IndexError, RuntimeError) as error:
log.error("Unknown protocol version: %s", error)
return
# Start GnuTLS connection
try:
session.handshake()
except gnutls.Error as error:
log.warning("Handshake failed: %s", error)
# Do not run session.bye() here: the session is not
# established. Just abandon the request.
return
log.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:
log.warning("Bad certificate: %s", error)
return
log.debug("Key ID: %s",
key_id.decode("utf-8",
errors="replace"))
else:
key_id = b""
try:
fpr = self.fingerprint(
self.peer_certificate(session))
except (TypeError, gnutls.Error) as error:
log.warning("Bad certificate: %s", error)
return
log.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:
log.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:
log.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:
log.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:
log.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:
log.warning("gnutls send failed", exc_info=error)
return
log.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:
log.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, gnutls.CTYPE_PEERS)
except AttributeError:
cert_type = gnutls.certificate_type_get(session)
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:
log.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, 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:
enabled: Boolean; whether this server is activated yet
interface: None or a network interface name (string)
use_ipv6: Boolean; to use IPv6 or not
"""
def __init__(self, server_address, RequestHandlerClass,
interface=None,
use_ipv6=True,
socketfd=None):
"""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."""
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.
log.warning("SO_BINDTODEVICE not found, trying 25")
SO_BINDTODEVICE = 25
try:
self.socket.setsockopt(
socket.SOL_SOCKET, SO_BINDTODEVICE,
(self.interface + "\0").encode("utf-8"))
except socket.error as error:
if error.errno == errno.EPERM:
log.error("No permission to bind to interface %s",
self.interface)
elif error.errno == errno.ENOPROTOOPT:
log.error("SO_BINDTODEVICE not available; cannot"
" bind to interface %s", self.interface)
elif error.errno == errno.ENODEV:
log.error("Interface %s does not exist, cannot"
" bind", self.interface)
else:
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]:
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.interface:
# self.server_address = (self.server_address[0],
# 0, # port
# 0, # flowinfo
# if_nametoindex
# (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 == ("E3B0C44298FC1C149AFBF4C8996FB924"
"27AE41E4649B934CA495991B7852B855"):
continue
if key_id and c.key_id == key_id:
client = c
break
if fpr and c.fingerprint == fpr:
client = c
break
else:
log.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.abc.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
>>> timedelta = datetime.timedelta
>>> rfc3339_duration_to_delta("P7D") == timedelta(7)
True
>>> rfc3339_duration_to_delta("PT60S") == timedelta(0, 60)
True
>>> rfc3339_duration_to_delta("PT60M") == timedelta(0, 3600)
True
>>> rfc3339_duration_to_delta("PT24H") == timedelta(1)
True
>>> rfc3339_duration_to_delta("P1W") == timedelta(7)
True
>>> rfc3339_duration_to_delta("PT5M30S") == timedelta(0, 330)
True
>>> rfc3339_duration_to_delta("P1DT3M20S") == timedelta(1, 200)
True
>>> del timedelta
"""
# 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)
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:
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()
os.setsid()
if not nochdir:
os.chdir("/")
if os.fork():
sys.exit()
if not noclose:
# Close all standard open file descriptors
null = os.open(os.devnull, os.O_NOCTTY | os.O_RDWR)
if not stat.S_ISCHR(os.fstat(null).st_mode):
raise OSError(errno.ENODEV,
"{} not a character device"
.format(os.devnull))
os.dup2(null, sys.stdin.fileno())
os.dup2(null, sys.stdout.fileno())
os.dup2(null, sys.stderr.fileno())
if null > 2:
os.close(null)
def main():
##################################################################
# 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
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.ConfigParser(server_defaults)
del server_defaults
server_config.read(os.path.join(options.configdir, "mandos.conf"))
# 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.
while 0 <= server_settings["socket"] <= 2:
server_settings["socket"] = os.dup(server_settings
["socket"])
os.set_inheritable(server_settings["socket"], False)
del server_config
# Override the settings from the config file with command line
# options, if set.
for option in ("interface", "address", "port", "debug",
"priority", "servicename", "configdir", "use_dbus",
"use_ipv6", "debuglevel", "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"]
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 ({}) [%(process)d]:"
" %(levelname)s: %(message)s".format(
server_settings["servicename"])))
# Parse config file with clients
client_config = configparser.ConfigParser(Client.client_defaults)
client_config.read(os.path.join(server_settings["configdir"],
"clients.conf"))
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:
log.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)
log.debug("Did setuid/setgid to %s:%s", uid, gid)
except OSError as error:
log.warning("Failed to setuid/setgid to %s:%s: %s", 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):
log.debug("GnuTLS: %s",
string[:-1].decode("utf-8", errors="replace"))
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
# From the Avahi example code
DBusGMainLoop(set_as_default=True)
main_loop = GLib.MainLoop()
if use_dbus or zeroconf:
bus = dbus.SystemBus()
# End of Avahi example code
if use_dbus:
try:
bus_name = dbus.service.BusName("se.recompile.Mandos",
bus,
do_not_queue=True)
old_bus_name = dbus.service.BusName(
"se.bsnet.fukt.Mandos", bus,
do_not_queue=True)
except dbus.exceptions.DBusException as e:
log.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:
log.warning("Could not load persistent state:"
" %s", os.strerror(e.errno))
else:
log.critical("Could not load persistent state:",
exc_info=e)
raise
except EOFError as e:
log.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"]:
log.warning("disabling client %s - Client"
" never performed a successful"
" checker", client_name)
client["enabled"] = False
elif client["last_checker_status"] != 0:
log.warning("disabling client %s - Client"
" last checker failed with error"
" code %s", client_name,
client["last_checker_status"])
client["enabled"] = False
else:
client["expires"] = (
datetime.datetime.utcnow()
+ client["timeout"])
log.debug("Last checker succeeded, keeping %s"
" enabled", 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
log.debug("Failed to decrypt %s old secret",
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:
log.warning("No clients defined")
if not foreground:
if pidfile is not None:
pid = os.getpid()
try:
with pidfile:
print(pid, file=pidfile)
except IOError:
log.error("Could not write to file %r with PID %d",
pidfilename, pid)
del pidfile
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"
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):
log.warning("Could not save persistent state: %s",
os.strerror(e.errno))
else:
log.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)
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(randomize_start=True)
tcp_server.enable()
tcp_server.server_activate()
# Find out what port we got
if zeroconf:
service.port = tcp_server.socket.getsockname()[1]
if use_ipv6:
log.info("Now listening on address %r, port %d, flowinfo %d,"
" scope_id %d", *tcp_server.socket.getsockname())
else: # IPv4
log.info("Now listening on address %r, port %d",
*tcp_server.socket.getsockname())
# service.interface = tcp_server.socket.getsockname()[3]
try:
if zeroconf:
# From the Avahi example code
try:
service.activate()
except dbus.exceptions.DBusException as error:
log.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))
log.debug("Starting main loop")
main_loop.run()
except AvahiError as error:
log.critical("Avahi Error", exc_info=error)
cleanup()
sys.exit(1)
except KeyboardInterrupt:
if debug:
print("", file=sys.stderr)
log.debug("Server received KeyboardInterrupt")
log.debug("Server exiting")
# Must run before the D-Bus bus name gets deregistered
cleanup()
def parse_test_args():
# type: () -> argparse.Namespace
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--check", action="store_true")
parser.add_argument("--prefix", )
args, unknown_args = parser.parse_known_args()
if args.check:
# Remove test options from sys.argv
sys.argv[1:] = unknown_args
return args
# Add all tests from doctest strings
def load_tests(loader, tests, none):
import doctest
tests.addTests(doctest.DocTestSuite())
return tests
if __name__ == "__main__":
options = parse_test_args()
try:
if options.check:
extra_test_prefix = options.prefix
if extra_test_prefix is not None:
if not (unittest.main(argv=[""], exit=False)
.result.wasSuccessful()):
sys.exit(1)
class ExtraTestLoader(unittest.TestLoader):
testMethodPrefix = extra_test_prefix
# Call using ./scriptname --test [--verbose]
unittest.main(argv=[""], testLoader=ExtraTestLoader())
else:
unittest.main(argv=[""])
else:
main()
finally:
logging.shutdown()
# Local Variables:
# run-tests:
# (lambda (&optional extra)
# (if (not (funcall run-tests-in-test-buffer default-directory
# extra))
# (funcall show-test-buffer-in-test-window)
# (funcall remove-test-window)
# (if extra (message "Extra tests run successfully!"))))
# run-tests-in-test-buffer:
# (lambda (dir &optional extra)
# (with-current-buffer (get-buffer-create "*Test*")
# (setq buffer-read-only nil
# default-directory dir)
# (erase-buffer)
# (compilation-mode))
# (let ((process-result
# (let ((inhibit-read-only t))
# (process-file-shell-command
# (funcall get-command-line extra) nil "*Test*"))))
# (and (numberp process-result)
# (= process-result 0))))
# get-command-line:
# (lambda (&optional extra)
# (let ((quoted-script
# (shell-quote-argument (funcall get-script-name))))
# (format
# (concat "%s --check" (if extra " --prefix=atest" ""))
# quoted-script)))
# get-script-name:
# (lambda ()
# (if (fboundp 'file-local-name)
# (file-local-name (buffer-file-name))
# (or (file-remote-p (buffer-file-name) 'localname)
# (buffer-file-name))))
# remove-test-window:
# (lambda ()
# (let ((test-window (get-buffer-window "*Test*")))
# (if test-window (delete-window test-window))))
# show-test-buffer-in-test-window:
# (lambda ()
# (when (not (get-buffer-window-list "*Test*"))
# (setq next-error-last-buffer (get-buffer "*Test*"))
# (let* ((side (if (>= (window-width) 146) 'right 'bottom))
# (display-buffer-overriding-action
# `((display-buffer-in-side-window) (side . ,side)
# (window-height . fit-window-to-buffer)
# (window-width . fit-window-to-buffer))))
# (display-buffer "*Test*"))))
# eval:
# (progn
# (let* ((run-extra-tests (lambda () (interactive)
# (funcall run-tests t)))
# (inner-keymap `(keymap (116 . ,run-extra-tests))) ; t
# (outer-keymap `(keymap (3 . ,inner-keymap)))) ; C-c
# (setq minor-mode-overriding-map-alist
# (cons `(run-tests . ,outer-keymap)
# minor-mode-overriding-map-alist)))
# (add-hook 'after-save-hook run-tests 90 t))
# End:
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos-clients.conf.xml 0000664 0001750 0001750 00000047315 14720643017 016544 0 ustar 00teddy teddy
/etc/mandos/clients.conf">
%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
&CONFNAME;
5
&CONFNAME;
Configuration file for the Mandos server
&CONFPATH;
DESCRIPTION
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 if the
is not set, and
optional otherwise.
This option sets the OpenPGP fingerprint that (before
GnuTLS 3.6.0) identified 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 required if the
is not set, and
optional otherwise.
This option sets the certificate key ID that (with GnuTLS
3.6.6 or later) 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 = PT5M
interval = PT2M
checker = fping -q -- %%(host)s
# 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
Xl89vGvdU1XfhKkVm9MDLOgT5ECDPysDGHFPDhqHOSu3Kaw2DWMV/iH9vz
3Z20erVNbdcvyBnuojcoWO/6yfB5EQO0BXp7kcyy00USA3CjD5FGZdoQGI
Tb8A/ar0tVA5crSQmaSotm6KmNLhrFnZ5BxX+TiE+eTUTqSloWRY6VAvqW
QHC7OASxK5E6RXPBuFH5IohUA2Qbk5AHt99pYvsIPX88j2rWauOokoiKZo
t/9leJ8VxO5l3wf/U64IH8bkPIoWmWZfd/nqh4uwGNbCgKMyT+AnvH7kMJ
3i7DivfWl2mKLV0PyPHUNva0VQxX6yYjcOhj1R6fCr/at8/NSLe2OhLchz
dC+Ls9h+kvJXgF8Sisv+Wk/1RadPLFmraRlqvJwt6Ww21LpiXqXHV2mIgq
WnR98YgSvUi3TJHrUQiNc9YyBzuRo0AjgG2C9qiE3FM+Y28+iQ/sR3+bFs
zYuZKVTObqiIslwXu7imO0cvvFRgJF/6u3HNFQ4LUTGhiM3FQmC6NNlF3/
vJM2hwRDMcJqDd54Twx90Wh+tYz0z7QMsK4ANXWHHWHR0JchnLWmenzbtW
5MHdW9AYsNJZAQSOpirE4Xi31CSlWAi9KV+cUCmWF5zOFy1x23P6PjdaRm
4T2zw4dxS5NswXWU0sVEXxjs6PYxuIiCTL7vdpx8QjBkrPWDrAbcMyBr2O
QlnHIvPzEArRQLo=
host = foo.example.org
interval = PT1M
# Client "bar"
[bar]
key_id = F90C7A81D72D1EA69A51031A91FF8885F36C8B46D155C8C58709A4C99AE9E361
fingerprint = 3e393aeaefb84c7e89e2f547b3a107558fca3a27
secfile = /etc/mandos/bar-secret
timeout = PT15M
approved_by_default = False
approval_delay = PT30S
SEE ALSO
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.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos-ctl 0000775 0001750 0001750 00000333506 14720643017 014145 0 ustar 00teddy teddy #!/usr/bin/python3 -bbI
# -*- coding: utf-8; lexical-binding: t -*-
#
# Mandos Control - Control or query the Mandos server
#
# Copyright © 2008-2022 Teddy Hogeborn
# Copyright © 2008-2022 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 unittest
import argparse
import logging
import os
import locale
import datetime
import re
import collections
import json
import io
import tempfile
import contextlib
if sys.version_info.major == 2:
__metaclass__ = type
str = unicode
input = raw_input
class gi:
"""Dummy gi module, for the tests"""
class repository:
class GLib:
class Error(Exception):
pass
dbussy = None
ravel = None
dbus_python = None
pydbus = None
try:
import dbussy
import ravel
except ImportError:
try:
import pydbus
import gi
except ImportError:
import dbus as dbus_python
# Show warnings by default
if not sys.warnoptions:
import warnings
warnings.simplefilter("default")
log = logging.getLogger(os.path.basename(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:
import StringIO
io.StringIO = StringIO.StringIO
locale.setlocale(locale.LC_ALL, "")
version = "1.8.18"
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:
logging.getLogger("").setLevel(logging.DEBUG)
if dbussy is not None and ravel is not None:
bus = dbussy_adapter.CachingBus(dbussy, ravel)
elif 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):
r"""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)
def call_method(self, methodname, busname, objectpath,
interface, *args):
raise NotImplementedError()
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
class dbussy_adapter:
class SystemBus(dbus.SystemBus):
"""Use DBussy"""
def __init__(self, dbussy, ravel):
self.dbussy = dbussy
self.ravel = ravel
self.bus = ravel.system_bus()
@contextlib.contextmanager
def convert_exception(self, exception_class=dbus.Error):
try:
yield
except self.dbussy.DBusError 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))
iface = proxy_object.get_interface(interface)
method = getattr(iface, methodname)
with self.convert_exception(dbus.Error):
value = method(*args)
# DBussy returns values either as an empty list or as a
# list of one element with the return value
if value:
return self.type_filter(value[0])
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[busname][objectpath]
def type_filter(self, value):
"""Convert the most bothersome types to Python types"""
# A D-Bus Variant value is represented as the Python type
# Tuple[dbussy.DBUS.Signature, Any]
if isinstance(value, tuple):
if (len(value) == 2
and isinstance(value[0],
self.dbussy.DBUS.Signature)):
return self.type_filter(value[1])
elif isinstance(value, self.dbussy.DBUS.ObjectPath):
return str(value)
# Also recurse into dictionaries
elif isinstance(value, dict):
return {self.type_filter(key):
self.type_filter(subval)
for key, subval in value.items()}
return value
def set_property(self, busname, objectpath, interface, key,
value):
proxy_object = self.get_object(busname, objectpath)
log.debug("D-Bus: %s:%s:%s.Set(%r, %r, %r)", busname,
objectpath, self.properties_iface, interface,
key, value)
if key == "Secret":
# DBussy wants a Byte Array to be a sequence of
# values, not a byte string
value = tuple(value)
setattr(proxy_object.get_interface(interface), key, value)
class MandosBus(SystemBus, dbus.MandosBus):
pass
class CachingBus(MandosBus):
"""A caching layer for dbussy_adapter.MandosBus"""
def __init__(self, *args, **kwargs):
self.object_cache = {}
super(dbussy_adapter.CachingBus, self).__init__(*args,
**kwargs)
def get_object(self, busname, objectpath):
try:
return self.object_cache[(busname, objectpath)]
except KeyError:
new_object = super(
dbussy_adapter.CachingBus,
self).get_object(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.exception, 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):
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.exception, 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_dbussy_adapter_SystemBus(TestCaseWithAssertLogs):
class dummy_dbussy:
class DBUS:
class ObjectPath(str):
pass
class DBusError(Exception):
pass
def fake_ravel_func(self, func):
class fake_ravel:
@staticmethod
def system_bus():
class DBusInterfaceProxy:
@staticmethod
def methodname(*args):
return [func(*args)]
class DBusObject:
@staticmethod
def get_interface(interface):
if interface == "interface":
return DBusInterfaceProxy()
return {"busname": {"objectpath": DBusObject()}}
return fake_ravel
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
fake_ravel = self.fake_ravel_func(func)
bus = dbussy_adapter.SystemBus(self.dummy_dbussy, fake_ravel)
ret = self.call_method(bus, "methodname", "busname",
"objectpath", "interface",
*method_args)
self.assertIs(ret, expected_method_return)
def test_call_method_filters_objectpath(self):
def func():
return method_return
fake_ravel = self.fake_ravel_func(func)
bus = dbussy_adapter.SystemBus(self.dummy_dbussy, fake_ravel)
method_return = (self.dummy_dbussy.DBUS
.ObjectPath("objectpath"))
ret = self.call_method(bus, "methodname", "busname",
"objectpath", "interface")
self.assertEqual("objectpath", ret)
self.assertNotIsInstance(ret,
self.dummy_dbussy.DBUS.ObjectPath)
def test_call_method_filters_objectpaths_in_dict(self):
ObjectPath = self.dummy_dbussy.DBUS.ObjectPath
def func():
return method_return
fake_ravel = self.fake_ravel_func(func)
bus = dbussy_adapter.SystemBus(self.dummy_dbussy, fake_ravel)
method_return = {
ObjectPath("objectpath_key_1"):
ObjectPath("objectpath_value_1"),
ObjectPath("objectpath_key_2"):
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()}
for key, value in ret.items():
self.assertNotIsInstance(key, ObjectPath)
self.assertNotIsInstance(value, ObjectPath)
self.assertEqual(expected_method_return, ret)
self.assertIsInstance(ret, dict)
def test_call_method_filters_objectpaths_in_dict_in_dict(self):
ObjectPath = self.dummy_dbussy.DBUS.ObjectPath
def func():
return method_return
fake_ravel = self.fake_ravel_func(func)
bus = dbussy_adapter.SystemBus(self.dummy_dbussy, fake_ravel)
method_return = {
ObjectPath("key1"): {
ObjectPath("key11"): ObjectPath("value11"),
ObjectPath("key12"): ObjectPath("value12"),
},
ObjectPath("key2"): {
ObjectPath("key21"): ObjectPath("value21"),
ObjectPath("key22"): ObjectPath("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)
for key, value in ret.items():
self.assertIsInstance(value, dict)
self.assertEqual(expected_method_return[key], value)
self.assertNotIsInstance(key, ObjectPath)
for inner_key, inner_value in value.items():
self.assertIsInstance(value, dict)
self.assertEqual(
expected_method_return[key][inner_key],
inner_value)
self.assertNotIsInstance(key, ObjectPath)
def test_call_method_filters_objectpaths_in_dict_three_deep(self):
ObjectPath = self.dummy_dbussy.DBUS.ObjectPath
def func():
return method_return
fake_ravel = self.fake_ravel_func(func)
bus = dbussy_adapter.SystemBus(self.dummy_dbussy, fake_ravel)
method_return = {
ObjectPath("key1"): {
ObjectPath("key2"): {
ObjectPath("key3"): ObjectPath("value"),
},
},
}
ret = self.call_method(bus, "methodname", "busname",
"objectpath", "interface")
expected_method_return = {"key1": {"key2": {"key3": "value"}}}
self.assertEqual(expected_method_return, ret)
self.assertIsInstance(ret, dict)
self.assertNotIsInstance(next(iter(ret.keys())), ObjectPath)
self.assertIsInstance(ret["key1"], dict)
self.assertNotIsInstance(next(iter(ret["key1"].keys())),
ObjectPath)
self.assertIsInstance(ret["key1"]["key2"], dict)
self.assertNotIsInstance(
next(iter(ret["key1"]["key2"].keys())),
ObjectPath)
self.assertEqual("value", ret["key1"]["key2"]["key3"])
self.assertNotIsInstance(ret["key1"]["key2"]["key3"],
self.dummy_dbussy.DBUS.ObjectPath)
def test_call_method_handles_exception(self):
def func():
raise self.dummy_dbussy.DBusError()
fake_ravel = self.fake_ravel_func(func)
bus = dbussy_adapter.SystemBus(self.dummy_dbussy, fake_ravel)
with self.assertRaises(dbus.Error) as e:
self.call_method(bus, "methodname", "busname",
"objectpath", "interface")
self.assertNotIsInstance(e.exception, dbus.ConnectFailed)
def test_get_object_converts_to_correct_exception(self):
class fake_ravel_raises_exception_on_connect:
@staticmethod
def system_bus():
class Bus:
@staticmethod
def __getitem__(key):
if key == "objectpath":
raise self.dummy_dbussy.DBusError()
raise Exception(key)
return {"busname": Bus()}
def func():
raise self.dummy_dbussy.DBusError()
bus = dbussy_adapter.SystemBus(
self.dummy_dbussy,
fake_ravel_raises_exception_on_connect)
with self.assertRaises(dbus.ConnectFailed):
self.call_method(bus, "methodname", "busname",
"objectpath", "interface")
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)
self.assertTrue(self.bus.clients)
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)
self.assertTrue(self.bus.clients)
for clientpath in self.bus.clients:
self.assertIn(("Approve", busname, clientpath,
client_interface, (False,)),
self.bus.calls)
def test_Remove(self):
busname = "se.recompile.Mandos"
server_path = "/"
server_interface = "se.recompile.Mandos"
orig_clients = self.bus.clients.copy()
command.Remove().run(self.bus.clients, self.bus)
self.assertFalse(self.bus.clients)
for clientpath in orig_clients:
self.assertIn(("RemoveClient", busname,
server_path, 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
self.assertTrue(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)
self.assertTrue(self.bus.clients)
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"
def __init__(self, *args, **kwargs):
self.values_to_set = [io.BytesIO(b""),
io.BytesIO(b"secret\0xyzzy\nbar")]
self.values_to_get = [f.getvalue() for f in
self.values_to_set]
super(TestSetSecretCmd, self).__init__(*args, **kwargs)
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 parse_test_args():
# type: () -> argparse.Namespace
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--check", action="store_true")
parser.add_argument("--prefix", )
args, unknown_args = parser.parse_known_args()
if args.check:
# Remove test options from sys.argv
sys.argv[1:] = unknown_args
return args
# Add all tests from doctest strings
def load_tests(loader, tests, none):
import doctest
tests.addTests(doctest.DocTestSuite())
return tests
if __name__ == "__main__":
options = parse_test_args()
try:
if options.check:
extra_test_prefix = options.prefix
if extra_test_prefix is not None:
if not (unittest.main(argv=[""], exit=False)
.result.wasSuccessful()):
sys.exit(1)
class ExtraTestLoader(unittest.TestLoader):
testMethodPrefix = extra_test_prefix
# Call using ./scriptname --check [--verbose]
unittest.main(argv=[""], testLoader=ExtraTestLoader())
else:
unittest.main(argv=[""])
else:
main()
finally:
logging.shutdown()
# Local Variables:
# run-tests:
# (lambda (&optional extra)
# (if (not (funcall run-tests-in-test-buffer default-directory
# extra))
# (funcall show-test-buffer-in-test-window)
# (funcall remove-test-window)
# (if extra (message "Extra tests run successfully!"))))
# run-tests-in-test-buffer:
# (lambda (dir &optional extra)
# (with-current-buffer (get-buffer-create "*Test*")
# (setq buffer-read-only nil
# default-directory dir)
# (erase-buffer)
# (compilation-mode))
# (let ((process-result
# (let ((inhibit-read-only t))
# (process-file-shell-command
# (funcall get-command-line extra) nil "*Test*"))))
# (and (numberp process-result)
# (= process-result 0))))
# get-command-line:
# (lambda (&optional extra)
# (let ((quoted-script
# (shell-quote-argument (funcall get-script-name))))
# (format
# (concat "%s --check" (if extra " --prefix=atest" ""))
# quoted-script)))
# get-script-name:
# (lambda ()
# (if (fboundp 'file-local-name)
# (file-local-name (buffer-file-name))
# (or (file-remote-p (buffer-file-name) 'localname)
# (buffer-file-name))))
# remove-test-window:
# (lambda ()
# (let ((test-window (get-buffer-window "*Test*")))
# (if test-window (delete-window test-window))))
# show-test-buffer-in-test-window:
# (lambda ()
# (when (not (get-buffer-window-list "*Test*"))
# (setq next-error-last-buffer (get-buffer "*Test*"))
# (let* ((side (if (>= (window-width) 146) 'right 'bottom))
# (display-buffer-overriding-action
# `((display-buffer-in-side-window) (side . ,side)
# (window-height . fit-window-to-buffer)
# (window-width . fit-window-to-buffer))))
# (display-buffer "*Test*"))))
# eval:
# (progn
# (let* ((run-extra-tests (lambda () (interactive)
# (funcall run-tests t)))
# (inner-keymap `(keymap (116 . ,run-extra-tests))) ; t
# (outer-keymap `(keymap (3 . ,inner-keymap)))) ; C-c
# (setq minor-mode-overriding-map-alist
# (cons `(run-tests . ,outer-keymap)
# minor-mode-overriding-map-alist)))
# (add-hook 'after-save-hook run-tests 90 t))
# End:
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos-ctl.xml 0000664 0001750 0001750 00000043536 14720643017 014742 0 ustar 00teddy teddy
%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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos-keygen 0000775 0001750 0001750 00000031610 14720643017 014634 0 ustar 00teddy teddy #!/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.18"
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
# The -q option is new in OpenSSH 9.8
for ssh_keyscan_quiet in "-q " ""; do
for ssh_keytype in ecdsa-sha2-nistp256 ed25519 rsa; do
set +e
ssh_fingerprint="`ssh-keyscan ${ssh_keyscan_quiet}-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 2
fi
done
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
printf "%s" "$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
if [ -n "$ssh_keyscan_quiet" ]; then
echo "# Note: if the Mandos server has OpenSSH older than 9.8, the ${ssh_keyscan_quiet}"
echo "# option *must* be removed from the 'checker' setting below"
fi
echo 'checker = ssh-keyscan '"$ssh_keyscan_quiet"'-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"
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos-keygen.xml 0000664 0001750 0001750 00000041575 14720643017 015443 0 ustar 00teddy teddy
%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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos-monitor 0000775 0001750 0001750 00000073036 14720643017 015051 0 ustar 00teddy teddy #!/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 logging
import os
import warnings
import datetime
import locale
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:
__metaclass__ = type
str = unicode
input = raw_input
# Show warnings by default
if not sys.warnoptions:
warnings.simplefilter("default")
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)
logging.getLogger("urwid").setLevel(logging.INFO)
# Some useful constants
domain = "se.recompile"
server_interface = domain + ".Mandos"
client_interface = domain + ".Mandos.Client"
version = "1.8.18"
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:
"""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(MandosClientPropertyCache, urwid.Widget):
"""A Mandos Client which is visible on the screen.
"""
_sizing = frozenset(["flow"])
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.Widget."""
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.Widget"""
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.Widget"""
return self.current_widget(focus).render(maxcolrow,
focus=focus)
def keypress(self, maxcolrow, key):
"""Handle keys.
This overrides the method from urwid.Widget"""
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:
"""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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos-monitor.xml 0000664 0001750 0001750 00000014567 14720643017 015651 0 ustar 00teddy teddy
%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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos-options.xml 0000664 0001750 0001750 00000012217 14720643017 015643 0 ustar 00teddy teddy
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.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos-to-cryptroot-unlock 0000775 0001750 0001750 00000005106 14720643017 017331 0 ustar 00teddy teddy #!/bin/sh
#
# Script to get password from plugin-runner to cryptroot-unlock
#
# Copyright © 2018-2019 Teddy Hogeborn
# Copyright © 2018-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 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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos.conf 0000664 0001750 0001750 00000003172 14720643017 014277 0 ustar 00teddy teddy # 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:!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
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos.conf.xml 0000664 0001750 0001750 00000021735 14720643017 015103 0 ustar 00teddy teddy
/etc/mandos/mandos.conf">
%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
&CONFNAME;
5
&CONFNAME;
Configuration file for the Mandos server
&CONFPATH;
DESCRIPTION
The file &CONFPATH; is a 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
FILES
The file described here is &CONFPATH;
BUGS
The [DEFAULT] is necessary because the Python
built-in module ConfigParser
requires it.
EXAMPLE
No options are actually required:
[DEFAULT]
An example using all the options:
[DEFAULT]
# A configuration example
interface = enp1s0
address = fe80::aede:48ff:fe71:f6f2
port = 1025
debug = True
priority = SECURE128:!CTYPE-X.509:+CTYPE-RAWPK:!RSA:!VERS-ALL:+VERS-TLS1.3:%PROFILE_ULTRA
servicename = Daena
use_dbus = False
use_ipv6 = True
restore = True
statedir = /var/lib/mandos
SEE ALSO
intro
8mandos,
gnutls_priority_init3,
mandos
8,
mandos-clients.conf
5
RFC 4291: IP Version 6 Addressing
Architecture
Section 2.2: Text Representation of
Addresses
Section 2.5.5.2: IPv4-Mapped IPv6
Address
Section 2.5.6, Link-Local IPv6 Unicast
Addresses
The clients use IPv6 link-local addresses, which are
immediately usable since a link-local addresses is
automatically assigned to a network interface when it
is brought up.
Zeroconf
Zeroconf is the network protocol standard used by clients
for finding the Mandos server on the local network.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos.lsm 0000664 0001750 0001750 00000001616 14720643017 014146 0 ustar 00teddy teddy Begin4
Title: Mandos
Version: 1.8.18
Entered-date: 2024-11-24
Description: The Mandos system allows computers to have encrypted
root file systems and at the same time be capable of
remote and/or unattended reboots.
Keywords: boot, encryption, luks, cryptsetup, network, openpgp,
tls, dm-crypt
Author: teddy@recompile.se (Teddy Hogeborn),
belorn@recompile.se (Björn Påhlsson)
Maintained-by: teddy@recompile.se (Teddy Hogeborn),
belorn@recompile.se (Björn Påhlsson)
Primary-site: https://www.recompile.se/mandos
241K mandos_1.8.18.orig.tar.gz
Alternate-site: ftp://ftp.recompile.se/pub/mandos
241K mandos_1.8.18.orig.tar.gz
Platforms: Requires GCC, GNU libC, Avahi, GnuPG, Python 2.7, and
various other libraries. While made for Debian
GNU/Linux, it is probably portable to other
distributions, but not other Unixes.
Copying-policy: GNU General Public License version 3.0 or later
End
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos.service 0000664 0001750 0001750 00000002420 14720643017 015005 0 ustar 00teddy teddy [Unit]
Description=Server of encrypted passwords to Mandos clients
Documentation=man:intro(8mandos) man:mandos(8)
## If the server is configured to listen to a specific IP or network
## interface, it may be necessary to change "network.target" to
## "network-online.target".
After=network.target
## If the server is configured to not use ZeroConf, these two lines
## become unnecessary and should be removed or commented out.
After=avahi-daemon.service
Requisite=avahi-daemon.service
[Service]
## If the server's D-Bus interface is disabled, the "BusName" setting
## should be removed or commented out.
BusName=se.recompile.Mandos
EnvironmentFile=/etc/default/mandos
ExecStart=/usr/sbin/mandos --foreground $DAEMON_ARGS
Restart=always
KillMode=mixed
## Using socket activation won't work, because systemd always does
## bind() on the socket, and also won't announce the ZeroConf service.
#ExecStart=/usr/sbin/mandos --foreground --socket=0
#StandardInput=socket
# Restrict what the Mandos daemon can do. Note that this also affects
# "checker" programs!
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=full
ProtectHome=yes
CapabilityBoundingSet=CAP_KILL CAP_SETGID CAP_SETUID CAP_DAC_OVERRIDE CAP_NET_RAW
ProtectKernelTunables=yes
ProtectControlGroups=yes
[Install]
WantedBy=multi-user.target
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/mandos.xml 0000664 0001750 0001750 00000060170 14720643017 014153 0 ustar 00teddy teddy
%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;
Gives encrypted passwords to authenticated Mandos clients
&COMMANDNAME;
&COMMANDNAME;
&COMMANDNAME;
&COMMANDNAME;
DESCRIPTION
&COMMANDNAME; is a server daemon which
handles incoming requests for passwords for a pre-defined list
of client host computers. For an introduction, see
intro
8mandos. The Mandos server
uses Zeroconf to announce itself on the local network, and uses
TLS to communicate securely with and to authenticate the
clients. The Mandos server uses IPv6 to allow Mandos clients to
use IPv6 link-local addresses, since the clients will probably
not have any other addresses configured (see ). Any authenticated client is then given
the stored pre-encrypted password for that specific client.
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
NAME
NAME
Run the server’s self-tests. This includes any unit
tests, etc.
Set the debugging log level.
LEVEL is a string, one of
CRITICAL
,
ERROR
,
WARNING
,
INFO
, or
DEBUG
, in order of
increasing verbosity. The default level is
WARNING
.
Directory to search for configuration files. Default is
/etc/mandos
. See
mandos.conf
5 and
mandos-clients.conf
5.
Prints the program version and exit.
See also .
See also .
OVERVIEW
This program is the server part. It is a normal server program
and will run in a normal system environment, not in an initial
RAM disk environment.
NETWORK PROTOCOL
The Mandos server announces itself as a Zeroconf service of type
_mandos._tcp
. The Mandos
client connects to the announced address and port, and sends a
line of text where the first whitespace-separated field is the
protocol version, which currently is
1
. The client and server then
start a TLS protocol handshake with a slight quirk: the Mandos
server program acts as a TLS client
while the
connecting Mandos client acts as a TLS server
.
The Mandos client must supply a TLS public key, and the key ID
of this public key is used by the Mandos server to look up (in a
list read from clients.conf at start time)
which binary blob to give the client. No other authentication
or authorization is done by the server.
Mandos Protocol (Version 1)
Mandos Client
Direction
Mandos Server
Connect
->
1\r\n
->
TLS handshake as TLS server
<->
TLS handshake as TLS client
Public key (part of TLS handshake)
->
<-
Binary blob (client will assume OpenPGP data)
<-
Close
CHECKING
The server will, by default, continually check that the clients
are still up. If a client has not been confirmed as being up
for some time, the client is assumed to be compromised and is no
longer eligible to receive the encrypted password. (Manual
intervention is required to re-enable a client.) The timeout,
extended timeout, checker program, and interval between checks
can be configured both globally and per client; see
mandos-clients.conf
5.
APPROVAL
The server can be configured to require manual approval for a
client before it is sent its secret. The delay to wait for such
approval and the default action (approve or deny) can be
configured both globally and per client; see
mandos-clients.conf
5. By default all clients
will be approved immediately without delay.
This can be used to deny a client its secret if not manually
approved within a specified time. It can also be used to make
the server delay before giving a client its secret, allowing
optional manual denying of this specific client.
LOGGING
The server will send log message with various severity levels to
/dev/log. With the
option, it will log even more messages,
and also show them on the console.
PERSISTENT STATE
Client settings, initially read from
clients.conf, are persistent across
restarts, and run-time changes will override settings in
clients.conf. However, if a setting is
changed (or a client added, or removed) in
clients.conf, this will take precedence.
D-BUS INTERFACE
The server will by default provide a D-Bus system bus interface.
This interface will only be accessible by the root user or a
Mandos-specific user, if such a user exists. For documentation
of the D-Bus API, see the file DBUS-API.
EXIT STATUS
The server will exit with a non-zero exit status only when a
critical error is encountered.
ENVIRONMENT
PATH
To start the configured checker (see ), the server uses
/bin/sh, which in turn uses
PATH to search for matching commands if
an absolute path is not given. See
sh1
.
FILES
Use the option to change where
&COMMANDNAME; looks for its configurations
files. The default file names are listed here.
/etc/mandos/mandos.conf
Server-global settings. See
mandos.conf
5 for details.
/etc/mandos/clients.conf
List of clients and client-specific settings. See
mandos-clients.conf
5 for details.
/run/mandos.pid
The file containing the process id of the
&COMMANDNAME; process started last.
Note: If the /run directory does not
exist, /var/run/mandos.pid will be
used instead.
/var/lib/mandos
Directory where persistent state will be saved. Change
this with the option. See
also the option.
/dev/log
The Unix domain socket to where local syslog messages are
sent.
/bin/sh
This is used to start the configured checker command for
each client. See
mandos-clients.conf
5 for details.
BUGS
This server might, on especially fatal errors, emit a Python
backtrace. This could be considered a feature.
There is no fine-grained control over logging and debug output.
EXAMPLE
Normal invocation needs no options:
&COMMANDNAME;
Run the server in debug mode, read configuration files from
the ~/mandos directory,
and use the Zeroconf service name Test
to not
collide with any other official Mandos server on this host:
&COMMANDNAME; --debug --configdir ~/mandos --servicename Test
Run the server normally, but only listen to one interface and
only on the link-local address on that interface:
&COMMANDNAME; --interface eth7 --address fe80::aede:48ff:fe71:f6f2
SECURITY
SERVER
Running this &COMMANDNAME; server program
should not in itself present any security risk to the host
computer running it. The program switches to a non-root user
soon after startup.
CLIENTS
The server only gives out its stored data to clients which
does have the correct key ID of the stored key ID. This is
guaranteed by the fact that the client sends its public key in
the TLS handshake; this ensures it to be genuine. The server
computes the key ID of the key itself and looks up the key ID
in its list of clients. The clients.conf
file (see
mandos-clients.conf
5)
must be made non-readable by anyone
except the user starting the server (usually root).
As detailed in , the status of all
client computers will continually be checked and be assumed
compromised if they are gone for too long.
For more details on client-side security, see
mandos-client
8mandos.
SEE ALSO
intro
8mandos,
mandos-clients.conf
5,
mandos.conf
5,
mandos-client
8mandos,
sh
1
Zeroconf
Zeroconf is the network protocol standard used by clients
for finding this Mandos server on the local network.
Avahi
Avahi is the library this server calls to implement
Zeroconf service announcements.
GnuTLS
GnuTLS is the library this server uses to implement TLS for
communicating securely with the client, and at the same time
confidently get the client’s public key.
RFC 4291: IP Version 6 Addressing
Architecture
Section 2.2: Text Representation of
Addresses
Section 2.5.5.2: IPv4-Mapped IPv6
Address
Section 2.5.6, Link-Local IPv6 Unicast
Addresses
The clients use IPv6 link-local addresses, which are
immediately usable since a link-local address is
automatically assigned to a network interfaces when it
is brought up.
RFC 5246: The Transport Layer Security (TLS)
Protocol Version 1.2
TLS 1.2 is the protocol implemented by GnuTLS.
RFC 4880: OpenPGP Message Format
The data sent to clients is binary encrypted OpenPGP data.
RFC 7250: Using Raw Public Keys in Transport
Layer Security (TLS) and Datagram Transport Layer Security
(DTLS)
This is implemented by GnuTLS version 3.6.6 and is, if
present, used by this server so that raw public keys can be
used.
RFC 6091: Using OpenPGP Keys for Transport Layer
Security (TLS) Authentication
This is implemented by GnuTLS before version 3.6.0 and is,
if present, used by this server so that OpenPGP keys can be
used.
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1732462094.522593
mandos-1.8.18/network-hooks.d/ 0000775 0001750 0001750 00000000000 14720643017 015200 5 ustar 00teddy teddy ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/network-hooks.d/bridge 0000775 0001750 0001750 00000004062 14720643017 016364 0 ustar 00teddy teddy #!/bin/sh
#
# This is an example of a Mandos client network hook. This hook
# brings up a bridge interface as specified in a separate
# configuration file. To be used, this file and any needed
# configuration file(s) should be copied into the
# /etc/mandos/network-hooks.d directory.
#
# Copyright © 2012-2018 Teddy Hogeborn
# Copyright © 2012-2018 Björn Påhlsson
#
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved. This file is offered as-is,
# without any warranty.
set -e
CONFIG="$MANDOSNETHOOKDIR/bridge.conf"
addrtoif(){
grep -liFe "$1" /sys/class/net/*/address \
| sed -e 's,.*/\([^/]*\)/[^/]*,\1,' -e "/^${BRIDGE}\$/d"
}
# Read config file, which must set "BRIDGE", "PORT_ADDRESSES", and
# optionally "IPADDRS" and "ROUTES".
if [ -e "$CONFIG" ]; then
. "$CONFIG"
fi
if [ -z "$BRIDGE" ] || [ -z "$PORT_ADDRESSES" ]; then
exit
fi
if [ -n "$DEVICE" ]; then
case "$DEVICE" in
*,"$BRIDGE"|*,"$BRIDGE",*|"$BRIDGE",*|"$BRIDGE") :;;
*) exit;;
esac
fi
brctl="/sbin/brctl"
for b in "$brctl" /usr/sbin/brctl; do
if [ -e "$b" ]; then
brctl="$b"
break
fi
done
do_start(){
"$brctl" addbr "$BRIDGE"
for address in $PORT_ADDRESSES; do
interface=`addrtoif "$address"`
"$brctl" addif "$BRIDGE" "$interface"
ip link set dev "$interface" up
done
ip link set dev "$BRIDGE" up
sleep "${DELAY%%.*}"
if [ -n "$IPADDRS" ]; then
for ipaddr in $IPADDRS; do
ip addr add "$ipaddr" dev "$BRIDGE"
done
fi
if [ -n "$ROUTES" ]; then
for route in $ROUTES; do
ip route add "$route" dev "$BRIDGE"
done
fi
}
do_stop(){
ip link set dev "$BRIDGE" down
for address in $PORT_ADDRESSES; do
interface=`addrtoif "$address"`
ip link set dev "$interface" down
"$brctl" delif "$BRIDGE" "$interface"
done
"$brctl" delbr "$BRIDGE"
}
case "${MODE:-$1}" in
start|stop)
do_"${MODE:-$1}"
;;
files)
echo /bin/ip
echo "$brctl"
;;
modules)
echo bridge
;;
esac
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/network-hooks.d/bridge.conf 0000664 0001750 0001750 00000000275 14720643017 017307 0 ustar 00teddy teddy ## Required
#BRIDGE=br0
#PORT_ADDRESSES="00:11:22:33:44:55 11:22:33:44:55:66"
## Optional
#IPADDRS="192.0.2.3/24 2001:DB8::aede:48ff:fe71:f6f2/32"
#ROUTES="192.0.2.0/24 2001:DB8::/32"
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/network-hooks.d/openvpn 0000775 0001750 0001750 00000002714 14720643017 016617 0 ustar 00teddy teddy #!/bin/sh
#
# This is an example of a Mandos client network hook. This hook
# brings up an OpenVPN interface as specified in a separate
# configuration file. To be used, this file and any needed
# configuration file(s) should be copied into the
# /etc/mandos/network-hooks.d directory.
#
# Copyright © 2012-2018 Teddy Hogeborn
# Copyright © 2012-2018 Björn Påhlsson
#
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved. This file is offered as-is,
# without any warranty.
set -e
CONFIG="openvpn.conf"
# Extract the "dev" setting from the config file
VPNDEVICE=`sed -n -e 's/[[:space:]]#.*//' \
-e 's/^[[:space:]]*dev[[:space:]]\+//p' \
"$MANDOSNETHOOKDIR/$CONFIG"`
PIDFILE=/run/openvpn-mandos.pid
# Exit if no device set in config
if [ -z "$VPNDEVICE" ]; then
exit
fi
# Exit if DEVICE is set and it doesn't match the VPN interface
if [ -n "$DEVICE" ]; then
case "$DEVICE" in
*,"$VPNDEVICE"*|"$VPNDEVICE"*) :;;
*) exit;;
esac
fi
openvpn=/usr/sbin/openvpn
do_start(){
"$openvpn" --cd "$MANDOSNETHOOKDIR" --daemon 'openvpn(Mandos)' \
--writepid "$PIDFILE" --config "$CONFIG"
sleep "$DELAY"
}
do_stop(){
PID="`cat \"$PIDFILE\"`"
if [ "$PID" -gt 0 ]; then
kill "$PID"
fi
}
case "${MODE:-$1}" in
start|stop)
do_"${MODE:-$1}"
;;
files)
echo "$openvpn"
;;
modules)
echo tun
;;
esac
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/network-hooks.d/openvpn.conf 0000664 0001750 0001750 00000000443 14720643017 017535 0 ustar 00teddy teddy # Sample OpenVPN configuration file
# Uncomment and change - see openvpn(8)
# Network device.
#dev tun
# Our remote peer
#remote 192.0.2.3
#float 192.0.2.3
#port 1194
# VPN endpoints
#ifconfig 10.1.0.1 10.1.0.2
# A pre-shared static key
#secret openvpn.key
# Cipher
#cipher AES-128-CBC
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/network-hooks.d/wireless 0000775 0001750 0001750 00000010053 14720643017 016762 0 ustar 00teddy teddy #!/bin/sh
#
# This is an example of a Mandos client network hook. This hook
# brings up a wireless interface as specified in a separate
# configuration file. To be used, this file and any needed
# configuration file(s) should be copied into the
# /etc/mandos/network-hooks.d directory.
#
# Copyright © 2012-2018 Teddy Hogeborn
# Copyright © 2012-2018 Björn Påhlsson
#
# Copying and distribution of this file, with or without modification,
# are permitted in any medium without royalty provided the copyright
# notice and this notice are preserved. This file is offered as-is,
# without any warranty.
set -e
RUNDIR="/run"
CTRL="$RUNDIR/wpa_supplicant-global"
CTRLDIR="$RUNDIR/wpa_supplicant"
PIDFILE="$RUNDIR/wpa_supplicant-mandos.pid"
CONFIG="$MANDOSNETHOOKDIR/wireless.conf"
addrtoif(){
grep -liFe "$1" /sys/class/net/*/address \
| sed -e 's,.*/\([^/]*\)/[^/]*,\1,'
}
# Read config file
if [ -e "$CONFIG" ]; then
. "$CONFIG"
else
exit
fi
ifkeys=`sed -n -e 's/^ADDRESS_\([^=]*\)=.*/\1/p' "$CONFIG" | sort -u`
# Exit if DEVICE is set and is not any of the wireless interfaces
if [ -n "$DEVICE" ]; then
while :; do
for KEY in $ifkeys; do
ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"`
INTERFACE=`addrtoif "$ADDRESS"`
case "$DEVICE" in
*,"$INTERFACE"|*,"$INTERFACE",*|"$INTERFACE",*|"$INTERFACE")
break 2;;
esac
done
exit
done
fi
wpa_supplicant=/sbin/wpa_supplicant
wpa_cli=/sbin/wpa_cli
ip=/bin/ip
# Used by the wpa_interface_* functions in the wireless.conf file
wpa_cli_set(){
case "$1" in
ssid|psk) arg="\"$2\"" ;;
*) arg="$2" ;;
esac
"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" set_network "$NETWORK" \
"$1" "$arg" 2>&1 | sed -e '/^OK$/d'
}
if [ $VERBOSITY -gt 0 ]; then
WPAS_OPTIONS="-d $WPAS_OPTIONS"
fi
if [ -n "$PIDFILE" ]; then
WPAS_OPTIONS="-P$PIDFILE $WPAS_OPTIONS"
fi
do_start(){
mkdir -m u=rwx,go= -p "$CTRLDIR"
"$wpa_supplicant" -B -g "$CTRL" -p "$CTRLDIR" $WPAS_OPTIONS
for KEY in $ifkeys; do
ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"`
INTERFACE=`addrtoif "$ADDRESS"`
DRIVER=`eval 'echo "$WPA_DRIVER_'"$KEY"\"`
IFDELAY=`eval 'echo "$DELAY_'"$KEY"\"`
"$wpa_cli" -g "$CTRL" interface_add "$INTERFACE" "" \
"${DRIVER:-wext}" "$CTRLDIR" > /dev/null \
| sed -e '/^OK$/d'
NETWORK=`"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" add_network`
eval wpa_interface_"$KEY"
"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" enable_network \
"$NETWORK" | sed -e '/^OK$/d'
sleep "${IFDELAY:-$DELAY}" &
sleep=$!
while :; do
kill -0 $sleep 2>/dev/null || break
STATE=`"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" status \
| sed -n -e 's/^wpa_state=//p'`
if [ "$STATE" = COMPLETED ]; then
while :; do
kill -0 $sleep 2>/dev/null || break 2
UP=`cat /sys/class/net/"$INTERFACE"/operstate`
if [ "$UP" = up ]; then
kill $sleep 2>/dev/null
break 2
fi
sleep 1
done
fi
sleep 1
done &
wait $sleep || :
IPADDRS=`eval 'echo "$IPADDRS_'"$KEY"\"`
if [ -n "$IPADDRS" ]; then
if [ "$IPADDRS" = dhcp ]; then
ipconfig -c dhcp -d "$INTERFACE" || :
#dhclient "$INTERFACE"
else
for ipaddr in $IPADDRS; do
"$ip" addr add "$ipaddr" dev "$INTERFACE"
done
fi
fi
ROUTES=`eval 'echo "$ROUTES_'"$KEY"\"`
if [ -n "$ROUTES" ]; then
for route in $ROUTES; do
"$ip" route add "$route" dev "$INTERFACE"
done
fi
done
}
do_stop(){
"$wpa_cli" -g "$CTRL" terminate 2>&1 | sed -e '/^OK$/d'
for KEY in $ifkeys; do
ADDRESS=`eval 'echo "$ADDRESS_'"$KEY"\"`
INTERFACE=`addrtoif "$ADDRESS"`
"$ip" addr show scope global permanent dev "$INTERFACE" \
| while read type addr rest; do
case "$type" in
inet|inet6)
"$ip" addr del "$addr" dev "$INTERFACE"
;;
esac
done
"$ip" link set dev "$INTERFACE" down
done
}
case "${MODE:-$1}" in
start|stop)
do_"${MODE:-$1}"
;;
files)
echo "$wpa_supplicant"
echo "$wpa_cli"
echo "$ip"
;;
modules)
if [ "$IPADDRS" = dhcp ]; then
echo af_packet
fi
sed -n -e 's/#.*$//' -e 's/[ ]*$//' \
-e 's/^MODULE_[^=]\+=//p' "$CONFIG"
;;
esac
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/network-hooks.d/wireless.conf 0000664 0001750 0001750 00000001143 14720643017 017703 0 ustar 00teddy teddy # Extra options for wpa_supplicant, if any
#WPAS_OPTIONS=""
# wlan0
ADDRESS_0=00:11:22:33:44:55
MODULE_0=ath9k
#WPA_DRIVER_0=wext
wpa_interface_0(){
# Use this format to set simple things:
wpa_cli_set ssid home
wpa_cli_set psk "secret passphrase"
# Use this format to do more complex things with wpa_cli:
#"$wpa_cli" -p "$CTRLDIR" -i "$INTERFACE" bssid "$NETWORK" 00:11:22:33:44:55
#"$wpa_cli" -g "$CTRL" ping
}
#DELAY_0=10
IPADDRS_0=dhcp
#IPADDRS_0="192.0.2.3/24 2001:DB8::aede:48ff:fe71:f6f2/32"
#ROUTES_0="192.0.2.0/24 2001:DB8::/32"
#ADDRESS_1=11:22:33:44:55:66
#MODULE_1=...
#...
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/overview.xml 0000664 0001750 0001750 00000001633 14720643017 014537 0 ustar 00teddy teddy
This is part of the Mandos system for allowing 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 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.
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1732462094.522593
mandos-1.8.18/plugin-helpers/ 0000775 0001750 0001750 00000000000 14720643017 015102 5 ustar 00teddy teddy ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugin-helpers/mandos-client-iprouteadddel.c 0000664 0001750 0001750 00000021227 14720643017 022632 0 ustar 00teddy teddy /* -*- coding: utf-8 -*- */
/*
* iprouteadddel - Add or delete direct route to a local IP address
*
* Copyright © 2015-2018, 2021-2022 Teddy Hogeborn
* Copyright © 2015-2018, 2021-2022 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 /* program_invocation_short_name */
#include /* bool, false, true */
#include /* argp_program_version,
argp_program_bug_address,
struct argp_option,
struct argp_state, ARGP_KEY_ARG,
argp_usage(), ARGP_KEY_END,
ARGP_ERR_UNKNOWN, struct argp,
argp_parse(), ARGP_IN_ORDER */
#include /* errno,
program_invocation_short_name,
error_t, EINVAL, ENOMEM */
#include /* fprintf(), stderr, perror(), FILE,
vfprintf() */
#include /* va_list, va_start(), vfprintf() */
#include /* EXIT_SUCCESS */
#include /* struct nl_addr, nl_addr_parse(),
nl_geterror(),
nl_addr_get_family(), NLM_F_EXCL,
nl_addr_put() */
#include /* NULL */
#include /* struct rtnl_route,
struct rtnl_nexthop, NETLINK_ROUTE,
rtnl_route_alloc(),
rtnl_route_set_family(),
rtnl_route_set_protocol(),
RTPROT_BOOT,
rtnl_route_set_scope(),
RT_SCOPE_LINK,
rtnl_route_set_type(), RTN_UNICAST,
rtnl_route_set_dst(),
rtnl_route_set_table(),
RT_TABLE_MAIN,
rtnl_route_nh_alloc(),
rtnl_route_nh_set_ifindex(),
rtnl_route_add_nexthop(),
rtnl_route_add(),
rtnl_route_delete(),
rtnl_route_put(),
rtnl_route_nh_free() */
#include /* struct nl_sock, nl_socket_alloc(),
nl_connect(), nl_socket_free() */
#include /* strcasecmp() */
#include /* AF_UNSPEC, AF_INET6, AF_INET */
#include /* EX_USAGE, EX_OSERR */
#include /* struct rtnl_link,
rtnl_link_get_kernel(),
rtnl_link_get_ifindex(),
rtnl_link_put() */
#include /* sa_family_t */
#include /* PRIdMAX, intmax_t */
#include /* uint8_t */
bool debug = false;
const char *argp_program_version = "mandos-client-iprouteadddel " VERSION;
const char *argp_program_bug_address = "";
/* Function to use when printing errors */
void perror_plus(const char *print_text){
int e = errno;
fprintf(stderr, "Mandos plugin helper %s: ",
program_invocation_short_name);
errno = e;
perror(print_text);
}
__attribute__((format (gnu_printf, 2, 3), nonnull))
int fprintf_plus(FILE *stream, const char *format, ...){
va_list ap;
va_start(ap, format);
fprintf(stream, "Mandos plugin helper %s: ",
program_invocation_short_name);
return vfprintf(stream, format, ap);
}
int main(int argc, char *argv[]){
int ret;
int exitcode = EXIT_SUCCESS;
struct arguments {
bool add; /* true: add, false: delete */
char *address; /* IP address as string */
struct nl_addr *nl_addr; /* Netlink IP address */
char *interface; /* interface name */
} arguments = { .add = true, .address = NULL, .interface = NULL };
struct argp_option options[] = {
{ .name = "debug", .key = 128,
.doc = "Debug mode" },
{ .name = NULL }
};
struct rtnl_route *route = NULL;
struct rtnl_nexthop *nexthop = NULL;
struct nl_sock *sk = NULL;
error_t parse_opt(int key, char *arg, struct argp_state *state){
int lret;
errno = 0;
switch(key){
case 128: /* --debug */
debug = true;
break;
case ARGP_KEY_ARG:
switch(state->arg_num){
case 0:
if(strcasecmp(arg, "add") == 0){
((struct arguments *)(state->input))->add = true;
} else if(strcasecmp(arg, "delete") == 0){
((struct arguments *)(state->input))->add = false;
} else {
fprintf_plus(stderr, "Unrecognized command: %s\n", arg);
argp_usage(state);
}
break;
case 1:
((struct arguments *)(state->input))->address = arg;
lret = nl_addr_parse(arg, AF_UNSPEC, &(((struct arguments *)
(state->input))
->nl_addr));
if(lret != 0){
fprintf_plus(stderr, "Failed to parse address %s: %s\n",
arg, nl_geterror(lret));
argp_usage(state);
}
break;
case 2:
((struct arguments *)(state->input))->interface = arg;
break;
default:
argp_usage(state);
}
break;
case ARGP_KEY_END:
if(state->arg_num < 3){
argp_usage(state);
}
break;
default:
return ARGP_ERR_UNKNOWN;
}
return errno;
}
struct argp argp = { .options = options, .parser = parse_opt,
.args_doc = "[ add | delete ] ADDRESS INTERFACE",
.doc = "Mandos client helper -- Add or delete"
" local route to IP address on interface" };
ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, 0, &arguments);
switch(ret){
case 0:
break;
case EINVAL:
exit(EX_USAGE);
case ENOMEM:
default:
errno = ret;
perror_plus("argp_parse");
exitcode = EX_OSERR;
goto end;
}
/* Get netlink socket */
sk = nl_socket_alloc();
if(sk == NULL){
fprintf_plus(stderr, "Failed to allocate netlink socket: %s\n",
nl_geterror(ret));
exitcode = EX_OSERR;
goto end;
}
/* Connect socket to netlink */
ret = nl_connect(sk, NETLINK_ROUTE);
if(ret < 0){
fprintf_plus(stderr, "Failed to connect socket to netlink: %s\n",
nl_geterror(ret));
exitcode = EX_OSERR;
goto end;
}
/* Get link object of specified interface */
struct rtnl_link *link = NULL;
ret = rtnl_link_get_kernel(sk, 0, arguments.interface, &link);
if(ret < 0){
fprintf_plus(stderr, "Failed to use interface %s: %s\n",
arguments.interface, nl_geterror(ret));
exitcode = EX_OSERR;
goto end;
}
/* Get netlink route object */
route = rtnl_route_alloc();
if(route == NULL){
fprintf_plus(stderr, "Failed to get netlink route:\n");
exitcode = EX_OSERR;
goto end;
}
/* Get address family of specified address */
sa_family_t af = (sa_family_t)nl_addr_get_family(arguments.nl_addr);
if(debug){
fprintf_plus(stderr, "Address family of %s is %s (%" PRIdMAX
")\n", arguments.address,
af == AF_INET6 ? "AF_INET6" :
( af == AF_INET ? "AF_INET" : "UNKNOWN"),
(intmax_t)af);
}
/* Set route parameters: */
rtnl_route_set_family(route, (uint8_t)af); /* Address family */
rtnl_route_set_protocol(route, RTPROT_BOOT); /* protocol - see
ip-route(8) */
rtnl_route_set_scope(route, RT_SCOPE_LINK); /* link scope */
rtnl_route_set_type(route, RTN_UNICAST); /* normal unicast
address route */
rtnl_route_set_dst(route, arguments.nl_addr); /* Destination
address */
rtnl_route_set_table(route, RT_TABLE_MAIN); /* "main" routing
table */
/* Create nexthop */
nexthop = rtnl_route_nh_alloc();
if(nexthop == NULL){
fprintf_plus(stderr, "Failed to get netlink route nexthop\n");
exitcode = EX_OSERR;
goto end;
}
/* Get index number of specified interface */
int ifindex = rtnl_link_get_ifindex(link);
if(debug){
fprintf_plus(stderr, "ifindex of %s is %d\n", arguments.interface,
ifindex);
}
/* Set interface index number on nexthop object */
rtnl_route_nh_set_ifindex(nexthop, ifindex);
/* Set route to use nexthop object */
rtnl_route_add_nexthop(route, nexthop);
/* Add or delete route? */
if(arguments.add){
ret = rtnl_route_add(sk, route, NLM_F_EXCL);
} else {
ret = rtnl_route_delete(sk, route, 0);
}
if(ret < 0){
fprintf_plus(stderr, "Failed to %s route: %s\n",
arguments.add ? "add" : "delete",
nl_geterror(ret));
exitcode = EX_OSERR;
goto end;
}
end:
/* Deallocate route */
if(route){
rtnl_route_put(route);
} else if(nexthop) {
/* Deallocate route nexthop */
rtnl_route_nh_free(nexthop);
}
/* Deallocate parsed address */
if(arguments.nl_addr){
nl_addr_put(arguments.nl_addr);
}
/* Deallocate link struct */
if(link){
rtnl_link_put(link);
}
/* Deallocate netlink socket struct */
if(sk){
nl_socket_free(sk);
}
return exitcode;
}
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugin-runner.c 0000664 0001750 0001750 00000110631 14720643017 015117 0 ustar 00teddy teddy /* -*- coding: utf-8; mode: c; mode: orgtbl -*- */
/*
* Mandos plugin runner - Run Mandos plugins
*
* Copyright © 2008-2022 Teddy Hogeborn
* Copyright © 2008-2022 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 /* strchrnul(), TEMP_FAILURE_RETRY(),
getline(), asprintf(), O_CLOEXEC,
scandirat(), pipe2() */
#include /* argp_program_version,
argp_program_bug_address,
struct argp_option,
struct argp_state, argp_error(),
ARGP_NO_EXIT, argp_state_help,
ARGP_HELP_STD_HELP,
ARGP_HELP_USAGE, ARGP_HELP_EXIT_OK,
ARGP_KEY_ARG, ARGP_ERR_UNKNOWN,
struct argp, argp_parse(),
ARGP_IN_ORDER, ARGP_NO_HELP */
#include /* bool, false, true */
#include /* pid_t, sig_atomic_t, uid_t, gid_t,
getuid(), setgid(), setuid() */
#include /* size_t, NULL */
#include /* or, and, not */
#include /* strcmp(), strdup(), strchrnul(),
strncmp(), strlen(), strcpy(),
strsep(), strchr(), strsignal() */
#include /* malloc(), free(), reallocarray(),
realloc(), EXIT_SUCCESS */
#include /* errno, EINTR, ENOMEM, ECHILD,
error_t, EINVAL, EMFILE, ENFILE,
ENOENT, ESRCH */
#include /* SIZE_MAX */
#define _GNU_SOURCE /* strchrnul(), TEMP_FAILURE_RETRY(),
getline(), asprintf(), O_CLOEXEC,
scandirat(), pipe2() */
#include /* TEMP_FAILURE_RETRY(), ssize_t,
write(), STDOUT_FILENO, uid_t,
gid_t, getuid(), fchown(), close(),
symlink(), setgid(), setuid(),
faccessat(), X_OK, pipe(), pipe2(),
fork(), _exit(), dup2(), fexecve(),
read(), getpass() */
#include /* fcntl(), F_GETFD, F_SETFD,
FD_CLOEXEC, open(), O_RDONLY,
O_CLOEXEC, openat() */
#include /* waitpid(), WNOHANG, WIFEXITED(),
WEXITSTATUS(), WIFSIGNALED(),
WTERMSIG(), wait() */
#include /* error() */
#include /* FILE, fprintf(), fopen(),
getline(), fclose(), EOF,
asprintf(), stderr */
#include /* struct dirent, scandirat(),
alphasort() */
#include /* struct stat, fstat(), S_ISDIR(),
lstat(), S_ISREG() */
#include /* fd_set, FD_ZERO(), FD_SETSIZE,
FD_SET(), select(), FD_CLR(),
FD_ISSET() */
#include /* struct sigaction, SA_NOCLDSTOP,
sigemptyset(), sigaddset(),
SIGCHLD, sigprocmask(), SIG_BLOCK,
SIG_UNBLOCK, kill(), SIGTERM */
#include /* EX_OSERR, EX_USAGE, EX_IOERR,
EX_CONFIG, EX_UNAVAILABLE, EX_OK */
#include /* intmax_t, strtoimax(), PRIdMAX */
#include /* fnmatch(), FNM_FILE_NAME,
FNM_PERIOD, FNM_NOMATCH */
#define BUFFER_SIZE 256
#define PDIR "/lib/mandos/plugins.d"
#define PHDIR "/lib/mandos/plugin-helpers"
#define AFILE "/conf/conf.d/mandos/plugin-runner.conf"
const char *argp_program_version = "plugin-runner " VERSION;
const char *argp_program_bug_address = "";
typedef struct plugin{
char *name; /* can be NULL or any plugin name */
char **argv;
int argc;
char **environ;
int envc;
bool disabled;
/* Variables used for running processes*/
pid_t pid;
int fd;
char *buffer;
size_t buffer_size;
size_t buffer_length;
bool eof;
volatile sig_atomic_t completed;
int status;
struct plugin *next;
} plugin;
static plugin *plugin_list = NULL;
/* Gets an existing plugin based on name,
or if none is found, creates a new one */
__attribute__((warn_unused_result))
static plugin *getplugin(char *name){
/* Check for existing plugin with that name */
for(plugin *p = plugin_list; p != NULL; p = p->next){
if((p->name == name)
or (p->name and name and (strcmp(p->name, name) == 0))){
return p;
}
}
/* Create a new plugin */
plugin *new_plugin = NULL;
do {
new_plugin = malloc(sizeof(plugin));
} while(new_plugin == NULL and errno == EINTR);
if(new_plugin == NULL){
return NULL;
}
char *copy_name = NULL;
if(name != NULL){
do {
copy_name = strdup(name);
} while(copy_name == NULL and errno == EINTR);
if(copy_name == NULL){
int e = errno;
free(new_plugin);
errno = e;
return NULL;
}
}
*new_plugin = (plugin){ .name = copy_name,
.argc = 1,
.disabled = false,
.next = plugin_list };
do {
new_plugin->argv = malloc(sizeof(char *) * 2);
} while(new_plugin->argv == NULL and errno == EINTR);
if(new_plugin->argv == NULL){
int e = errno;
free(copy_name);
free(new_plugin);
errno = e;
return NULL;
}
new_plugin->argv[0] = copy_name;
new_plugin->argv[1] = NULL;
do {
new_plugin->environ = malloc(sizeof(char *));
} while(new_plugin->environ == NULL and errno == EINTR);
if(new_plugin->environ == NULL){
int e = errno;
free(copy_name);
free(new_plugin->argv);
free(new_plugin);
errno = e;
return NULL;
}
new_plugin->environ[0] = NULL;
/* Append the new plugin to the list */
plugin_list = new_plugin;
return new_plugin;
}
/* Helper function for add_argument and add_environment */
__attribute__((nonnull, warn_unused_result))
static bool add_to_char_array(const char *new, char ***array,
int *len){
/* Resize the pointed-to array to hold one more pointer */
char **new_array = NULL;
do {
#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 26)
new_array = reallocarray(*array, (size_t)((*len) + 2),
sizeof(char *));
#else
if(((size_t)((*len) + 2)) > (SIZE_MAX / sizeof(char *))){
/* overflow */
new_array = NULL;
errno = ENOMEM;
} else {
new_array = realloc(*array, (size_t)((*len) + 2)
* sizeof(char *));
}
#endif
} while(new_array == NULL and errno == EINTR);
/* Malloc check */
if(new_array == NULL){
return false;
}
*array = new_array;
/* Make a copy of the new string */
char *copy;
do {
copy = strdup(new);
} while(copy == NULL and errno == EINTR);
if(copy == NULL){
return false;
}
/* Insert the copy */
(*array)[*len] = copy;
(*len)++;
/* Add a new terminating NULL pointer to the last element */
(*array)[*len] = NULL;
return true;
}
/* Add to a plugin's argument vector */
__attribute__((nonnull(2), warn_unused_result))
static bool add_argument(plugin *p, const char *arg){
if(p == NULL){
return false;
}
return add_to_char_array(arg, &(p->argv), &(p->argc));
}
/* Add to a plugin's environment */
__attribute__((nonnull(2), warn_unused_result))
static bool add_environment(plugin *p, const char *def, bool replace){
if(p == NULL){
return false;
}
/* namelen = length of name of environment variable */
size_t namelen = (size_t)(strchrnul(def, '=') - def);
/* Search for this environment variable */
for(char **envdef = p->environ; *envdef != NULL; envdef++){
if(strncmp(*envdef, def, namelen + 1) == 0){
/* It already exists */
if(replace){
char *new_envdef;
do {
new_envdef = realloc(*envdef, strlen(def) + 1);
} while(new_envdef == NULL and errno == EINTR);
if(new_envdef == NULL){
return false;
}
*envdef = new_envdef;
strcpy(*envdef, def);
}
return true;
}
}
return add_to_char_array(def, &(p->environ), &(p->envc));
}
#ifndef O_CLOEXEC
/*
* Based on the example in the GNU LibC manual chapter 13.13 "File
* Descriptor Flags".
| [[info:libc:Descriptor%20Flags][File Descriptor Flags]] |
*/
__attribute__((warn_unused_result))
static int set_cloexec_flag(int fd){
int ret = (int)TEMP_FAILURE_RETRY(fcntl(fd, F_GETFD, 0));
/* If reading the flags failed, return error indication now. */
if(ret < 0){
return ret;
}
/* Store modified flag word in the descriptor. */
return (int)TEMP_FAILURE_RETRY(fcntl(fd, F_SETFD,
ret | FD_CLOEXEC));
}
#endif /* not O_CLOEXEC */
/* Mark processes as completed when they exit, and save their exit
status. */
static void handle_sigchld(__attribute__((unused)) int sig){
int old_errno = errno;
while(true){
plugin *proc = plugin_list;
int status;
pid_t pid = waitpid(-1, &status, WNOHANG);
if(pid == 0){
/* Only still running child processes */
break;
}
if(pid == -1){
if(errno == ECHILD){
/* No child processes */
break;
}
error(0, errno, "waitpid");
}
/* A child exited, find it in process_list */
while(proc != NULL and proc->pid != pid){
proc = proc->next;
}
if(proc == NULL){
/* Process not found in process list */
continue;
}
proc->status = status;
proc->completed = 1;
}
errno = old_errno;
}
/* Prints out a password to stdout */
__attribute__((nonnull, warn_unused_result))
static bool print_out_password(const char *buffer, size_t length){
ssize_t ret;
for(size_t written = 0; written < length; written += (size_t)ret){
ret = TEMP_FAILURE_RETRY(write(STDOUT_FILENO, buffer + written,
length - written));
if(ret < 0){
return false;
}
}
return true;
}
/* Removes and free a plugin from the plugin list */
__attribute__((nonnull))
static void free_plugin(plugin *plugin_node){
for(char **arg = (plugin_node->argv)+1; *arg != NULL; arg++){
free(*arg);
}
free(plugin_node->name);
free(plugin_node->argv);
for(char **env = plugin_node->environ; *env != NULL; env++){
free(*env);
}
free(plugin_node->environ);
free(plugin_node->buffer);
/* Removes the plugin from the singly-linked list */
if(plugin_node == plugin_list){
/* First one - simple */
plugin_list = plugin_list->next;
} else {
/* Second one or later */
for(plugin *p = plugin_list; p != NULL; p = p->next){
if(p->next == plugin_node){
p->next = plugin_node->next;
break;
}
}
}
free(plugin_node);
}
static void free_plugin_list(void){
while(plugin_list != NULL){
free_plugin(plugin_list);
}
}
int main(int argc, char *argv[]){
char *plugindir = NULL;
char *pluginhelperdir = NULL;
char *argfile = NULL;
FILE *conffp;
struct dirent **direntries = NULL;
struct stat st;
fd_set rfds_all;
int ret, maxfd = 0;
ssize_t sret;
uid_t uid = 65534;
gid_t gid = 65534;
bool debug = false;
int exitstatus = EXIT_SUCCESS;
struct sigaction old_sigchld_action;
struct sigaction sigchld_action = { .sa_handler = handle_sigchld,
.sa_flags = SA_NOCLDSTOP };
char **custom_argv = NULL;
int custom_argc = 0;
int dir_fd = -1;
/* Establish a signal handler */
sigemptyset(&sigchld_action.sa_mask);
ret = sigaddset(&sigchld_action.sa_mask, SIGCHLD);
if(ret == -1){
error(0, errno, "sigaddset");
exitstatus = EX_OSERR;
goto fallback;
}
ret = sigaction(SIGCHLD, &sigchld_action, &old_sigchld_action);
if(ret == -1){
error(0, errno, "sigaction");
exitstatus = EX_OSERR;
goto fallback;
}
/* The options we understand. */
struct argp_option options[] = {
{ .name = "global-options", .key = 'g',
.arg = "OPTION[,OPTION[,...]]",
.doc = "Options passed to all plugins" },
{ .name = "global-env", .key = 'G',
.arg = "VAR=value",
.doc = "Environment variable passed to all plugins" },
{ .name = "options-for", .key = 'o',
.arg = "PLUGIN:OPTION[,OPTION[,...]]",
.doc = "Options passed only to specified plugin" },
{ .name = "env-for", .key = 'E',
.arg = "PLUGIN:ENV=value",
.doc = "Environment variable passed to specified plugin" },
{ .name = "disable", .key = 'd',
.arg = "PLUGIN",
.doc = "Disable a specific plugin", .group = 1 },
{ .name = "enable", .key = 'e',
.arg = "PLUGIN",
.doc = "Enable a specific plugin", .group = 1 },
{ .name = "plugin-dir", .key = 128,
.arg = "DIRECTORY",
.doc = "Specify a different plugin directory", .group = 2 },
{ .name = "config-file", .key = 129,
.arg = "FILE",
.doc = "Specify a different configuration file", .group = 2 },
{ .name = "userid", .key = 130,
.arg = "ID", .flags = 0,
.doc = "User ID the plugins will run as", .group = 3 },
{ .name = "groupid", .key = 131,
.arg = "ID", .flags = 0,
.doc = "Group ID the plugins will run as", .group = 3 },
{ .name = "debug", .key = 132,
.doc = "Debug mode", .group = 4 },
{ .name = "plugin-helper-dir", .key = 133,
.arg = "DIRECTORY",
.doc = "Specify a different plugin helper directory",
.group = 2 },
/*
* These reproduce what we would get without ARGP_NO_HELP
*/
{ .name = "help", .key = '?',
.doc = "Give this help list", .group = -1 },
{ .name = "usage", .key = -3,
.doc = "Give a short usage message", .group = -1 },
{ .name = "version", .key = 'V',
.doc = "Print program version", .group = -1 },
{ .name = NULL }
};
__attribute__((nonnull(3)))
error_t parse_opt(int key, char *arg, struct argp_state *state){
errno = 0;
switch(key){
char *tmp;
intmax_t tmp_id;
case 'g': /* --global-options */
{
char *plugin_option;
while((plugin_option = strsep(&arg, ",")) != NULL){
if(not add_argument(getplugin(NULL), plugin_option)){
break;
}
}
errno = 0;
}
break;
case 'G': /* --global-env */
if(add_environment(getplugin(NULL), arg, true)){
errno = 0;
}
break;
case 'o': /* --options-for */
{
char *option_list = strchr(arg, ':');
if(option_list == NULL){
argp_error(state, "No colon in \"%s\"", arg);
errno = EINVAL;
break;
}
*option_list = '\0';
option_list++;
if(arg[0] == '\0'){
argp_error(state, "Empty plugin name");
errno = EINVAL;
break;
}
char *option;
while((option = strsep(&option_list, ",")) != NULL){
if(not add_argument(getplugin(arg), option)){
break;
}
}
errno = 0;
}
break;
case 'E': /* --env-for */
{
char *envdef = strchr(arg, ':');
if(envdef == NULL){
argp_error(state, "No colon in \"%s\"", arg);
errno = EINVAL;
break;
}
*envdef = '\0';
envdef++;
if(arg[0] == '\0'){
argp_error(state, "Empty plugin name");
errno = EINVAL;
break;
}
if(add_environment(getplugin(arg), envdef, true)){
errno = 0;
}
}
break;
case 'd': /* --disable */
{
plugin *p = getplugin(arg);
if(p != NULL){
p->disabled = true;
errno = 0;
}
}
break;
case 'e': /* --enable */
{
plugin *p = getplugin(arg);
if(p != NULL){
p->disabled = false;
errno = 0;
}
}
break;
case 128: /* --plugin-dir */
free(plugindir);
plugindir = strdup(arg);
if(plugindir != NULL){
errno = 0;
}
break;
case 129: /* --config-file */
/* This is already done by parse_opt_config_file() */
break;
case 130: /* --userid */
tmp_id = strtoimax(arg, &tmp, 10);
if(errno != 0 or tmp == arg or *tmp != '\0'
or tmp_id != (uid_t)tmp_id){
argp_error(state, "Bad user ID number: \"%s\", using %"
PRIdMAX, arg, (intmax_t)uid);
break;
}
uid = (uid_t)tmp_id;
errno = 0;
break;
case 131: /* --groupid */
tmp_id = strtoimax(arg, &tmp, 10);
if(errno != 0 or tmp == arg or *tmp != '\0'
or tmp_id != (gid_t)tmp_id){
argp_error(state, "Bad group ID number: \"%s\", using %"
PRIdMAX, arg, (intmax_t)gid);
break;
}
gid = (gid_t)tmp_id;
errno = 0;
break;
case 132: /* --debug */
debug = true;
break;
case 133: /* --plugin-helper-dir */
free(pluginhelperdir);
pluginhelperdir = strdup(arg);
if(pluginhelperdir != NULL){
errno = 0;
}
break;
/*
* These reproduce what we would get without ARGP_NO_HELP
*/
case '?': /* --help */
state->flags &= ~(unsigned int)ARGP_NO_EXIT; /* force exit */
argp_state_help(state, state->out_stream, ARGP_HELP_STD_HELP);
__builtin_unreachable();
case -3: /* --usage */
state->flags &= ~(unsigned int)ARGP_NO_EXIT; /* force exit */
argp_state_help(state, state->out_stream,
ARGP_HELP_USAGE | ARGP_HELP_EXIT_OK);
__builtin_unreachable();
case 'V': /* --version */
fprintf(state->out_stream, "%s\n", argp_program_version);
exit(EXIT_SUCCESS);
break;
/*
* When adding more options before this line, remember to also add a
* "case" to the "parse_opt_config_file" function below.
*/
case ARGP_KEY_ARG:
/* Cryptsetup always passes an argument, which is an empty
string if "none" was specified in /etc/crypttab. So if
argument was empty, we ignore it silently. */
if(arg[0] == '\0'){
break;
}
#if __GNUC__ >= 7
__attribute__((fallthrough));
#else
/* FALLTHROUGH */
#endif
default:
return ARGP_ERR_UNKNOWN;
}
return errno; /* Set to 0 at start */
}
/* This option parser is the same as parse_opt() above, except it
ignores everything but the --config-file option. */
error_t parse_opt_config_file(int key, char *arg,
__attribute__((unused))
struct argp_state *state){
errno = 0;
switch(key){
case 'g': /* --global-options */
case 'G': /* --global-env */
case 'o': /* --options-for */
case 'E': /* --env-for */
case 'd': /* --disable */
case 'e': /* --enable */
case 128: /* --plugin-dir */
break;
case 129: /* --config-file */
free(argfile);
argfile = strdup(arg);
if(argfile != NULL){
errno = 0;
}
break;
case 130: /* --userid */
case 131: /* --groupid */
case 132: /* --debug */
case 133: /* --plugin-helper-dir */
case '?': /* --help */
case -3: /* --usage */
case 'V': /* --version */
case ARGP_KEY_ARG:
break;
default:
return ARGP_ERR_UNKNOWN;
}
return errno;
}
struct argp argp = { .options = options,
.parser = parse_opt_config_file,
.args_doc = "",
.doc = "Mandos plugin runner -- Run plugins" };
/* Parse using parse_opt_config_file() in order to get the custom
config file location, if any. */
ret = argp_parse(&argp, argc, argv,
ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP,
NULL, NULL);
switch(ret){
case 0:
break;
case ENOMEM:
default:
errno = ret;
error(0, errno, "argp_parse");
exitstatus = EX_OSERR;
goto fallback;
case EINVAL:
exitstatus = EX_USAGE;
goto fallback;
}
/* Reset to the normal argument parser */
argp.parser = parse_opt;
/* Open the configfile if available */
if(argfile == NULL){
conffp = fopen(AFILE, "r");
} else {
conffp = fopen(argfile, "r");
}
if(conffp != NULL){
char *org_line = NULL;
char *p, *arg, *new_arg, *line;
size_t size = 0;
const char whitespace_delims[] = " \r\t\f\v\n";
const char comment_delim[] = "#";
custom_argc = 1;
custom_argv = malloc(sizeof(char*) * 2);
if(custom_argv == NULL){
error(0, errno, "malloc");
exitstatus = EX_OSERR;
goto fallback;
}
custom_argv[0] = argv[0];
custom_argv[1] = NULL;
/* for each line in the config file, strip whitespace and ignore
commented text */
while(true){
sret = getline(&org_line, &size, conffp);
if(sret == -1){
break;
}
line = org_line;
arg = strsep(&line, comment_delim);
while((p = strsep(&arg, whitespace_delims)) != NULL){
if(p[0] == '\0'){
continue;
}
new_arg = strdup(p);
if(new_arg == NULL){
error(0, errno, "strdup");
exitstatus = EX_OSERR;
free(org_line);
goto fallback;
}
custom_argc += 1;
{
#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 26)
char **new_argv = reallocarray(custom_argv,
(size_t)custom_argc + 1,
sizeof(char *));
#else
char **new_argv = NULL;
if(((size_t)custom_argc + 1) > (SIZE_MAX / sizeof(char *))){
/* overflow */
errno = ENOMEM;
} else {
new_argv = realloc(custom_argv, ((size_t)custom_argc + 1)
* sizeof(char *));
}
#endif
if(new_argv == NULL){
error(0, errno, "reallocarray");
exitstatus = EX_OSERR;
free(new_arg);
free(org_line);
goto fallback;
} else {
custom_argv = new_argv;
}
}
custom_argv[custom_argc-1] = new_arg;
custom_argv[custom_argc] = NULL;
}
}
do {
ret = fclose(conffp);
} while(ret == EOF and errno == EINTR);
if(ret == EOF){
error(0, errno, "fclose");
exitstatus = EX_IOERR;
goto fallback;
}
free(org_line);
} else {
/* Check for harmful errors and go to fallback. Other errors might
not affect opening plugins */
if(errno == EMFILE or errno == ENFILE or errno == ENOMEM){
error(0, errno, "fopen");
exitstatus = EX_OSERR;
goto fallback;
}
}
/* If there were any arguments from the configuration file, pass
them to parser as command line arguments */
if(custom_argv != NULL){
ret = argp_parse(&argp, custom_argc, custom_argv,
ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP,
NULL, NULL);
switch(ret){
case 0:
break;
case ENOMEM:
default:
errno = ret;
error(0, errno, "argp_parse");
exitstatus = EX_OSERR;
goto fallback;
case EINVAL:
exitstatus = EX_CONFIG;
goto fallback;
}
}
/* Parse actual command line arguments, to let them override the
config file */
ret = argp_parse(&argp, argc, argv,
ARGP_IN_ORDER | ARGP_NO_EXIT | ARGP_NO_HELP,
NULL, NULL);
switch(ret){
case 0:
break;
case ENOMEM:
default:
errno = ret;
error(0, errno, "argp_parse");
exitstatus = EX_OSERR;
goto fallback;
case EINVAL:
exitstatus = EX_USAGE;
goto fallback;
}
{
char *pluginhelperenv;
bool bret = true;
ret = asprintf(&pluginhelperenv, "MANDOSPLUGINHELPERDIR=%s",
pluginhelperdir != NULL ? pluginhelperdir : PHDIR);
if(ret != -1){
bret = add_environment(getplugin(NULL), pluginhelperenv, true);
}
if(ret == -1 or not bret){
error(0, errno, "Failed to set MANDOSPLUGINHELPERDIR"
" environment variable to \"%s\" for all plugins\n",
pluginhelperdir != NULL ? pluginhelperdir : PHDIR);
}
if(ret != -1){
free(pluginhelperenv);
}
}
if(debug){
for(plugin *p = plugin_list; p != NULL; p = p->next){
fprintf(stderr, "Plugin: %s has %d arguments\n",
p->name ? p->name : "Global", p->argc - 1);
for(char **a = p->argv; *a != NULL; a++){
fprintf(stderr, "\tArg: %s\n", *a);
}
fprintf(stderr, "...and %d environment variables\n", p->envc);
for(char **a = p->environ; *a != NULL; a++){
fprintf(stderr, "\t%s\n", *a);
}
}
}
if(getuid() == 0){
/* Work around Debian bug #633582:
*/
int plugindir_fd = open(/* plugindir or */ PDIR, O_RDONLY);
if(plugindir_fd == -1){
if(errno != ENOENT){
error(0, errno, "open(\"" PDIR "\")");
}
} else {
ret = (int)TEMP_FAILURE_RETRY(fstat(plugindir_fd, &st));
if(ret == -1){
error(0, errno, "fstat");
} else {
if(S_ISDIR(st.st_mode) and st.st_uid == 0 and st.st_gid == 0){
ret = fchown(plugindir_fd, uid, gid);
if(ret == -1){
error(0, errno, "fchown");
}
}
}
close(plugindir_fd);
}
/* Work around Debian bug #981302
*/
if(lstat("/dev/fd", &st) != 0 and errno == ENOENT){
ret = symlink("/proc/self/fd", "/dev/fd");
if(ret == -1){
error(0, errno, "Failed to create /dev/fd symlink");
}
}
}
/* Lower permissions */
ret = setgid(gid);
if(ret == -1){
error(0, errno, "setgid");
}
ret = setuid(uid);
if(ret == -1){
error(0, errno, "setuid");
}
/* Open plugin directory with close_on_exec flag */
{
dir_fd = open(plugindir != NULL ? plugindir : PDIR, O_RDONLY |
#ifdef O_CLOEXEC
O_CLOEXEC
#else /* not O_CLOEXEC */
0
#endif /* not O_CLOEXEC */
);
if(dir_fd == -1){
error(0, errno, "Could not open plugin dir");
exitstatus = EX_UNAVAILABLE;
goto fallback;
}
#ifndef O_CLOEXEC
/* Set the FD_CLOEXEC flag on the directory */
ret = set_cloexec_flag(dir_fd);
if(ret < 0){
error(0, errno, "set_cloexec_flag");
exitstatus = EX_OSERR;
goto fallback;
}
#endif /* O_CLOEXEC */
}
int good_name(const struct dirent * const dirent){
const char * const patterns[] = { ".*", "#*#", "*~", "*.dpkg-new",
"*.dpkg-old", "*.dpkg-bak",
"*.dpkg-divert", NULL };
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual"
#endif
for(const char **pat = (const char **)patterns;
*pat != NULL; pat++){
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
if(fnmatch(*pat, dirent->d_name, FNM_FILE_NAME | FNM_PERIOD)
!= FNM_NOMATCH){
if(debug){
fprintf(stderr, "Ignoring plugin dir entry \"%s\""
" matching pattern %s\n", dirent->d_name, *pat);
}
return 0;
}
}
return 1;
}
int numplugins = scandirat(dir_fd, ".", &direntries, good_name,
alphasort);
if(numplugins == -1){
error(0, errno, "Could not scan plugin dir");
direntries = NULL;
exitstatus = EX_OSERR;
goto fallback;
}
FD_ZERO(&rfds_all);
/* Read and execute any executable in the plugin directory*/
for(int i = 0; i < numplugins; i++){
int plugin_fd = openat(dir_fd, direntries[i]->d_name, O_RDONLY);
if(plugin_fd == -1){
error(0, errno, "Could not open plugin");
free(direntries[i]);
continue;
}
ret = (int)TEMP_FAILURE_RETRY(fstat(plugin_fd, &st));
if(ret == -1){
error(0, errno, "stat");
close(plugin_fd);
free(direntries[i]);
continue;
}
/* Ignore non-executable files */
if(not S_ISREG(st.st_mode)
or (TEMP_FAILURE_RETRY(faccessat(dir_fd, direntries[i]->d_name,
X_OK, 0)) != 0)){
if(debug){
fprintf(stderr, "Ignoring plugin dir entry \"%s/%s\""
" with bad type or mode\n",
plugindir != NULL ? plugindir : PDIR,
direntries[i]->d_name);
}
close(plugin_fd);
free(direntries[i]);
continue;
}
plugin *p = getplugin(direntries[i]->d_name);
if(p == NULL){
error(0, errno, "getplugin");
close(plugin_fd);
free(direntries[i]);
continue;
}
if(p->disabled){
if(debug){
fprintf(stderr, "Ignoring disabled plugin \"%s\"\n",
direntries[i]->d_name);
}
close(plugin_fd);
free(direntries[i]);
continue;
}
{
/* Add global arguments to argument list for this plugin */
plugin *g = getplugin(NULL);
if(g != NULL){
for(char **a = g->argv + 1; *a != NULL; a++){
if(not add_argument(p, *a)){
error(0, errno, "add_argument");
}
}
/* Add global environment variables */
for(char **e = g->environ; *e != NULL; e++){
if(not add_environment(p, *e, false)){
error(0, errno, "add_environment");
}
}
}
}
/* If this plugin has any environment variables, we need to
duplicate the environment from this process, too. */
if(p->environ[0] != NULL){
for(char **e = environ; *e != NULL; e++){
if(not add_environment(p, *e, false)){
error(0, errno, "add_environment");
}
}
}
int pipefd[2];
#ifndef O_CLOEXEC
ret = (int)TEMP_FAILURE_RETRY(pipe(pipefd));
#else /* O_CLOEXEC */
ret = (int)TEMP_FAILURE_RETRY(pipe2(pipefd, O_CLOEXEC));
#endif /* O_CLOEXEC */
if(ret == -1){
error(0, errno, "pipe");
exitstatus = EX_OSERR;
free(direntries[i]);
goto fallback;
}
if(pipefd[0] >= FD_SETSIZE){
fprintf(stderr, "pipe()[0] (%d) >= FD_SETSIZE (%d)", pipefd[0],
FD_SETSIZE);
close(pipefd[0]);
close(pipefd[1]);
exitstatus = EX_OSERR;
free(direntries[i]);
goto fallback;
}
#ifndef O_CLOEXEC
/* Ask OS to automatic close the pipe on exec */
ret = set_cloexec_flag(pipefd[0]);
if(ret < 0){
error(0, errno, "set_cloexec_flag");
close(pipefd[0]);
close(pipefd[1]);
exitstatus = EX_OSERR;
free(direntries[i]);
goto fallback;
}
ret = set_cloexec_flag(pipefd[1]);
if(ret < 0){
error(0, errno, "set_cloexec_flag");
close(pipefd[0]);
close(pipefd[1]);
exitstatus = EX_OSERR;
free(direntries[i]);
goto fallback;
}
#endif /* not O_CLOEXEC */
/* Block SIGCHLD until process is safely in process list */
ret = (int)TEMP_FAILURE_RETRY(sigprocmask(SIG_BLOCK,
&sigchld_action.sa_mask,
NULL));
if(ret < 0){
error(0, errno, "sigprocmask");
exitstatus = EX_OSERR;
free(direntries[i]);
goto fallback;
}
/* Starting a new process to be watched */
pid_t pid;
do {
pid = fork();
} while(pid == -1 and errno == EINTR);
if(pid == -1){
error(0, errno, "fork");
TEMP_FAILURE_RETRY(sigprocmask(SIG_UNBLOCK,
&sigchld_action.sa_mask, NULL));
close(pipefd[0]);
close(pipefd[1]);
exitstatus = EX_OSERR;
free(direntries[i]);
goto fallback;
}
if(pid == 0){
/* this is the child process */
ret = sigaction(SIGCHLD, &old_sigchld_action, NULL);
if(ret < 0){
error(0, errno, "sigaction");
_exit(EX_OSERR);
}
ret = sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask, NULL);
if(ret < 0){
error(0, errno, "sigprocmask");
_exit(EX_OSERR);
}
ret = dup2(pipefd[1], STDOUT_FILENO); /* replace our stdout */
if(ret == -1){
error(0, errno, "dup2");
_exit(EX_OSERR);
}
if(fexecve(plugin_fd, p->argv,
(p->environ[0] != NULL) ? p->environ : environ) < 0){
error(0, errno, "fexecve for %s/%s",
plugindir != NULL ? plugindir : PDIR,
direntries[i]->d_name);
_exit(EX_OSERR);
}
/* no return */
}
/* Parent process */
close(pipefd[1]); /* Close unused write end of pipe */
close(plugin_fd);
plugin *new_plugin = getplugin(direntries[i]->d_name);
if(new_plugin == NULL){
error(0, errno, "getplugin");
ret = (int)(TEMP_FAILURE_RETRY
(sigprocmask(SIG_UNBLOCK, &sigchld_action.sa_mask,
NULL)));
if(ret < 0){
error(0, errno, "sigprocmask");
}
exitstatus = EX_OSERR;
free(direntries[i]);
goto fallback;
}
free(direntries[i]);
new_plugin->pid = pid;
new_plugin->fd = pipefd[0];
if(debug){
fprintf(stderr, "Plugin %s started (PID %" PRIdMAX ")\n",
new_plugin->name, (intmax_t) (new_plugin->pid));
}
/* Unblock SIGCHLD so signal handler can be run if this process
has already completed */
ret = (int)TEMP_FAILURE_RETRY(sigprocmask(SIG_UNBLOCK,
&sigchld_action.sa_mask,
NULL));
if(ret < 0){
error(0, errno, "sigprocmask");
exitstatus = EX_OSERR;
goto fallback;
}
FD_SET(new_plugin->fd, &rfds_all);
if(maxfd < new_plugin->fd){
maxfd = new_plugin->fd;
}
}
free(direntries);
direntries = NULL;
close(dir_fd);
dir_fd = -1;
free_plugin(getplugin(NULL));
for(plugin *p = plugin_list; p != NULL; p = p->next){
if(p->pid != 0){
break;
}
if(p->next == NULL){
fprintf(stderr, "No plugin processes started. Incorrect plugin"
" directory?\n");
free_plugin_list();
}
}
/* Main loop while running plugins exist */
while(plugin_list){
fd_set rfds = rfds_all;
int select_ret = select(maxfd+1, &rfds, NULL, NULL, NULL);
if(select_ret == -1 and errno != EINTR){
error(0, errno, "select");
exitstatus = EX_OSERR;
goto fallback;
}
/* OK, now either a process completed, or something can be read
from one of them */
for(plugin *proc = plugin_list; proc != NULL;){
/* Is this process completely done? */
if(proc->completed and proc->eof){
/* Only accept the plugin output if it exited cleanly */
if(not WIFEXITED(proc->status)
or WEXITSTATUS(proc->status) != 0){
/* Bad exit by plugin */
if(debug){
if(WIFEXITED(proc->status)){
fprintf(stderr, "Plugin %s [%" PRIdMAX "] exited with"
" status %d\n", proc->name,
(intmax_t) (proc->pid),
WEXITSTATUS(proc->status));
} else if(WIFSIGNALED(proc->status)){
fprintf(stderr, "Plugin %s [%" PRIdMAX "] killed by"
" signal %d: %s\n", proc->name,
(intmax_t) (proc->pid),
WTERMSIG(proc->status),
strsignal(WTERMSIG(proc->status)));
}
}
/* Remove the plugin */
FD_CLR(proc->fd, &rfds_all);
/* Block signal while modifying process_list */
ret = (int)TEMP_FAILURE_RETRY(sigprocmask
(SIG_BLOCK,
&sigchld_action.sa_mask,
NULL));
if(ret < 0){
error(0, errno, "sigprocmask");
exitstatus = EX_OSERR;
goto fallback;
}
plugin *next_plugin = proc->next;
free_plugin(proc);
proc = next_plugin;
/* We are done modifying process list, so unblock signal */
ret = (int)(TEMP_FAILURE_RETRY
(sigprocmask(SIG_UNBLOCK,
&sigchld_action.sa_mask, NULL)));
if(ret < 0){
error(0, errno, "sigprocmask");
exitstatus = EX_OSERR;
goto fallback;
}
if(plugin_list == NULL){
break;
}
continue;
}
/* This process exited nicely, so print its buffer */
bool bret = print_out_password(proc->buffer,
proc->buffer_length);
if(not bret){
error(0, errno, "print_out_password");
exitstatus = EX_IOERR;
}
goto fallback;
}
/* This process has not completed. Does it have any output? */
if(proc->eof or not FD_ISSET(proc->fd, &rfds)){
/* This process had nothing to say at this time */
proc = proc->next;
continue;
}
/* Before reading, make the process' data buffer large enough */
if(proc->buffer_length + BUFFER_SIZE > proc->buffer_size){
char *new_buffer = realloc(proc->buffer, proc->buffer_size
+ (size_t) BUFFER_SIZE);
if(new_buffer == NULL){
error(0, errno, "malloc");
exitstatus = EX_OSERR;
goto fallback;
}
proc->buffer = new_buffer;
proc->buffer_size += BUFFER_SIZE;
}
/* Read from the process */
sret = TEMP_FAILURE_RETRY(read(proc->fd,
proc->buffer
+ proc->buffer_length,
BUFFER_SIZE));
if(sret < 0){
/* Read error from this process; ignore the error */
proc = proc->next;
continue;
}
if(sret == 0){
/* got EOF */
proc->eof = true;
} else {
proc->buffer_length += (size_t) sret;
}
}
}
fallback:
if(plugin_list == NULL or (exitstatus != EXIT_SUCCESS
and exitstatus != EX_OK)){
/* Fallback if all plugins failed, none are found or an error
occured */
bool bret;
fprintf(stderr, "Going to fallback mode using getpass(3)\n");
char *passwordbuffer = getpass("Password: ");
size_t len = strlen(passwordbuffer);
/* Strip trailing newline */
if(len > 0 and passwordbuffer[len-1] == '\n'){
passwordbuffer[len-1] = '\0'; /* not strictly necessary */
len--;
}
bret = print_out_password(passwordbuffer, len);
if(not bret){
error(0, errno, "print_out_password");
exitstatus = EX_IOERR;
}
}
/* Restore old signal handler */
ret = sigaction(SIGCHLD, &old_sigchld_action, NULL);
if(ret == -1){
error(0, errno, "sigaction");
exitstatus = EX_OSERR;
}
if(custom_argv != NULL){
for(char **arg = custom_argv+1; *arg != NULL; arg++){
free(*arg);
}
free(custom_argv);
}
free(direntries);
if(dir_fd != -1){
close(dir_fd);
}
/* Kill the processes */
for(plugin *p = plugin_list; p != NULL; p = p->next){
if(p->pid != 0){
close(p->fd);
ret = kill(p->pid, SIGTERM);
if(ret == -1 and errno != ESRCH){
/* Set-uid proccesses might not get closed */
error(0, errno, "kill");
}
}
}
/* Wait for any remaining child processes to terminate */
do {
ret = wait(NULL);
} while(ret >= 0);
if(errno != ECHILD){
error(0, errno, "wait");
}
free_plugin_list();
free(plugindir);
free(pluginhelperdir);
free(argfile);
return exitstatus;
}
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugin-runner.conf 0000664 0001750 0001750 00000000574 14720643017 015626 0 ustar 00teddy teddy ## This is the configuration file for plugin-runner(8mandos). This
## file should be installed as "/etc/mandos/plugin-runner.conf", and
## will be copied to "/conf/conf.d/mandos/plugin-runner.conf" in the
## initrd.img file.
##
## After editing this file, the initrd image file must be updated for
## the changes to take effect!
## Example:
#--options-for=mandos-client:--debug
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugin-runner.xml 0000664 0001750 0001750 00000055224 14720643017 015503 0 ustar 00teddy teddy
%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;
8mandos
&COMMANDNAME;
Run Mandos plugins, pass data from first to succeed.
&COMMANDNAME;
&COMMANDNAME;
&COMMANDNAME;
&COMMANDNAME;
DESCRIPTION
&COMMANDNAME; is a program which is meant to
be specified as a keyscript
for the root disk in
crypttab
5. The aim of this
program is therefore to output a password, which then
cryptsetup
8 will use to unlock the
root disk.
This program is not meant to be invoked directly, but can be in
order to test it. Note that any password obtained will simply
be output on standard output.
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
This option will add an environment variable setting to
all plugins. This will override any inherited environment
variable.
This option will add an environment variable setting to
the PLUGIN plugin. This will
override any inherited environment variables or
environment variables specified using
.
Pass some options to all plugins.
OPTIONS is a comma separated
list of options. This is not a very useful option, except
for specifying the
option to all plugins.
Pass some options to a specific plugin. PLUGIN is the name (file basename) of a
plugin, and OPTIONS is a comma
separated list of options.
Note that since options are not split on whitespace, the
way to pass, to the plugin
foo
, the option
with the option argument
baz
is either
--options-for=foo:--bar=baz or
--options-for=foo:--bar,baz. Using
--options-for="foo:--bar baz". will
not work.
Disable the plugin named
PLUGIN. The plugin will not be
started.
Re-enable the plugin named
PLUGIN. This is only useful to
undo a previous option, maybe
from the configuration file.
Change to group ID ID on
startup. The default is 65534. All plugins will be
started using this group ID. Note:
This must be a number, not a name.
Change to user ID ID on
startup. The default is 65534. All plugins will be
started using this user ID. Note:
This must be a number, not a name.
Specify a different plugin directory. The default is
/lib/mandos/plugins.d, which will
exist in the initial RAM disk
environment.
Specify a different plugin helper directory. The default
is /lib/mandos/plugin-helpers, which
will exist in the initial RAM disk
environment. (This will simply be passed to all plugins
via the MANDOSPLUGINHELPERDIR environment
variable. See )
Specify a different file to read additional options from.
See . Other command line options
will override options specified in the file.
Enable debug mode. This will enable a lot of output to
standard error about what the program is doing. The
program will still perform all other functions normally.
The default is to not run in debug
mode.
The plugins will not be affected by
this option. Use
if complete debugging eruption is desired.
Gives a help message about options and their meanings.
Gives a short usage message.
Prints the program version.
OVERVIEW
This program will run on the client side in the initial
RAM disk environment, and is responsible for
getting a password. It does this by running plugins, one of
which will normally be the actual client program communicating
with the server.
PLUGINS
This program will get a password by running a number of
plugins, which are executable programs in
a directory in the initial RAM disk
environment. The default directory is
/lib/mandos/plugins.d, but this can be
changed with the option. The
plugins are started in parallel, and the first plugin to output
a password and exit with a successful exit
code will make this plugin-runner output the password from that
plugin, stop any other plugins, and exit.
WRITING PLUGINS
A plugin is an executable program which prints a password to
its standard output and then exits with a successful (zero)
exit status. If the exit status is not zero, any output on
standard output will be ignored by the plugin runner. Any
output on its standard error channel will simply be passed to
the standard error of the plugin runner, usually the system
console.
If the password is a single-line, manually entered passprase,
a final trailing newline character should
not be printed.
The plugin will run in the initial RAM disk environment, so
care must be taken not to depend on any files or running
services not available there. Any helper executables required
by the plugin (which are not in the PATH) can
be placed in the plugin helper directory, the name of which
will be made available to the plugin via the
MANDOSPLUGINHELPERDIR environment variable.
The plugin must exit cleanly and free all allocated resources
upon getting the TERM signal, since this is what the plugin
runner uses to stop all other plugins when one plugin has
output a password and exited cleanly.
The plugin must not use resources, like for instance reading
from the standard input, without knowing that no other plugin
is also using it.
It is useful, but not required, for the plugin to take the
option.
FALLBACK
If no plugins succeed, this program will, as a fallback, ask for
a password on the console using getpass3,
and output it. This is not meant to be the normal mode of
operation, as there is a separate plugin for getting a password
from the console.
EXIT STATUS
Exit status of this program is zero if no errors were
encountered, and otherwise not. The fallback (see ) may or may not have succeeded in either
case.
ENVIRONMENT
This program does not use any environment variables itself, it
only passes on its environment to all the plugins. The
environment passed to plugins can be modified using the
and
options. Also, the option
will affect the environment variable
MANDOSPLUGINHELPERDIR for the plugins.
FILES
/conf/conf.d/mandos/plugin-runner.conf
Since this program will be run as a keyscript, there is
little to no opportunity to pass command line arguments
to it. Therefore, it will also
read this file and use its contents as
whitespace-separated command line options. Also,
everything from a #
character to the end
of a line is ignored.
This program is meant to run in the initial RAM disk
environment, so that is where this file is assumed to
exist. The file does not need to exist in the normal
file system.
This file will be processed before
the normal command line options, so the latter can
override the former, if need be.
This file name is the default; the file to read for
arguments can be changed using the
option.
/lib/mandos/plugins.d
The default plugin directory; can be changed by the
option.
/lib/mandos/plugin-helpers
The default plugin helper directory; can be changed by
the option.
BUGS
The option is ignored when
specified from within a configuration file.
EXAMPLE
Normal invocation needs no options:
&COMMANDNAME;
Run the program, but not the plugins, in debug mode:
&COMMANDNAME; --debug
Run all plugins, but run the foo
plugin in
debug mode:
&COMMANDNAME; --options-for=foo:--debug
Run all plugins, but not the program, in debug mode:
&COMMANDNAME; --global-options=--debug
Read a different configuration file, run plugins from a
different directory, specify an alternate plugin helper
directory and add four options to the
mandos-client
8mandos plugin:
cd /etc/keys/mandos; &COMMANDNAME; --config-file=/etc/mandos/plugin-runner.conf --plugin-dir /usr/lib/x86_64-linux-gnu/mandos/plugins.d --plugin-helper-dir /usr/lib/x86_64-linux-gnu/mandos/plugin-helpers --options-for=mandos-client:--pubkey=pubkey.txt,--seckey=seckey.txt,--tls-pubkey=tls-pubkey.pem,--tls-privkey=tls-privkey.pem
SECURITY
This program will, when starting, try to switch to another user.
If it is started as root, it will succeed, and will by default
switch to user and group 65534, which are assumed to be
non-privileged. This user and group is then what all plugins
will be started as. Therefore, the only way to run a plugin as
a privileged user is to have the set-user-ID or set-group-ID bit
set on the plugin executable file (see
execve2
).
If this program is used as a keyscript in crypttab5
, there is a slight risk that if this program
fails to work, there might be no way to boot the system except
for booting from another media and editing the initial RAM disk
image to not run this program. This is, however, unlikely,
since the password-prompt8mandos
plugin will read a password from the console in
case of failure of the other plugins, and this plugin runner
will also, in case of catastrophic failure, itself fall back to
asking and outputting a password on the console (see ).
SEE ALSO
intro
8mandos,
cryptsetup
8,
crypttab
5,
execve
2,
mandos
8,
password-prompt
8mandos,
mandos-client
8mandos
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1732462094.522593
mandos-1.8.18/plugins.d/ 0000775 0001750 0001750 00000000000 14720643017 014047 5 ustar 00teddy teddy ././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/askpass-fifo.c 0000664 0001750 0001750 00000013116 14720643017 016603 0 ustar 00teddy teddy /* -*- coding: utf-8 -*- */
/*
* Askpass-FIFO - Read a password from a FIFO and output it
*
* Copyright © 2008-2019, 2021 Teddy Hogeborn
* Copyright © 2008-2019, 2021 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 /* vasprintf(),
program_invocation_short_name */
#include /* uid_t, gid_t, getuid(), getgid(),
setgid(), setuid() */
#include /* uid_t, gid_t, ssize_t, getuid(),
getgid(), setgid(), setuid(),
read(), close(), write(),
STDOUT_FILENO */
#include /* va_list, va_start(), vfprintf() */
#include /* vasprintf(), fprintf(), stderr,
vfprintf() */
#include /* program_invocation_short_name,
errno, EACCES, ENOTDIR, ELOOP,
ENAMETOOLONG, ENOSPC, EROFS,
ENOENT, EEXIST, EFAULT, EMFILE,
ENFILE, ENOMEM, EBADF, EINVAL, EIO,
EISDIR, EFBIG */
#include /* strerror() */
#include /* error() */
#include /* free(), realloc(), EXIT_SUCCESS */
#include /* mkfifo(), S_IRUSR, S_IWUSR */
#include /* EX_OSFILE, EX_OSERR,
EX_UNAVAILABLE, EX_IOERR */
#include /* open(), O_RDONLY */
#include /* NULL, size_t */
uid_t uid = 65534;
gid_t gid = 65534;
/* Function to use when printing errors */
__attribute__((format (gnu_printf, 3, 4)))
void error_plus(int status, int errnum, const char *formatstring,
...){
va_list ap;
char *text;
int ret;
va_start(ap, formatstring);
ret = vasprintf(&text, formatstring, ap);
if(ret == -1){
fprintf(stderr, "Mandos plugin %s: ",
program_invocation_short_name);
vfprintf(stderr, formatstring, ap);
fprintf(stderr, ": ");
fprintf(stderr, "%s\n", strerror(errnum));
error(status, errno, "vasprintf while printing error");
if(status){
__builtin_unreachable();
}
return;
}
fprintf(stderr, "Mandos plugin ");
error(status, errnum, "%s", text);
if(status){
__builtin_unreachable();
}
free(text);
}
int main(__attribute__((unused))int argc,
__attribute__((unused))char **argv){
int ret = 0;
ssize_t sret;
uid = getuid();
gid = getgid();
/* Create FIFO */
const char passfifo[] = "/lib/cryptsetup/passfifo";
ret = mkfifo(passfifo, S_IRUSR | S_IWUSR);
if(ret == -1){
int e = errno;
switch(e){
case EACCES:
case ENOTDIR:
case ELOOP:
error_plus(EX_OSFILE, errno, "mkfifo");
__builtin_unreachable();
case ENAMETOOLONG:
case ENOSPC:
case EROFS:
default:
error_plus(EX_OSERR, errno, "mkfifo");
__builtin_unreachable();
case ENOENT:
/* no "/lib/cryptsetup"? */
error_plus(EX_UNAVAILABLE, errno, "mkfifo");
__builtin_unreachable();
case EEXIST:
break; /* not an error */
}
}
/* Open FIFO */
int fifo_fd = open(passfifo, O_RDONLY);
if(fifo_fd == -1){
int e = errno;
error_plus(0, errno, "open");
switch(e){
case EACCES:
case ENOENT:
case EFAULT:
return EX_UNAVAILABLE;
case ENAMETOOLONG:
case EMFILE:
case ENFILE:
case ENOMEM:
default:
return EX_OSERR;
case ENOTDIR:
case ELOOP:
return EX_OSFILE;
}
}
/* Lower group privileges */
if(setgid(gid) == -1){
error_plus(0, errno, "setgid");
}
/* Lower user privileges */
if(setuid(uid) == -1){
error_plus(0, errno, "setuid");
}
/* Read from FIFO */
char *buf = NULL;
size_t buf_len = 0;
{
size_t buf_allocated = 0;
const size_t blocksize = 1024;
do {
if(buf_len + blocksize > buf_allocated){
char *tmp = realloc(buf, buf_allocated + blocksize);
if(tmp == NULL){
error_plus(0, errno, "realloc");
free(buf);
return EX_OSERR;
}
buf = tmp;
buf_allocated += blocksize;
}
sret = read(fifo_fd, buf + buf_len, buf_allocated - buf_len);
if(sret == -1){
int e = errno;
free(buf);
errno = e;
error_plus(0, errno, "read");
switch(e){
case EBADF:
case EFAULT:
case EINVAL:
default:
return EX_OSERR;
case EIO:
return EX_IOERR;
case EISDIR:
return EX_UNAVAILABLE;
}
}
buf_len += (size_t)sret;
} while(sret != 0);
}
/* Close FIFO */
close(fifo_fd);
/* Print password to stdout */
size_t written = 0;
while(written < buf_len){
sret = write(STDOUT_FILENO, buf + written, buf_len - written);
if(sret == -1){
int e = errno;
free(buf);
errno = e;
error_plus(0, errno, "write");
switch(e){
case EBADF:
case EFAULT:
case EINVAL:
return EX_OSFILE;
case EFBIG:
case EIO:
case ENOSPC:
default:
return EX_IOERR;
}
}
written += (size_t)sret;
}
free(buf);
ret = close(STDOUT_FILENO);
if(ret == -1){
int e = errno;
error_plus(0, errno, "close");
switch(e){
case EBADF:
return EX_OSFILE;
case EIO:
default:
return EX_IOERR;
}
}
return EXIT_SUCCESS;
}
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/askpass-fifo.xml 0000664 0001750 0001750 00000012367 14720643017 017170 0 ustar 00teddy teddy
%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;
8mandos
&COMMANDNAME;
Mandos plugin to get a password from a
FIFO.
&COMMANDNAME;
DESCRIPTION
This program reads a password from a FIFO and
outputs it to standard output.
This program is not very useful on its own. This program is
really meant to run as a plugin in the Mandos client-side system, where it is used as a
fallback and alternative to retrieving passwords from a
Mandos server.
This program is meant to be imitate a feature of the
askpass program, so that programs written to
interface with it can keep working under the
Mandos system.
OPTIONS
This program takes no options.
EXIT STATUS
If exit status is 0, the output from the program is the password
as it was read. Otherwise, if exit status is other than 0, the
program was interrupted or encountered an error, and any output
so far could be corrupt and/or truncated, and should therefore
be ignored.
FILES
/lib/cryptsetup/passfifo
This is the FIFO where this program
will read the password. If it does not exist, it will be
created.
BUGS
EXAMPLE
Note that normally, this program will not be invoked directly,
but instead started by the Mandos plugin-runner8mandos
.
This program takes no options.
&COMMANDNAME;
SECURITY
The only thing that could be considered worthy of note is
this: This program is meant to be run by
plugin-runner8mandos, and will, when run
standalone, outside, in a normal environment, immediately output
on its standard output any presumably secret password it just
received. Therefore, when running this program standalone
(which should never normally be done), take care not to type in
any real secret password by force of habit, since it would then
immediately be shown as output.
SEE ALSO
intro
8mandos,
fifo
7,
plugin-runner
8mandos
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/mandos-client.c 0000664 0001750 0001750 00000257322 14720643017 016763 0 ustar 00teddy teddy /* -*- coding: utf-8 -*- */
/*
* Mandos-client - get and decrypt data from a Mandos server
*
* This program is partly derived from an example program for an Avahi
* service browser, downloaded from
* . This
* includes the following functions: "resolve_callback",
* "browse_callback", and parts of "main".
*
* Everything else is
* Copyright © 2008-2022 Teddy Hogeborn
* Copyright © 2008-2022 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 .
*/
/* Needed by GPGME, specifically gpgme_data_seek() */
#ifndef _LARGEFILE_SOURCE
#define _LARGEFILE_SOURCE
#endif /* not _LARGEFILE_SOURCE */
#ifndef _FILE_OFFSET_BITS
#define _FILE_OFFSET_BITS 64
#endif /* not _FILE_OFFSET_BITS */
#define _GNU_SOURCE /* program_invocation_short_name,
TEMP_FAILURE_RETRY(), O_CLOEXEC,
scandirat(), asprintf() */
#include /* bool, false, true */
#include /* argp_program_version,
argp_program_bug_address,
struct argp_option,
struct argp_state, argp_error(),
argp_state_help,
ARGP_HELP_STD_HELP,
ARGP_HELP_EXIT_ERR,
ARGP_HELP_EXIT_OK, ARGP_HELP_USAGE,
argp_err_exit_status,
ARGP_ERR_UNKNOWN, struct argp,
argp_parse(), ARGP_IN_ORDER,
ARGP_NO_HELP */
#include /* NULL, size_t */
#include /* uid_t, gid_t, sig_atomic_t,
seteuid(), setuid(), pid_t,
setgid(), getuid(), getgid() */
#include /* uid_t, gid_t, TEMP_FAILURE_RETRY(),
seteuid(), setuid(), close(),
ssize_t, read(), fork(), setgid(),
_exit(), dup2(), STDIN_FILENO,
STDERR_FILENO, STDOUT_FILENO,
fexecve(), write(), getuid(),
getgid(), fchown(), symlink(),
sleep(), unlinkat(), pause() */
#include /* in_port_t, struct sockaddr_in6,
sa_family_t, struct sockaddr_in,
htons(), IN6_IS_ADDR_LINKLOCAL,
INET_ADDRSTRLEN, INET6_ADDRSTRLEN,
ntohl(), IPPROTO_IP */
#include /* struct timespec, clock_gettime(),
CLOCK_MONOTONIC, time_t, struct tm,
gmtime_r(), clock_settime(),
CLOCK_REALTIME, nanosleep() */
#include /* errno,
program_invocation_short_name,
EINTR, EINVAL, ENETUNREACH,
EHOSTUNREACH, ECONNREFUSED, EPROTO,
EIO, ENOENT, ENXIO, error_t,
ENOMEM, EISDIR, ENOTEMPTY */
#include /* fprintf(), stderr, perror(), FILE,
vfprintf(), off_t, SEEK_SET,
stdout, fwrite(), ferror(),
fflush(), asprintf() */
#include /* va_list, va_start(), vfprintf() */
#include /* realloc(), free(), malloc(),
getenv(), EXIT_FAILURE, setenv(),
EXIT_SUCCESS, strtof(), strtod(),
srand(), mkdtemp(), abort() */
#include /* strdup(), strcmp(), strlen(),
strerror(), strncpy(), strspn(),
memcpy(), strrchr(), strchr(),
strsignal() */
#include /* open(), O_RDONLY, O_DIRECTORY,
O_PATH, O_CLOEXEC, openat(),
O_NOFOLLOW, AT_REMOVEDIR */
#include /* or, and, not */
#include /* struct stat, fstat(), fstatat(),
S_ISREG(), S_IXUSR, S_IXGRP,
S_IXOTH, lstat() */
#include /* IF_NAMESIZE, if_indextoname(),
if_nametoindex(), SIOCGIFFLAGS,
IFF_LOOPBACK, IFF_POINTOPOINT,
IFF_BROADCAST, IFF_NOARP, IFF_UP,
IFF_RUNNING, SIOCSIFFLAGS */
#include /* EX_NOPERM, EX_OSERR,
EX_UNAVAILABLE, EX_USAGE */
#include /* setgroups() */
#include /* waitpid(), WIFEXITED(),
WEXITSTATUS(), WIFSIGNALED(),
WTERMSIG() */
#include /* kill(), SIGTERM, struct sigaction,
SIG_DFL, sigemptyset(),
sigaddset(), SIGINT, SIGHUP,
SIG_IGN, raise() */
#include /* struct sockaddr_storage, AF_INET6,
PF_INET6, AF_INET, PF_INET,
socket(), SOCK_STREAM,
SOCK_CLOEXEC, struct sockaddr,
connect(), SOCK_DGRAM */
#include /* argz_next(), argz_add_sep(),
argz_delete(), argz_stringify(),
argz_add(), argz_count() */
#include /* PRIuMAX, uintmax_t, uint32_t,
PRIdMAX, PRIu16, intmax_t,
strtoimax() */
#include /* inet_pton() */
#include /* uint32_t, intptr_t, uint16_t */
#include /* getnameinfo(), NI_NUMERICHOST,
EAI_SYSTEM, gai_strerror() */
#include /* ioctl() */
#include /* struct dirent, scandirat(),
alphasort(), scandir() */
#include /* INT_MAX */
#ifdef __linux__
#include /* klogctl() */
#endif /* __linux__ */
/* Avahi */
/* All Avahi types, constants and functions
Avahi*, avahi_*,
AVAHI_* */
#include
#include
#include
#include
#include
#include
/* GnuTLS */
#include /* All GnuTLS types, constants and
functions: gnutls_*, GNUTLS_* */
#if GNUTLS_VERSION_NUMBER < 0x030600
#include
/* gnutls_certificate_set_openpgp_key_file(),
GNUTLS_OPENPGP_FMT_BASE64 */
#elif GNUTLS_VERSION_NUMBER >= 0x030606
#include /* GNUTLS_PKCS_PLAIN,
GNUTLS_PKCS_NULL_PASSWORD */
#endif
/* GPGME */
#include /* All GPGME types, constants and
functions:
gpgme_*, GPG_ERR_NO_*,
GPGME_IMPORT_*
GPGME_PROTOCOL_OpenPGP */
#define BUFFER_SIZE 256
#define PATHDIR "/conf/conf.d/mandos"
#define SECKEY "seckey.txt"
#define PUBKEY "pubkey.txt"
#define TLS_PRIVKEY "tls-privkey.pem"
#define TLS_PUBKEY "tls-pubkey.pem"
#define HOOKDIR "/lib/mandos/network-hooks.d"
bool debug = false;
static const char mandos_protocol_version[] = "1";
const char *argp_program_version = "mandos-client " VERSION;
const char *argp_program_bug_address = "";
static const char sys_class_net[] = "/sys/class/net";
char *connect_to = NULL;
const char *hookdir = HOOKDIR;
int hookdir_fd = -1;
uid_t uid = 65534;
gid_t gid = 65534;
/* Doubly linked list that need to be circularly linked when used */
typedef struct server{
const char *ip;
in_port_t port;
AvahiIfIndex if_index;
int af;
struct timespec last_seen;
struct server *next;
struct server *prev;
} server;
/* Used for passing in values through the Avahi callback functions */
typedef struct {
AvahiServer *server;
gnutls_certificate_credentials_t cred;
unsigned int dh_bits;
gnutls_dh_params_t dh_params;
const char *priority;
gpgme_ctx_t ctx;
server *current_server;
char *interfaces;
size_t interfaces_size;
} mandos_context;
/* global so signal handler can reach it*/
AvahiSimplePoll *simple_poll;
sig_atomic_t quit_now = 0;
int signal_received = 0;
/* Function to use when printing errors */
void perror_plus(const char *print_text){
int e = errno;
fprintf(stderr, "Mandos plugin %s: ",
program_invocation_short_name);
errno = e;
perror(print_text);
}
__attribute__((format (gnu_printf, 2, 3), nonnull))
int fprintf_plus(FILE *stream, const char *format, ...){
va_list ap;
va_start (ap, format);
TEMP_FAILURE_RETRY(fprintf(stream, "Mandos plugin %s: ",
program_invocation_short_name));
return (int)TEMP_FAILURE_RETRY(vfprintf(stream, format, ap));
}
/*
* Make additional room in "buffer" for at least BUFFER_SIZE more
* bytes. "buffer_capacity" is how much is currently allocated,
* "buffer_length" is how much is already used.
*/
__attribute__((nonnull, warn_unused_result))
size_t incbuffer(char **buffer, size_t buffer_length,
size_t buffer_capacity){
if(buffer_length + BUFFER_SIZE > buffer_capacity){
char *new_buf = realloc(*buffer, buffer_capacity + BUFFER_SIZE);
if(new_buf == NULL){
int old_errno = errno;
free(*buffer);
errno = old_errno;
*buffer = NULL;
return 0;
}
*buffer = new_buf;
buffer_capacity += BUFFER_SIZE;
}
return buffer_capacity;
}
/* Add server to set of servers to retry periodically */
__attribute__((nonnull, warn_unused_result))
bool add_server(const char *ip, in_port_t port, AvahiIfIndex if_index,
int af, server **current_server){
int ret;
server *new_server = malloc(sizeof(server));
if(new_server == NULL){
perror_plus("malloc");
return false;
}
*new_server = (server){ .ip = strdup(ip),
.port = port,
.if_index = if_index,
.af = af };
if(new_server->ip == NULL){
perror_plus("strdup");
free(new_server);
return false;
}
ret = clock_gettime(CLOCK_MONOTONIC, &(new_server->last_seen));
if(ret == -1){
perror_plus("clock_gettime");
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual"
#endif
free((char *)(new_server->ip));
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
free(new_server);
return false;
}
/* Special case of first server */
if(*current_server == NULL){
new_server->next = new_server;
new_server->prev = new_server;
*current_server = new_server;
} else {
/* Place the new server last in the list */
new_server->next = *current_server;
new_server->prev = (*current_server)->prev;
new_server->prev->next = new_server;
(*current_server)->prev = new_server;
}
return true;
}
/* Set effective uid to 0, return errno */
__attribute__((warn_unused_result))
int raise_privileges(void){
int old_errno = errno;
int ret = 0;
if(seteuid(0) == -1){
ret = errno;
}
errno = old_errno;
return ret;
}
/* Set effective and real user ID to 0. Return errno. */
__attribute__((warn_unused_result))
int raise_privileges_permanently(void){
int old_errno = errno;
int ret = raise_privileges();
if(ret != 0){
errno = old_errno;
return ret;
}
if(setuid(0) == -1){
ret = errno;
}
errno = old_errno;
return ret;
}
/* Set effective user ID to unprivileged saved user ID */
__attribute__((warn_unused_result))
int lower_privileges(void){
int old_errno = errno;
int ret = 0;
if(seteuid(uid) == -1){
ret = errno;
}
errno = old_errno;
return ret;
}
/* Lower privileges permanently */
__attribute__((warn_unused_result))
int lower_privileges_permanently(void){
int old_errno = errno;
int ret = 0;
if(setuid(uid) == -1){
ret = errno;
}
errno = old_errno;
return ret;
}
/*
* Initialize GPGME.
*/
__attribute__((nonnull, warn_unused_result))
static bool init_gpgme(const char * const seckey,
const char * const pubkey,
const char * const tempdir,
mandos_context *mc){
gpgme_error_t rc;
gpgme_engine_info_t engine_info;
/*
* Helper function to insert pub and seckey to the engine keyring.
*/
bool import_key(const char * const filename){
int ret;
int fd;
gpgme_data_t pgp_data;
fd = (int)TEMP_FAILURE_RETRY(open(filename, O_RDONLY));
if(fd == -1){
perror_plus("open");
return false;
}
/* Workaround for systems without a real-time clock; see also
Debian bug #894495: */
do {
{
time_t currtime = time(NULL);
if(currtime != (time_t)-1){
struct tm tm;
if(gmtime_r(&currtime, &tm) == NULL) {
perror_plus("gmtime_r");
break;
}
if(tm.tm_year != 70 or tm.tm_mon != 0){
break;
}
if(debug){
fprintf_plus(stderr, "System clock is January 1970");
}
} else {
if(debug){
fprintf_plus(stderr, "System clock is invalid");
}
}
}
struct stat keystat;
ret = fstat(fd, &keystat);
if(ret != 0){
perror_plus("fstat");
break;
}
ret = raise_privileges();
if(ret != 0){
errno = ret;
perror_plus("Failed to raise privileges");
break;
}
if(debug){
fprintf_plus(stderr,
"Setting system clock to key file mtime");
}
if(clock_settime(CLOCK_REALTIME, &keystat.st_mtim) != 0){
perror_plus("clock_settime");
}
ret = lower_privileges();
if(ret != 0){
errno = ret;
perror_plus("Failed to lower privileges");
}
} while(false);
rc = gpgme_data_new_from_fd(&pgp_data, fd);
if(rc != GPG_ERR_NO_ERROR){
fprintf_plus(stderr, "bad gpgme_data_new_from_fd: %s: %s\n",
gpgme_strsource(rc), gpgme_strerror(rc));
return false;
}
rc = gpgme_op_import(mc->ctx, pgp_data);
if(rc != GPG_ERR_NO_ERROR){
fprintf_plus(stderr, "bad gpgme_op_import: %s: %s\n",
gpgme_strsource(rc), gpgme_strerror(rc));
return false;
}
{
gpgme_import_result_t import_result
= gpgme_op_import_result(mc->ctx);
if((import_result->imported < 1
or import_result->not_imported > 0)
and import_result->unchanged == 0){
fprintf_plus(stderr, "bad gpgme_op_import_results:\n");
fprintf_plus(stderr,
"The total number of considered keys: %d\n",
import_result->considered);
fprintf_plus(stderr,
"The number of keys without user ID: %d\n",
import_result->no_user_id);
fprintf_plus(stderr,
"The total number of imported keys: %d\n",
import_result->imported);
fprintf_plus(stderr, "The number of imported RSA keys: %d\n",
import_result->imported_rsa);
fprintf_plus(stderr, "The number of unchanged keys: %d\n",
import_result->unchanged);
fprintf_plus(stderr, "The number of new user IDs: %d\n",
import_result->new_user_ids);
fprintf_plus(stderr, "The number of new sub keys: %d\n",
import_result->new_sub_keys);
fprintf_plus(stderr, "The number of new signatures: %d\n",
import_result->new_signatures);
fprintf_plus(stderr, "The number of new revocations: %d\n",
import_result->new_revocations);
fprintf_plus(stderr,
"The total number of secret keys read: %d\n",
import_result->secret_read);
fprintf_plus(stderr,
"The number of imported secret keys: %d\n",
import_result->secret_imported);
fprintf_plus(stderr,
"The number of unchanged secret keys: %d\n",
import_result->secret_unchanged);
fprintf_plus(stderr, "The number of keys not imported: %d\n",
import_result->not_imported);
for(gpgme_import_status_t import_status
= import_result->imports;
import_status != NULL;
import_status = import_status->next){
fprintf_plus(stderr, "Import status for key: %s\n",
import_status->fpr);
if(import_status->result != GPG_ERR_NO_ERROR){
fprintf_plus(stderr, "Import result: %s: %s\n",
gpgme_strsource(import_status->result),
gpgme_strerror(import_status->result));
}
fprintf_plus(stderr, "Key status:\n");
fprintf_plus(stderr,
import_status->status & GPGME_IMPORT_NEW
? "The key was new.\n"
: "The key was not new.\n");
fprintf_plus(stderr,
import_status->status & GPGME_IMPORT_UID
? "The key contained new user IDs.\n"
: "The key did not contain new user IDs.\n");
fprintf_plus(stderr,
import_status->status & GPGME_IMPORT_SIG
? "The key contained new signatures.\n"
: "The key did not contain new signatures.\n");
fprintf_plus(stderr,
import_status->status & GPGME_IMPORT_SUBKEY
? "The key contained new sub keys.\n"
: "The key did not contain new sub keys.\n");
fprintf_plus(stderr,
import_status->status & GPGME_IMPORT_SECRET
? "The key contained a secret key.\n"
: "The key did not contain a secret key.\n");
}
return false;
}
}
ret = close(fd);
if(ret == -1){
perror_plus("close");
}
gpgme_data_release(pgp_data);
return true;
}
if(debug){
fprintf_plus(stderr, "Initializing GPGME\n");
}
/* Init GPGME */
gpgme_check_version(NULL);
rc = gpgme_engine_check_version(GPGME_PROTOCOL_OpenPGP);
if(rc != GPG_ERR_NO_ERROR){
fprintf_plus(stderr, "bad gpgme_engine_check_version: %s: %s\n",
gpgme_strsource(rc), gpgme_strerror(rc));
return false;
}
/* Set GPGME home directory for the OpenPGP engine only */
rc = gpgme_get_engine_info(&engine_info);
if(rc != GPG_ERR_NO_ERROR){
fprintf_plus(stderr, "bad gpgme_get_engine_info: %s: %s\n",
gpgme_strsource(rc), gpgme_strerror(rc));
return false;
}
while(engine_info != NULL){
if(engine_info->protocol == GPGME_PROTOCOL_OpenPGP){
gpgme_set_engine_info(GPGME_PROTOCOL_OpenPGP,
engine_info->file_name, tempdir);
break;
}
engine_info = engine_info->next;
}
if(engine_info == NULL){
fprintf_plus(stderr, "Could not set GPGME home dir to %s\n",
tempdir);
return false;
}
/* Create new GPGME "context" */
rc = gpgme_new(&(mc->ctx));
if(rc != GPG_ERR_NO_ERROR){
fprintf_plus(stderr, "bad gpgme_new: %s: %s\n",
gpgme_strsource(rc), gpgme_strerror(rc));
return false;
}
if(not import_key(pubkey) or not import_key(seckey)){
return false;
}
return true;
}
/*
* Decrypt OpenPGP data.
* Returns -1 on error
*/
__attribute__((nonnull, warn_unused_result))
static ssize_t pgp_packet_decrypt(const char *cryptotext,
size_t crypto_size,
char **plaintext,
mandos_context *mc){
gpgme_data_t dh_crypto, dh_plain;
gpgme_error_t rc;
ssize_t ret;
size_t plaintext_capacity = 0;
ssize_t plaintext_length = 0;
if(debug){
fprintf_plus(stderr, "Trying to decrypt OpenPGP data\n");
}
/* Create new GPGME data buffer from memory cryptotext */
rc = gpgme_data_new_from_mem(&dh_crypto, cryptotext, crypto_size,
0);
if(rc != GPG_ERR_NO_ERROR){
fprintf_plus(stderr, "bad gpgme_data_new_from_mem: %s: %s\n",
gpgme_strsource(rc), gpgme_strerror(rc));
return -1;
}
/* Create new empty GPGME data buffer for the plaintext */
rc = gpgme_data_new(&dh_plain);
if(rc != GPG_ERR_NO_ERROR){
fprintf_plus(stderr, "bad gpgme_data_new: %s: %s\n",
gpgme_strsource(rc), gpgme_strerror(rc));
gpgme_data_release(dh_crypto);
return -1;
}
/* Decrypt data from the cryptotext data buffer to the plaintext
data buffer */
rc = gpgme_op_decrypt(mc->ctx, dh_crypto, dh_plain);
if(rc != GPG_ERR_NO_ERROR){
fprintf_plus(stderr, "bad gpgme_op_decrypt: %s: %s\n",
gpgme_strsource(rc), gpgme_strerror(rc));
plaintext_length = -1;
if(debug){
gpgme_decrypt_result_t result;
result = gpgme_op_decrypt_result(mc->ctx);
if(result == NULL){
fprintf_plus(stderr, "gpgme_op_decrypt_result failed\n");
} else {
if(result->unsupported_algorithm != NULL) {
fprintf_plus(stderr, "Unsupported algorithm: %s\n",
result->unsupported_algorithm);
}
fprintf_plus(stderr, "Wrong key usage: %s\n",
result->wrong_key_usage ? "Yes" : "No");
if(result->file_name != NULL){
fprintf_plus(stderr, "File name: %s\n", result->file_name);
}
for(gpgme_recipient_t r = result->recipients; r != NULL;
r = r->next){
fprintf_plus(stderr, "Public key algorithm: %s\n",
gpgme_pubkey_algo_name(r->pubkey_algo));
fprintf_plus(stderr, "Key ID: %s\n", r->keyid);
fprintf_plus(stderr, "Secret key available: %s\n",
r->status == GPG_ERR_NO_SECKEY ? "No" : "Yes");
}
}
}
goto decrypt_end;
}
if(debug){
fprintf_plus(stderr, "Decryption of OpenPGP data succeeded\n");
}
/* Seek back to the beginning of the GPGME plaintext data buffer */
if(gpgme_data_seek(dh_plain, (off_t)0, SEEK_SET) == -1){
perror_plus("gpgme_data_seek");
plaintext_length = -1;
goto decrypt_end;
}
*plaintext = NULL;
while(true){
plaintext_capacity = incbuffer(plaintext,
(size_t)plaintext_length,
plaintext_capacity);
if(plaintext_capacity == 0){
perror_plus("incbuffer");
plaintext_length = -1;
goto decrypt_end;
}
ret = gpgme_data_read(dh_plain, *plaintext + plaintext_length,
BUFFER_SIZE);
/* Print the data, if any */
if(ret == 0){
/* EOF */
break;
}
if(ret < 0){
perror_plus("gpgme_data_read");
plaintext_length = -1;
goto decrypt_end;
}
plaintext_length += ret;
}
if(debug){
fprintf_plus(stderr, "Decrypted password is: ");
for(ssize_t i = 0; i < plaintext_length; i++){
fprintf(stderr, "%02hhX ", (*plaintext)[i]);
}
fprintf(stderr, "\n");
}
decrypt_end:
/* Delete the GPGME cryptotext data buffer */
gpgme_data_release(dh_crypto);
/* Delete the GPGME plaintext data buffer */
gpgme_data_release(dh_plain);
return plaintext_length;
}
__attribute__((warn_unused_result, const))
static const char *safe_string(const char *str){
if(str == NULL)
return "(unknown)";
return str;
}
__attribute__((warn_unused_result))
static const char *safer_gnutls_strerror(int value){
const char *ret = gnutls_strerror(value);
return safe_string(ret);
}
/* GnuTLS log function callback */
__attribute__((nonnull))
static void debuggnutls(__attribute__((unused)) int level,
const char* string){
fprintf_plus(stderr, "GnuTLS: %s", string);
}
__attribute__((nonnull(1, 2, 4), warn_unused_result))
static int init_gnutls_global(const char *pubkeyfilename,
const char *seckeyfilename,
const char *dhparamsfilename,
mandos_context *mc){
int ret;
if(debug){
fprintf_plus(stderr, "Initializing GnuTLS\n");
}
if(debug){
/* "Use a log level over 10 to enable all debugging options."
* - GnuTLS manual
*/
gnutls_global_set_log_level(11);
gnutls_global_set_log_function(debuggnutls);
}
/* OpenPGP credentials */
ret = gnutls_certificate_allocate_credentials(&mc->cred);
if(ret != GNUTLS_E_SUCCESS){
fprintf_plus(stderr, "GnuTLS memory error: %s\n",
safer_gnutls_strerror(ret));
return -1;
}
if(debug){
fprintf_plus(stderr, "Attempting to use public key %s and"
" private key %s as GnuTLS credentials\n",
pubkeyfilename,
seckeyfilename);
}
#if GNUTLS_VERSION_NUMBER >= 0x030606
ret = gnutls_certificate_set_rawpk_key_file
(mc->cred, pubkeyfilename, seckeyfilename,
GNUTLS_X509_FMT_PEM, /* format */
NULL, /* pass */
/* key_usage */
GNUTLS_KEY_DIGITAL_SIGNATURE | GNUTLS_KEY_KEY_ENCIPHERMENT,
NULL, /* names */
0, /* names_length */
/* privkey_flags */
GNUTLS_PKCS_PLAIN | GNUTLS_PKCS_NULL_PASSWORD,
0); /* pkcs11_flags */
#elif GNUTLS_VERSION_NUMBER < 0x030600
ret = gnutls_certificate_set_openpgp_key_file
(mc->cred, pubkeyfilename, seckeyfilename,
GNUTLS_OPENPGP_FMT_BASE64);
#else
#error "Needs GnuTLS 3.6.6 or later, or before 3.6.0"
#endif
if(ret != GNUTLS_E_SUCCESS){
fprintf_plus(stderr,
"Error[%d] while reading the key pair ('%s',"
" '%s')\n", ret, pubkeyfilename, seckeyfilename);
fprintf_plus(stderr, "The GnuTLS error is: %s\n",
safer_gnutls_strerror(ret));
goto globalfail;
}
/* GnuTLS server initialization */
ret = gnutls_dh_params_init(&mc->dh_params);
if(ret != GNUTLS_E_SUCCESS){
fprintf_plus(stderr, "Error in GnuTLS DH parameter"
" initialization: %s\n",
safer_gnutls_strerror(ret));
goto globalfail;
}
/* If a Diffie-Hellman parameters file was given, try to use it */
if(dhparamsfilename != NULL){
gnutls_datum_t params = { .data = NULL, .size = 0 };
do {
int dhpfile = open(dhparamsfilename, O_RDONLY);
if(dhpfile == -1){
perror_plus("open");
dhparamsfilename = NULL;
break;
}
size_t params_capacity = 0;
while(true){
params_capacity = incbuffer((char **)¶ms.data,
(size_t)params.size,
(size_t)params_capacity);
if(params_capacity == 0){
perror_plus("incbuffer");
free(params.data);
params.data = NULL;
dhparamsfilename = NULL;
break;
}
ssize_t bytes_read = read(dhpfile,
params.data + params.size,
BUFFER_SIZE);
/* EOF */
if(bytes_read == 0){
break;
}
/* check bytes_read for failure */
if(bytes_read < 0){
perror_plus("read");
free(params.data);
params.data = NULL;
dhparamsfilename = NULL;
break;
}
params.size += (unsigned int)bytes_read;
}
ret = close(dhpfile);
if(ret == -1){
perror_plus("close");
}
if(params.data == NULL){
dhparamsfilename = NULL;
}
if(dhparamsfilename == NULL){
break;
}
ret = gnutls_dh_params_import_pkcs3(mc->dh_params, ¶ms,
GNUTLS_X509_FMT_PEM);
if(ret != GNUTLS_E_SUCCESS){
fprintf_plus(stderr, "Failed to parse DH parameters in file"
" \"%s\": %s\n", dhparamsfilename,
safer_gnutls_strerror(ret));
dhparamsfilename = NULL;
}
free(params.data);
} while(false);
}
if(dhparamsfilename == NULL){
if(mc->dh_bits == 0){
#if GNUTLS_VERSION_NUMBER < 0x030600
/* Find out the optimal number of DH bits */
/* Try to read the private key file */
gnutls_datum_t buffer = { .data = NULL, .size = 0 };
do {
int secfile = open(seckeyfilename, O_RDONLY);
if(secfile == -1){
perror_plus("open");
break;
}
size_t buffer_capacity = 0;
while(true){
buffer_capacity = incbuffer((char **)&buffer.data,
(size_t)buffer.size,
(size_t)buffer_capacity);
if(buffer_capacity == 0){
perror_plus("incbuffer");
free(buffer.data);
buffer.data = NULL;
break;
}
ssize_t bytes_read = read(secfile,
buffer.data + buffer.size,
BUFFER_SIZE);
/* EOF */
if(bytes_read == 0){
break;
}
/* check bytes_read for failure */
if(bytes_read < 0){
perror_plus("read");
free(buffer.data);
buffer.data = NULL;
break;
}
buffer.size += (unsigned int)bytes_read;
}
close(secfile);
} while(false);
/* If successful, use buffer to parse private key */
gnutls_sec_param_t sec_param = GNUTLS_SEC_PARAM_ULTRA;
if(buffer.data != NULL){
{
gnutls_openpgp_privkey_t privkey = NULL;
ret = gnutls_openpgp_privkey_init(&privkey);
if(ret != GNUTLS_E_SUCCESS){
fprintf_plus(stderr, "Error initializing OpenPGP key"
" structure: %s",
safer_gnutls_strerror(ret));
free(buffer.data);
buffer.data = NULL;
} else {
ret = gnutls_openpgp_privkey_import
(privkey, &buffer, GNUTLS_OPENPGP_FMT_BASE64, "", 0);
if(ret != GNUTLS_E_SUCCESS){
fprintf_plus(stderr, "Error importing OpenPGP key : %s",
safer_gnutls_strerror(ret));
privkey = NULL;
}
free(buffer.data);
buffer.data = NULL;
if(privkey != NULL){
/* Use private key to suggest an appropriate
sec_param */
sec_param = gnutls_openpgp_privkey_sec_param(privkey);
gnutls_openpgp_privkey_deinit(privkey);
if(debug){
fprintf_plus(stderr, "This OpenPGP key implies using"
" a GnuTLS security parameter \"%s\".\n",
safe_string(gnutls_sec_param_get_name
(sec_param)));
}
}
}
}
if(sec_param == GNUTLS_SEC_PARAM_UNKNOWN){
/* Err on the side of caution */
sec_param = GNUTLS_SEC_PARAM_ULTRA;
if(debug){
fprintf_plus(stderr, "Falling back to security parameter"
" \"%s\"\n",
safe_string(gnutls_sec_param_get_name
(sec_param)));
}
}
}
unsigned int uret = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, sec_param);
if(uret != 0){
mc->dh_bits = uret;
if(debug){
fprintf_plus(stderr, "A \"%s\" GnuTLS security parameter"
" implies %u DH bits; using that.\n",
safe_string(gnutls_sec_param_get_name
(sec_param)),
mc->dh_bits);
}
} else {
fprintf_plus(stderr, "Failed to get implied number of DH"
" bits for security parameter \"%s\"): %s\n",
safe_string(gnutls_sec_param_get_name
(sec_param)),
safer_gnutls_strerror(ret));
goto globalfail;
}
#endif
} else { /* dh_bits != 0 */
if(debug){
fprintf_plus(stderr, "DH bits explicitly set to %u\n",
mc->dh_bits);
}
ret = gnutls_dh_params_generate2(mc->dh_params, mc->dh_bits);
if(ret != GNUTLS_E_SUCCESS){
fprintf_plus(stderr, "Error in GnuTLS prime generation (%u"
" bits): %s\n", mc->dh_bits,
safer_gnutls_strerror(ret));
goto globalfail;
}
gnutls_certificate_set_dh_params(mc->cred, mc->dh_params);
}
}
return 0;
globalfail:
gnutls_certificate_free_credentials(mc->cred);
gnutls_dh_params_deinit(mc->dh_params);
return -1;
}
__attribute__((nonnull, warn_unused_result))
static int init_gnutls_session(gnutls_session_t *session,
mandos_context *mc){
int ret;
/* GnuTLS session creation */
do {
ret = gnutls_init(session, (GNUTLS_SERVER
#if GNUTLS_VERSION_NUMBER >= 0x030506
| GNUTLS_NO_TICKETS
#endif
#if GNUTLS_VERSION_NUMBER >= 0x030606
| GNUTLS_ENABLE_RAWPK
#endif
));
if(quit_now){
return -1;
}
} while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN);
if(ret != GNUTLS_E_SUCCESS){
fprintf_plus(stderr,
"Error in GnuTLS session initialization: %s\n",
safer_gnutls_strerror(ret));
}
{
const char *err;
do {
ret = gnutls_priority_set_direct(*session, mc->priority, &err);
if(quit_now){
gnutls_deinit(*session);
return -1;
}
} while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN);
if(ret != GNUTLS_E_SUCCESS){
fprintf_plus(stderr, "Syntax error at: %s\n", err);
fprintf_plus(stderr, "GnuTLS error: %s\n",
safer_gnutls_strerror(ret));
gnutls_deinit(*session);
return -1;
}
}
do {
ret = gnutls_credentials_set(*session, GNUTLS_CRD_CERTIFICATE,
mc->cred);
if(quit_now){
gnutls_deinit(*session);
return -1;
}
} while(ret == GNUTLS_E_INTERRUPTED or ret == GNUTLS_E_AGAIN);
if(ret != GNUTLS_E_SUCCESS){
fprintf_plus(stderr, "Error setting GnuTLS credentials: %s\n",
safer_gnutls_strerror(ret));
gnutls_deinit(*session);
return -1;
}
/* ignore client certificate if any. */
gnutls_certificate_server_set_request(*session, GNUTLS_CERT_IGNORE);
return 0;
}
/* Avahi log function callback */
static void empty_log(__attribute__((unused)) AvahiLogLevel level,
__attribute__((unused)) const char *txt){}
/* Helper function to add_local_route() and delete_local_route() */
__attribute__((nonnull, warn_unused_result))
static bool add_delete_local_route(const bool add,
const char *address,
AvahiIfIndex if_index){
int ret;
char helper[] = "mandos-client-iprouteadddel";
char add_arg[] = "add";
char delete_arg[] = "delete";
char debug_flag[] = "--debug";
char *pluginhelperdir = getenv("MANDOSPLUGINHELPERDIR");
if(pluginhelperdir == NULL){
if(debug){
fprintf_plus(stderr, "MANDOSPLUGINHELPERDIR environment"
" variable not set; cannot run helper\n");
}
return false;
}
char interface[IF_NAMESIZE];
if(if_indextoname((unsigned int)if_index, interface) == NULL){
perror_plus("if_indextoname");
return false;
}
int devnull = (int)TEMP_FAILURE_RETRY(open("/dev/null", O_RDONLY));
if(devnull == -1){
perror_plus("open(\"/dev/null\", O_RDONLY)");
return false;
}
pid_t pid = fork();
if(pid == 0){
/* Child */
/* Raise privileges */
errno = raise_privileges_permanently();
if(errno != 0){
perror_plus("Failed to raise privileges");
/* _exit(EX_NOPERM); */
} else {
/* Set group */
errno = 0;
ret = setgid(0);
if(ret == -1){
perror_plus("setgid");
close(devnull);
_exit(EX_NOPERM);
}
/* Reset supplementary groups */
errno = 0;
ret = setgroups(0, NULL);
if(ret == -1){
perror_plus("setgroups");
close(devnull);
_exit(EX_NOPERM);
}
}
ret = dup2(devnull, STDIN_FILENO);
if(ret == -1){
perror_plus("dup2(devnull, STDIN_FILENO)");
close(devnull);
_exit(EX_OSERR);
}
ret = close(devnull);
if(ret == -1){
perror_plus("close");
}
ret = dup2(STDERR_FILENO, STDOUT_FILENO);
if(ret == -1){
perror_plus("dup2(STDERR_FILENO, STDOUT_FILENO)");
_exit(EX_OSERR);
}
int helperdir_fd = (int)TEMP_FAILURE_RETRY(open(pluginhelperdir,
O_RDONLY
| O_DIRECTORY
| O_PATH
| O_CLOEXEC));
if(helperdir_fd == -1){
perror_plus("open");
_exit(EX_UNAVAILABLE);
}
int helper_fd = (int)TEMP_FAILURE_RETRY(openat(helperdir_fd,
helper, O_RDONLY));
if(helper_fd == -1){
perror_plus("openat");
close(helperdir_fd);
_exit(EX_UNAVAILABLE);
}
close(helperdir_fd);
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual"
#endif
if(fexecve(helper_fd, (char *const [])
{ helper, add ? add_arg : delete_arg, (char *)address,
interface, debug ? debug_flag : NULL, NULL },
environ) == -1){
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
perror_plus("fexecve");
_exit(EXIT_FAILURE);
}
}
if(pid == -1){
perror_plus("fork");
close(devnull);
return false;
}
ret = close(devnull);
if(ret == -1){
perror_plus("close");
}
int status;
pid_t pret = -1;
errno = 0;
do {
pret = waitpid(pid, &status, 0);
if(pret == -1 and errno == EINTR and quit_now){
int errno_raising = 0;
if((errno = raise_privileges()) != 0){
errno_raising = errno;
perror_plus("Failed to raise privileges in order to"
" kill helper program");
}
if(kill(pid, SIGTERM) == -1){
perror_plus("kill");
}
if((errno_raising == 0) and (errno = lower_privileges()) != 0){
perror_plus("Failed to lower privileges after killing"
" helper program");
}
return false;
}
} while(pret == -1 and errno == EINTR);
if(pret == -1){
perror_plus("waitpid");
return false;
}
if(WIFEXITED(status)){
if(WEXITSTATUS(status) != 0){
fprintf_plus(stderr, "Error: iprouteadddel exited"
" with status %d\n", WEXITSTATUS(status));
return false;
}
return true;
}
if(WIFSIGNALED(status)){
fprintf_plus(stderr, "Error: iprouteadddel died by"
" signal %d\n", WTERMSIG(status));
return false;
}
fprintf_plus(stderr, "Error: iprouteadddel crashed\n");
return false;
}
__attribute__((nonnull, warn_unused_result))
static bool add_local_route(const char *address,
AvahiIfIndex if_index){
if(debug){
fprintf_plus(stderr, "Adding route to %s\n", address);
}
return add_delete_local_route(true, address, if_index);
}
__attribute__((nonnull, warn_unused_result))
static bool delete_local_route(const char *address,
AvahiIfIndex if_index){
if(debug){
fprintf_plus(stderr, "Removing route to %s\n", address);
}
return add_delete_local_route(false, address, if_index);
}
/* Called when a Mandos server is found */
__attribute__((nonnull, warn_unused_result))
static int start_mandos_communication(const char *ip, in_port_t port,
AvahiIfIndex if_index,
int af, mandos_context *mc){
int ret, tcp_sd = -1;
ssize_t sret;
struct sockaddr_storage to;
char *buffer = NULL;
char *decrypted_buffer = NULL;
size_t buffer_length = 0;
size_t buffer_capacity = 0;
size_t written;
int retval = -1;
gnutls_session_t session;
int pf; /* Protocol family */
bool route_added = false;
errno = 0;
if(quit_now){
errno = EINTR;
return -1;
}
switch(af){
case AF_INET6:
pf = PF_INET6;
break;
case AF_INET:
pf = PF_INET;
break;
default:
fprintf_plus(stderr, "Bad address family: %d\n", af);
errno = EINVAL;
return -1;
}
/* If the interface is specified and we have a list of interfaces */
if(if_index != AVAHI_IF_UNSPEC and mc->interfaces != NULL){
/* Check if the interface is one of the interfaces we are using */
bool match = false;
{
char *interface = NULL;
while((interface = argz_next(mc->interfaces,
mc->interfaces_size,
interface))){
if(if_nametoindex(interface) == (unsigned int)if_index){
match = true;
break;
}
}
}
if(not match){
/* This interface does not match any in the list, so we don't
connect to the server */
if(debug){
char interface[IF_NAMESIZE];
if(if_indextoname((unsigned int)if_index, interface) == NULL){
perror_plus("if_indextoname");
} else {
fprintf_plus(stderr, "Skipping server on non-used interface"
" \"%s\"\n",
if_indextoname((unsigned int)if_index,
interface));
}
}
return -1;
}
}
ret = init_gnutls_session(&session, mc);
if(ret != 0){
return -1;
}
if(debug){
fprintf_plus(stderr, "Setting up a TCP connection to %s, port %"
PRIuMAX "\n", ip, (uintmax_t)port);
}
tcp_sd = socket(pf, SOCK_STREAM | SOCK_CLOEXEC, 0);
if(tcp_sd < 0){
int e = errno;
perror_plus("socket");
errno = e;
goto mandos_end;
}
if(quit_now){
errno = EINTR;
goto mandos_end;
}
if(af == AF_INET6){
struct sockaddr_in6 *to6 = (struct sockaddr_in6 *)&to;
*to6 = (struct sockaddr_in6){ .sin6_family = (sa_family_t)af };
ret = inet_pton(af, ip, &to6->sin6_addr);
} else { /* IPv4 */
struct sockaddr_in *to4 = (struct sockaddr_in *)&to;
*to4 = (struct sockaddr_in){ .sin_family = (sa_family_t)af };
ret = inet_pton(af, ip, &to4->sin_addr);
}
if(ret < 0 ){
int e = errno;
perror_plus("inet_pton");
errno = e;
goto mandos_end;
}
if(ret == 0){
int e = errno;
fprintf_plus(stderr, "Bad address: %s\n", ip);
errno = e;
goto mandos_end;
}
if(af == AF_INET6){
((struct sockaddr_in6 *)&to)->sin6_port = htons(port);
if(IN6_IS_ADDR_LINKLOCAL
(&((struct sockaddr_in6 *)&to)->sin6_addr)){
if(if_index == AVAHI_IF_UNSPEC){
fprintf_plus(stderr, "An IPv6 link-local address is"
" incomplete without a network interface\n");
errno = EINVAL;
goto mandos_end;
}
/* Set the network interface number as scope */
((struct sockaddr_in6 *)&to)->sin6_scope_id = (uint32_t)if_index;
}
} else {
((struct sockaddr_in *)&to)->sin_port = htons(port);
}
if(quit_now){
errno = EINTR;
goto mandos_end;
}
if(debug){
if(af == AF_INET6 and if_index != AVAHI_IF_UNSPEC){
char interface[IF_NAMESIZE];
if(if_indextoname((unsigned int)if_index, interface) == NULL){
perror_plus("if_indextoname");
} else {
fprintf_plus(stderr, "Connection to: %s%%%s, port %" PRIuMAX
"\n", ip, interface, (uintmax_t)port);
}
} else {
fprintf_plus(stderr, "Connection to: %s, port %" PRIuMAX "\n",
ip, (uintmax_t)port);
}
char addrstr[(INET_ADDRSTRLEN > INET6_ADDRSTRLEN) ?
INET_ADDRSTRLEN : INET6_ADDRSTRLEN] = "";
if(af == AF_INET6){
ret = getnameinfo((struct sockaddr *)&to,
sizeof(struct sockaddr_in6),
addrstr, sizeof(addrstr), NULL, 0,
NI_NUMERICHOST);
} else {
ret = getnameinfo((struct sockaddr *)&to,
sizeof(struct sockaddr_in),
addrstr, sizeof(addrstr), NULL, 0,
NI_NUMERICHOST);
}
if(ret == EAI_SYSTEM){
perror_plus("getnameinfo");
} else if(ret != 0) {
fprintf_plus(stderr, "getnameinfo: %s", gai_strerror(ret));
} else if(strcmp(addrstr, ip) != 0){
fprintf_plus(stderr, "Canonical address form: %s\n", addrstr);
}
}
if(quit_now){
errno = EINTR;
goto mandos_end;
}
while(true){
if(af == AF_INET6){
ret = connect(tcp_sd, (struct sockaddr *)&to,
sizeof(struct sockaddr_in6));
} else {
ret = connect(tcp_sd, (struct sockaddr *)&to, /* IPv4 */
sizeof(struct sockaddr_in));
}
if(ret < 0){
if(((errno == ENETUNREACH) or (errno == EHOSTUNREACH))
and if_index != AVAHI_IF_UNSPEC
and connect_to == NULL
and not route_added and
((af == AF_INET6 and not
IN6_IS_ADDR_LINKLOCAL(&(((struct sockaddr_in6 *)
&to)->sin6_addr)))
or (af == AF_INET and
/* Not a a IPv4LL address */
(ntohl(((struct sockaddr_in *)&to)->sin_addr.s_addr)
& 0xFFFF0000L) != 0xA9FE0000L))){
/* Work around Avahi bug - Avahi does not announce link-local
addresses if it has a global address, so local hosts with
*only* a link-local address (e.g. Mandos clients) cannot
connect to a Mandos server announced by Avahi on a server
host with a global address. Work around this by retrying
with an explicit route added with the server's address.
Avahi bug reference:
https://lists.freedesktop.org/archives/avahi/2010-February/001833.html
https://bugs.debian.org/587961
*/
if(debug){
fprintf_plus(stderr, "Mandos server unreachable, trying"
" direct route\n");
}
int e = errno;
route_added = add_local_route(ip, if_index);
if(route_added){
continue;
}
errno = e;
}
if(errno != ECONNREFUSED or debug){
int e = errno;
perror_plus("connect");
errno = e;
}
goto mandos_end;
}
if(quit_now){
errno = EINTR;
goto mandos_end;
}
break;
}
const char *out = mandos_protocol_version;
written = 0;
while(true){
size_t out_size = strlen(out);
ret = (int)TEMP_FAILURE_RETRY(write(tcp_sd, out + written,
out_size - written));
if(ret == -1){
int e = errno;
perror_plus("write");
errno = e;
goto mandos_end;
}
written += (size_t)ret;
if(written < out_size){
continue;
} else {
if(out == mandos_protocol_version){
written = 0;
out = "\r\n";
} else {
break;
}
}
if(quit_now){
errno = EINTR;
goto mandos_end;
}
}
if(debug){
fprintf_plus(stderr, "Establishing TLS session with %s\n", ip);
}
if(quit_now){
errno = EINTR;
goto mandos_end;
}
/* This casting via intptr_t is to eliminate warning about casting
an int to a pointer type. This is exactly how the GnuTLS Guile
function "set-session-transport-fd!" does it. */
gnutls_transport_set_ptr(session,
(gnutls_transport_ptr_t)(intptr_t)tcp_sd);
if(quit_now){
errno = EINTR;
goto mandos_end;
}
do {
ret = gnutls_handshake(session);
if(quit_now){
errno = EINTR;
goto mandos_end;
}
} while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED);
if(ret != GNUTLS_E_SUCCESS){
if(debug){
fprintf_plus(stderr, "*** GnuTLS Handshake failed ***\n");
gnutls_perror(ret);
}
errno = EPROTO;
goto mandos_end;
}
/* Read OpenPGP packet that contains the wanted password */
if(debug){
fprintf_plus(stderr, "Retrieving OpenPGP encrypted password from"
" %s\n", ip);
}
while(true){
if(quit_now){
errno = EINTR;
goto mandos_end;
}
buffer_capacity = incbuffer(&buffer, buffer_length,
buffer_capacity);
if(buffer_capacity == 0){
int e = errno;
perror_plus("incbuffer");
errno = e;
goto mandos_end;
}
if(quit_now){
errno = EINTR;
goto mandos_end;
}
sret = gnutls_record_recv(session, buffer+buffer_length,
BUFFER_SIZE);
if(sret == 0){
break;
}
if(sret < 0){
switch(sret){
case GNUTLS_E_INTERRUPTED:
case GNUTLS_E_AGAIN:
break;
case GNUTLS_E_REHANDSHAKE:
do {
ret = gnutls_handshake(session);
if(quit_now){
errno = EINTR;
goto mandos_end;
}
} while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED);
if(ret < 0){
fprintf_plus(stderr, "*** GnuTLS Re-handshake failed "
"***\n");
gnutls_perror(ret);
errno = EPROTO;
goto mandos_end;
}
break;
default:
fprintf_plus(stderr, "Unknown error while reading data from"
" encrypted session with Mandos server\n");
gnutls_bye(session, GNUTLS_SHUT_RDWR);
errno = EIO;
goto mandos_end;
}
} else {
buffer_length += (size_t) sret;
}
}
if(debug){
fprintf_plus(stderr, "Closing TLS session\n");
}
if(quit_now){
errno = EINTR;
goto mandos_end;
}
do {
ret = gnutls_bye(session, GNUTLS_SHUT_RDWR);
if(quit_now){
errno = EINTR;
goto mandos_end;
}
} while(ret == GNUTLS_E_AGAIN or ret == GNUTLS_E_INTERRUPTED);
if(buffer_length > 0){
ssize_t decrypted_buffer_size;
decrypted_buffer_size = pgp_packet_decrypt(buffer, buffer_length,
&decrypted_buffer, mc);
if(decrypted_buffer_size >= 0){
clearerr(stdout);
written = 0;
while(written < (size_t) decrypted_buffer_size){
if(quit_now){
errno = EINTR;
goto mandos_end;
}
ret = (int)fwrite(decrypted_buffer + written, 1,
(size_t)decrypted_buffer_size - written,
stdout);
if(ret == 0 and ferror(stdout)){
int e = errno;
if(debug){
fprintf_plus(stderr, "Error writing encrypted data: %s\n",
strerror(errno));
}
errno = e;
goto mandos_end;
}
written += (size_t)ret;
}
ret = fflush(stdout);
if(ret != 0){
int e = errno;
if(debug){
fprintf_plus(stderr, "Error writing encrypted data: %s\n",
strerror(errno));
}
errno = e;
goto mandos_end;
}
retval = 0;
}
}
/* Shutdown procedure */
mandos_end:
{
if(route_added){
if(not delete_local_route(ip, if_index)){
fprintf_plus(stderr, "Failed to delete local route to %s on"
" interface %d", ip, if_index);
}
}
int e = errno;
free(decrypted_buffer);
free(buffer);
if(tcp_sd >= 0){
ret = close(tcp_sd);
}
if(ret == -1){
if(e == 0){
e = errno;
}
perror_plus("close");
}
gnutls_deinit(session);
errno = e;
if(quit_now){
errno = EINTR;
retval = -1;
}
}
return retval;
}
static void resolve_callback(AvahiSServiceResolver *r,
AvahiIfIndex interface,
AvahiProtocol proto,
AvahiResolverEvent event,
const char *name,
const char *type,
const char *domain,
const char *host_name,
const AvahiAddress *address,
uint16_t port,
AVAHI_GCC_UNUSED AvahiStringList *txt,
AVAHI_GCC_UNUSED AvahiLookupResultFlags
flags,
void *mc){
if(r == NULL){
return;
}
/* Called whenever a service has been resolved successfully or
timed out */
if(quit_now){
avahi_s_service_resolver_free(r);
return;
}
switch(event){
default:
case AVAHI_RESOLVER_FAILURE:
fprintf_plus(stderr, "(Avahi Resolver) Failed to resolve service "
"'%s' of type '%s' in domain '%s': %s\n", name, type,
domain,
avahi_strerror(avahi_server_errno
(((mandos_context*)mc)->server)));
break;
case AVAHI_RESOLVER_FOUND:
{
char ip[AVAHI_ADDRESS_STR_MAX];
avahi_address_snprint(ip, sizeof(ip), address);
if(debug){
fprintf_plus(stderr, "Mandos server \"%s\" found on %s (%s, %"
PRIdMAX ") on port %" PRIu16 "\n", name,
host_name, ip, (intmax_t)interface, port);
}
int ret = start_mandos_communication(ip, (in_port_t)port,
interface,
avahi_proto_to_af(proto),
mc);
if(ret == 0){
avahi_simple_poll_quit(simple_poll);
} else {
if(not add_server(ip, (in_port_t)port, interface,
avahi_proto_to_af(proto),
&((mandos_context*)mc)->current_server)){
fprintf_plus(stderr, "Failed to add server \"%s\" to server"
" list\n", name);
}
}
}
}
avahi_s_service_resolver_free(r);
}
static void browse_callback(AvahiSServiceBrowser *b,
AvahiIfIndex interface,
AvahiProtocol protocol,
AvahiBrowserEvent event,
const char *name,
const char *type,
const char *domain,
AVAHI_GCC_UNUSED AvahiLookupResultFlags
flags,
void *mc){
if(b == NULL){
return;
}
/* Called whenever a new services becomes available on the LAN or
is removed from the LAN */
if(quit_now){
return;
}
switch(event){
default:
case AVAHI_BROWSER_FAILURE:
fprintf_plus(stderr, "(Avahi browser) %s\n",
avahi_strerror(avahi_server_errno
(((mandos_context*)mc)->server)));
avahi_simple_poll_quit(simple_poll);
return;
case AVAHI_BROWSER_NEW:
/* We ignore the returned Avahi resolver object. In the callback
function we free it. If the Avahi server is terminated before
the callback function is called the Avahi server will free the
resolver for us. */
if(avahi_s_service_resolver_new(((mandos_context*)mc)->server,
interface, protocol, name, type,
domain, protocol, 0,
resolve_callback, mc) == NULL)
fprintf_plus(stderr, "Avahi: Failed to resolve service '%s':"
" %s\n", name,
avahi_strerror(avahi_server_errno
(((mandos_context*)mc)->server)));
break;
case AVAHI_BROWSER_REMOVE:
break;
case AVAHI_BROWSER_ALL_FOR_NOW:
case AVAHI_BROWSER_CACHE_EXHAUSTED:
if(debug){
fprintf_plus(stderr, "No Mandos server found, still"
" searching...\n");
}
break;
}
}
/* Signal handler that stops main loop after SIGTERM */
static void handle_sigterm(int sig){
if(quit_now){
return;
}
quit_now = 1;
signal_received = sig;
int old_errno = errno;
/* set main loop to exit */
if(simple_poll != NULL){
avahi_simple_poll_quit(simple_poll);
}
errno = old_errno;
}
__attribute__((nonnull, warn_unused_result))
bool get_flags(const char *ifname, struct ifreq *ifr){
int ret;
int old_errno;
int s = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP);
if(s < 0){
old_errno = errno;
perror_plus("socket");
errno = old_errno;
return false;
}
strncpy(ifr->ifr_name, ifname, IF_NAMESIZE);
ifr->ifr_name[IF_NAMESIZE-1] = '\0'; /* NUL terminate */
ret = ioctl(s, SIOCGIFFLAGS, ifr);
if(ret == -1){
if(debug){
old_errno = errno;
perror_plus("ioctl SIOCGIFFLAGS");
errno = old_errno;
}
if((close(s) == -1) and debug){
old_errno = errno;
perror_plus("close");
errno = old_errno;
}
return false;
}
if((close(s) == -1) and debug){
old_errno = errno;
perror_plus("close");
errno = old_errno;
}
return true;
}
__attribute__((nonnull, warn_unused_result))
bool good_flags(const char *ifname, const struct ifreq *ifr){
/* Reject the loopback device */
if(ifr->ifr_flags & IFF_LOOPBACK){
if(debug){
fprintf_plus(stderr, "Rejecting loopback interface \"%s\"\n",
ifname);
}
return false;
}
/* Accept point-to-point devices only if connect_to is specified */
if(connect_to != NULL and (ifr->ifr_flags & IFF_POINTOPOINT)){
if(debug){
fprintf_plus(stderr, "Accepting point-to-point interface"
" \"%s\"\n", ifname);
}
return true;
}
/* Otherwise, reject non-broadcast-capable devices */
if(not (ifr->ifr_flags & IFF_BROADCAST)){
if(debug){
fprintf_plus(stderr, "Rejecting non-broadcast interface"
" \"%s\"\n", ifname);
}
return false;
}
/* Reject non-ARP interfaces (including dummy interfaces) */
if(ifr->ifr_flags & IFF_NOARP){
if(debug){
fprintf_plus(stderr, "Rejecting non-ARP interface \"%s\"\n",
ifname);
}
return false;
}
/* Accept this device */
if(debug){
fprintf_plus(stderr, "Interface \"%s\" is good\n", ifname);
}
return true;
}
/*
* This function determines if a directory entry in /sys/class/net
* corresponds to an acceptable network device.
* (This function is passed to scandir(3) as a filter function.)
*/
__attribute__((nonnull, warn_unused_result))
int good_interface(const struct dirent *if_entry){
if(if_entry->d_name[0] == '.'){
return 0;
}
struct ifreq ifr;
if(not get_flags(if_entry->d_name, &ifr)){
if(debug){
fprintf_plus(stderr, "Failed to get flags for interface "
"\"%s\"\n", if_entry->d_name);
}
return 0;
}
if(not good_flags(if_entry->d_name, &ifr)){
return 0;
}
return 1;
}
/*
* This function determines if a network interface is up.
*/
__attribute__((nonnull, warn_unused_result))
bool interface_is_up(const char *interface){
struct ifreq ifr;
if(not get_flags(interface, &ifr)){
if(debug){
fprintf_plus(stderr, "Failed to get flags for interface "
"\"%s\"\n", interface);
}
return false;
}
return (bool)(ifr.ifr_flags & IFF_UP);
}
/*
* This function determines if a network interface is running
*/
__attribute__((nonnull, warn_unused_result))
bool interface_is_running(const char *interface){
struct ifreq ifr;
if(not get_flags(interface, &ifr)){
if(debug){
fprintf_plus(stderr, "Failed to get flags for interface "
"\"%s\"\n", interface);
}
return false;
}
return (bool)(ifr.ifr_flags & IFF_RUNNING);
}
__attribute__((nonnull, pure, warn_unused_result))
int notdotentries(const struct dirent *direntry){
/* Skip "." and ".." */
if(direntry->d_name[0] == '.'
and (direntry->d_name[1] == '\0'
or (direntry->d_name[1] == '.'
and direntry->d_name[2] == '\0'))){
return 0;
}
return 1;
}
/* Is this directory entry a runnable program? */
__attribute__((nonnull, warn_unused_result))
int runnable_hook(const struct dirent *direntry){
int ret;
size_t sret;
struct stat st;
if((direntry->d_name)[0] == '\0'){
/* Empty name? */
return 0;
}
sret = strspn(direntry->d_name, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789"
"_.-");
if((direntry->d_name)[sret] != '\0'){
/* Contains non-allowed characters */
if(debug){
fprintf_plus(stderr, "Ignoring hook \"%s\" with bad name\n",
direntry->d_name);
}
return 0;
}
ret = fstatat(hookdir_fd, direntry->d_name, &st, 0);
if(ret == -1){
if(debug){
perror_plus("Could not stat hook");
}
return 0;
}
if(not (S_ISREG(st.st_mode))){
/* Not a regular file */
if(debug){
fprintf_plus(stderr, "Ignoring hook \"%s\" - not a file\n",
direntry->d_name);
}
return 0;
}
if(not (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))){
/* Not executable */
if(debug){
fprintf_plus(stderr, "Ignoring hook \"%s\" - not executable\n",
direntry->d_name);
}
return 0;
}
if(debug){
fprintf_plus(stderr, "Hook \"%s\" is acceptable\n",
direntry->d_name);
}
return 1;
}
__attribute__((nonnull, warn_unused_result))
int avahi_loop_with_timeout(AvahiSimplePoll *s, int retry_interval,
mandos_context *mc){
int ret;
struct timespec now;
struct timespec waited_time;
intmax_t block_time;
while(true){
if(mc->current_server == NULL){
if(debug){
fprintf_plus(stderr, "Wait until first server is found."
" No timeout!\n");
}
ret = avahi_simple_poll_iterate(s, -1);
} else {
if(debug){
fprintf_plus(stderr, "Check current_server if we should run"
" it, or wait\n");
}
/* the current time */
ret = clock_gettime(CLOCK_MONOTONIC, &now);
if(ret == -1){
perror_plus("clock_gettime");
return -1;
}
/* Calculating in ms how long time between now and server
who we visted longest time ago. Now - last seen. */
waited_time.tv_sec = (now.tv_sec
- mc->current_server->last_seen.tv_sec);
waited_time.tv_nsec = (now.tv_nsec
- mc->current_server->last_seen.tv_nsec);
/* total time is 10s/10,000ms.
Converting to s from ms by dividing by 1,000,
and ns to ms by dividing by 1,000,000. */
block_time = ((retry_interval
- ((intmax_t)waited_time.tv_sec * 1000))
- ((intmax_t)waited_time.tv_nsec / 1000000));
if(debug){
fprintf_plus(stderr, "Blocking for %" PRIdMAX " ms\n",
block_time);
}
if(block_time <= 0){
ret = start_mandos_communication(mc->current_server->ip,
mc->current_server->port,
mc->current_server->if_index,
mc->current_server->af, mc);
if(ret == 0){
avahi_simple_poll_quit(s);
return 0;
}
ret = clock_gettime(CLOCK_MONOTONIC,
&mc->current_server->last_seen);
if(ret == -1){
perror_plus("clock_gettime");
return -1;
}
mc->current_server = mc->current_server->next;
block_time = 0; /* Call avahi to find new Mandos
servers, but don't block */
}
ret = avahi_simple_poll_iterate(s, (int)block_time);
}
if(ret != 0){
if(ret > 0 or errno != EINTR){
return (ret != 1) ? ret : 0;
}
}
}
}
__attribute__((nonnull))
void run_network_hooks(const char *mode, const char *interface,
const float delay){
struct dirent **direntries = NULL;
if(hookdir_fd == -1){
hookdir_fd = open(hookdir, O_RDONLY | O_DIRECTORY | O_PATH
| O_CLOEXEC);
if(hookdir_fd == -1){
if(errno == ENOENT){
if(debug){
fprintf_plus(stderr, "Network hook directory \"%s\" not"
" found\n", hookdir);
}
} else {
perror_plus("open");
}
return;
}
}
int devnull = (int)TEMP_FAILURE_RETRY(open("/dev/null", O_RDONLY));
if(devnull == -1){
perror_plus("open(\"/dev/null\", O_RDONLY)");
return;
}
int numhooks = scandirat(hookdir_fd, ".", &direntries,
runnable_hook, alphasort);
if(numhooks == -1){
perror_plus("scandir");
close(devnull);
return;
}
struct dirent *direntry;
int ret;
for(int i = 0; i < numhooks; i++){
direntry = direntries[i];
if(debug){
fprintf_plus(stderr, "Running network hook \"%s\"\n",
direntry->d_name);
}
pid_t hook_pid = fork();
if(hook_pid == 0){
/* Child */
/* Raise privileges */
errno = raise_privileges_permanently();
if(errno != 0){
perror_plus("Failed to raise privileges");
_exit(EX_NOPERM);
}
/* Set group */
errno = 0;
ret = setgid(0);
if(ret == -1){
perror_plus("setgid");
_exit(EX_NOPERM);
}
/* Reset supplementary groups */
errno = 0;
ret = setgroups(0, NULL);
if(ret == -1){
perror_plus("setgroups");
_exit(EX_NOPERM);
}
ret = setenv("MANDOSNETHOOKDIR", hookdir, 1);
if(ret == -1){
perror_plus("setenv");
_exit(EX_OSERR);
}
ret = setenv("DEVICE", interface, 1);
if(ret == -1){
perror_plus("setenv");
_exit(EX_OSERR);
}
ret = setenv("VERBOSITY", debug ? "1" : "0", 1);
if(ret == -1){
perror_plus("setenv");
_exit(EX_OSERR);
}
ret = setenv("MODE", mode, 1);
if(ret == -1){
perror_plus("setenv");
_exit(EX_OSERR);
}
char *delaystring;
ret = asprintf(&delaystring, "%f", (double)delay);
if(ret == -1){
perror_plus("asprintf");
_exit(EX_OSERR);
}
ret = setenv("DELAY", delaystring, 1);
if(ret == -1){
free(delaystring);
perror_plus("setenv");
_exit(EX_OSERR);
}
free(delaystring);
if(connect_to != NULL){
ret = setenv("CONNECT", connect_to, 1);
if(ret == -1){
perror_plus("setenv");
_exit(EX_OSERR);
}
}
int hook_fd = (int)TEMP_FAILURE_RETRY(openat(hookdir_fd,
direntry->d_name,
O_RDONLY));
if(hook_fd == -1){
perror_plus("openat");
_exit(EXIT_FAILURE);
}
if(close(hookdir_fd) == -1){
perror_plus("close");
_exit(EXIT_FAILURE);
}
ret = dup2(devnull, STDIN_FILENO);
if(ret == -1){
perror_plus("dup2(devnull, STDIN_FILENO)");
_exit(EX_OSERR);
}
ret = close(devnull);
if(ret == -1){
perror_plus("close");
_exit(EX_OSERR);
}
ret = dup2(STDERR_FILENO, STDOUT_FILENO);
if(ret == -1){
perror_plus("dup2(STDERR_FILENO, STDOUT_FILENO)");
_exit(EX_OSERR);
}
if(fexecve(hook_fd, (char *const []){ direntry->d_name, NULL },
environ) == -1){
perror_plus("fexecve");
_exit(EXIT_FAILURE);
}
} else {
if(hook_pid == -1){
perror_plus("fork");
free(direntry);
continue;
}
int status;
if(TEMP_FAILURE_RETRY(waitpid(hook_pid, &status, 0)) == -1){
perror_plus("waitpid");
free(direntry);
continue;
}
if(WIFEXITED(status)){
if(WEXITSTATUS(status) != 0){
fprintf_plus(stderr, "Warning: network hook \"%s\" exited"
" with status %d\n", direntry->d_name,
WEXITSTATUS(status));
free(direntry);
continue;
}
} else if(WIFSIGNALED(status)){
fprintf_plus(stderr, "Warning: network hook \"%s\" died by"
" signal %d\n", direntry->d_name,
WTERMSIG(status));
free(direntry);
continue;
} else {
fprintf_plus(stderr, "Warning: network hook \"%s\""
" crashed\n", direntry->d_name);
free(direntry);
continue;
}
}
if(debug){
fprintf_plus(stderr, "Network hook \"%s\" ran successfully\n",
direntry->d_name);
}
free(direntry);
}
free(direntries);
if(close(hookdir_fd) == -1){
perror_plus("close");
} else {
hookdir_fd = -1;
}
close(devnull);
}
__attribute__((nonnull, warn_unused_result))
int bring_up_interface(const char *const interface,
const float delay){
int old_errno = errno;
int ret;
struct ifreq network;
unsigned int if_index = if_nametoindex(interface);
if(if_index == 0){
fprintf_plus(stderr, "No such interface: \"%s\"\n", interface);
errno = old_errno;
return ENXIO;
}
if(quit_now){
errno = old_errno;
return EINTR;
}
if(not interface_is_up(interface)){
int ret_errno = 0;
int ioctl_errno = 0;
if(not get_flags(interface, &network)){
ret_errno = errno;
fprintf_plus(stderr, "Failed to get flags for interface "
"\"%s\"\n", interface);
errno = old_errno;
return ret_errno;
}
network.ifr_flags |= IFF_UP; /* set flag */
int sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP);
if(sd == -1){
ret_errno = errno;
perror_plus("socket");
errno = old_errno;
return ret_errno;
}
if(quit_now){
ret = close(sd);
if(ret == -1){
perror_plus("close");
}
errno = old_errno;
return EINTR;
}
if(debug){
fprintf_plus(stderr, "Bringing up interface \"%s\"\n",
interface);
}
/* Raise privileges */
ret_errno = raise_privileges();
if(ret_errno != 0){
errno = ret_errno;
perror_plus("Failed to raise privileges");
}
#ifdef __linux__
int ret_linux;
bool restore_loglevel = false;
if(ret_errno == 0){
/* Lower kernel loglevel to KERN_NOTICE to avoid KERN_INFO
messages about the network interface to mess up the prompt */
ret_linux = klogctl(8, NULL, 5);
if(ret_linux == -1){
perror_plus("klogctl");
} else {
restore_loglevel = true;
}
}
#endif /* __linux__ */
int ret_setflags = ioctl(sd, SIOCSIFFLAGS, &network);
ioctl_errno = errno;
#ifdef __linux__
if(restore_loglevel){
ret_linux = klogctl(7, NULL, 0);
if(ret_linux == -1){
perror_plus("klogctl");
}
}
#endif /* __linux__ */
/* If raise_privileges() succeeded above */
if(ret_errno == 0){
/* Lower privileges */
ret_errno = lower_privileges();
if(ret_errno != 0){
errno = ret_errno;
perror_plus("Failed to lower privileges");
}
}
/* Close the socket */
ret = close(sd);
if(ret == -1){
perror_plus("close");
}
if(ret_setflags == -1){
errno = ioctl_errno;
perror_plus("ioctl SIOCSIFFLAGS +IFF_UP");
errno = old_errno;
return ioctl_errno;
}
} else if(debug){
fprintf_plus(stderr, "Interface \"%s\" is already up; good\n",
interface);
}
/* Sleep checking until interface is running.
Check every 0.25s, up to total time of delay */
for(int i = 0; i < delay * 4; i++){
if(interface_is_running(interface)){
break;
}
struct timespec sleeptime = { .tv_nsec = 250000000 };
ret = nanosleep(&sleeptime, NULL);
if(ret == -1 and errno != EINTR){
perror_plus("nanosleep");
}
}
errno = old_errno;
return 0;
}
__attribute__((nonnull, warn_unused_result))
int take_down_interface(const char *const interface){
int old_errno = errno;
struct ifreq network;
unsigned int if_index = if_nametoindex(interface);
if(if_index == 0){
fprintf_plus(stderr, "No such interface: \"%s\"\n", interface);
errno = old_errno;
return ENXIO;
}
if(interface_is_up(interface)){
int ret_errno = 0;
int ioctl_errno = 0;
if(not get_flags(interface, &network) and debug){
ret_errno = errno;
fprintf_plus(stderr, "Failed to get flags for interface "
"\"%s\"\n", interface);
errno = old_errno;
return ret_errno;
}
network.ifr_flags &= ~(short)IFF_UP; /* clear flag */
int sd = socket(PF_INET6, SOCK_DGRAM, IPPROTO_IP);
if(sd == -1){
ret_errno = errno;
perror_plus("socket");
errno = old_errno;
return ret_errno;
}
if(debug){
fprintf_plus(stderr, "Taking down interface \"%s\"\n",
interface);
}
/* Raise privileges */
ret_errno = raise_privileges();
if(ret_errno != 0){
errno = ret_errno;
perror_plus("Failed to raise privileges");
}
int ret_setflags = ioctl(sd, SIOCSIFFLAGS, &network);
ioctl_errno = errno;
/* If raise_privileges() succeeded above */
if(ret_errno == 0){
/* Lower privileges */
ret_errno = lower_privileges();
if(ret_errno != 0){
errno = ret_errno;
perror_plus("Failed to lower privileges");
}
}
/* Close the socket */
int ret = close(sd);
if(ret == -1){
perror_plus("close");
}
if(ret_setflags == -1){
errno = ioctl_errno;
perror_plus("ioctl SIOCSIFFLAGS -IFF_UP");
errno = old_errno;
return ioctl_errno;
}
} else if(debug){
fprintf_plus(stderr, "Interface \"%s\" is already down; odd\n",
interface);
}
errno = old_errno;
return 0;
}
int main(int argc, char *argv[]){
mandos_context mc = { .server = NULL, .dh_bits = 0,
#if GNUTLS_VERSION_NUMBER >= 0x030606
.priority = "SECURE128:!CTYPE-X.509"
":+CTYPE-RAWPK:!RSA:!VERS-ALL:+VERS-TLS1.3"
":%PROFILE_ULTRA",
#elif GNUTLS_VERSION_NUMBER < 0x030600
.priority = "SECURE256:!CTYPE-X.509"
":+CTYPE-OPENPGP:!RSA:+SIGN-DSA-SHA256",
#else
#error "Needs GnuTLS 3.6.6 or later, or before 3.6.0"
#endif
.current_server = NULL, .interfaces = NULL,
.interfaces_size = 0 };
AvahiSServiceBrowser *sb = NULL;
error_t ret_errno;
int ret;
intmax_t tmpmax;
char *tmp;
int exitcode = EXIT_SUCCESS;
char *interfaces_to_take_down = NULL;
size_t interfaces_to_take_down_size = 0;
char run_tempdir[] = "/run/tmp/mandosXXXXXX";
char old_tempdir[] = "/tmp/mandosXXXXXX";
char *tempdir = NULL;
AvahiIfIndex if_index = AVAHI_IF_UNSPEC;
const char *seckey = PATHDIR "/" SECKEY;
const char *pubkey = PATHDIR "/" PUBKEY;
#if GNUTLS_VERSION_NUMBER >= 0x030606
const char *tls_privkey = PATHDIR "/" TLS_PRIVKEY;
const char *tls_pubkey = PATHDIR "/" TLS_PUBKEY;
#endif
const char *dh_params_file = NULL;
char *interfaces_hooks = NULL;
bool gnutls_initialized = false;
bool gpgme_initialized = false;
float delay = 2.5f;
double retry_interval = 10; /* 10s between trying a server and
retrying the same server again */
struct sigaction old_sigterm_action = { .sa_handler = SIG_DFL };
struct sigaction sigterm_action = { .sa_handler = handle_sigterm };
uid = getuid();
gid = getgid();
/* Lower any group privileges we might have, just to be safe */
errno = 0;
ret = setgid(gid);
if(ret == -1){
perror_plus("setgid");
}
/* Lower user privileges (temporarily) */
errno = 0;
ret = seteuid(uid);
if(ret == -1){
perror_plus("seteuid");
}
if(quit_now){
goto end;
}
{
struct argp_option options[] = {
{ .name = "debug", .key = 128,
.doc = "Debug mode", .group = 3 },
{ .name = "connect", .key = 'c',
.arg = "ADDRESS:PORT",
.doc = "Connect directly to a specific Mandos server",
.group = 1 },
{ .name = "interface", .key = 'i',
.arg = "NAME",
.doc = "Network interface that will be used to search for"
" Mandos servers",
.group = 1 },
{ .name = "seckey", .key = 's',
.arg = "FILE",
.doc = "OpenPGP secret key file base name",
.group = 1 },
{ .name = "pubkey", .key = 'p',
.arg = "FILE",
.doc = "OpenPGP public key file base name",
.group = 1 },
{ .name = "tls-privkey", .key = 't',
.arg = "FILE",
#if GNUTLS_VERSION_NUMBER >= 0x030606
.doc = "TLS private key file base name",
#else
.doc = "Dummy; ignored (requires GnuTLS 3.6.6)",
#endif
.group = 1 },
{ .name = "tls-pubkey", .key = 'T',
.arg = "FILE",
#if GNUTLS_VERSION_NUMBER >= 0x030606
.doc = "TLS public key file base name",
#else
.doc = "Dummy; ignored (requires GnuTLS 3.6.6)",
#endif
.group = 1 },
{ .name = "dh-bits", .key = 129,
.arg = "BITS",
.doc = "Bit length of the prime number used in the"
" Diffie-Hellman key exchange",
.group = 2 },
{ .name = "dh-params", .key = 134,
.arg = "FILE",
.doc = "PEM-encoded PKCS#3 file with pre-generated parameters"
" for the Diffie-Hellman key exchange",
.group = 2 },
{ .name = "priority", .key = 130,
.arg = "STRING",
.doc = "GnuTLS priority string for the TLS handshake",
.group = 1 },
{ .name = "delay", .key = 131,
.arg = "SECONDS",
.doc = "Maximum delay to wait for interface startup",
.group = 2 },
{ .name = "retry", .key = 132,
.arg = "SECONDS",
.doc = "Retry interval used when denied by the Mandos server",
.group = 2 },
{ .name = "network-hook-dir", .key = 133,
.arg = "DIR",
.doc = "Directory where network hooks are located",
.group = 2 },
/*
* These reproduce what we would get without ARGP_NO_HELP
*/
{ .name = "help", .key = '?',
.doc = "Give this help list", .group = -1 },
{ .name = "usage", .key = -3,
.doc = "Give a short usage message", .group = -1 },
{ .name = "version", .key = 'V',
.doc = "Print program version", .group = -1 },
{ .name = NULL }
};
error_t parse_opt(int key, char *arg,
struct argp_state *state){
errno = 0;
switch(key){
case 128: /* --debug */
debug = true;
break;
case 'c': /* --connect */
connect_to = arg;
break;
case 'i': /* --interface */
ret_errno = argz_add_sep(&mc.interfaces, &mc.interfaces_size,
arg, (int)',');
if(ret_errno != 0){
argp_error(state, "%s", strerror(ret_errno));
}
break;
case 's': /* --seckey */
seckey = arg;
break;
case 'p': /* --pubkey */
pubkey = arg;
break;
case 't': /* --tls-privkey */
#if GNUTLS_VERSION_NUMBER >= 0x030606
tls_privkey = arg;
#endif
break;
case 'T': /* --tls-pubkey */
#if GNUTLS_VERSION_NUMBER >= 0x030606
tls_pubkey = arg;
#endif
break;
case 129: /* --dh-bits */
errno = 0;
tmpmax = strtoimax(arg, &tmp, 10);
if(errno != 0 or tmp == arg or *tmp != '\0'
or tmpmax != (typeof(mc.dh_bits))tmpmax){
argp_error(state, "Bad number of DH bits");
}
mc.dh_bits = (typeof(mc.dh_bits))tmpmax;
break;
case 134: /* --dh-params */
dh_params_file = arg;
break;
case 130: /* --priority */
mc.priority = arg;
break;
case 131: /* --delay */
errno = 0;
delay = strtof(arg, &tmp);
if(errno != 0 or tmp == arg or *tmp != '\0'){
argp_error(state, "Bad delay");
}
case 132: /* --retry */
errno = 0;
retry_interval = strtod(arg, &tmp);
if(errno != 0 or tmp == arg or *tmp != '\0'
or (retry_interval * 1000) > INT_MAX
or retry_interval < 0){
argp_error(state, "Bad retry interval");
}
break;
case 133: /* --network-hook-dir */
hookdir = arg;
break;
/*
* These reproduce what we would get without ARGP_NO_HELP
*/
case '?': /* --help */
argp_state_help(state, state->out_stream,
(ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR)
& ~(unsigned int)ARGP_HELP_EXIT_OK);
__builtin_unreachable();
case -3: /* --usage */
argp_state_help(state, state->out_stream,
ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR);
__builtin_unreachable();
case 'V': /* --version */
fprintf_plus(state->out_stream, "%s\n", argp_program_version);
exit(argp_err_exit_status);
break;
default:
return ARGP_ERR_UNKNOWN;
}
return errno;
}
struct argp argp = { .options = options, .parser = parse_opt,
.args_doc = "",
.doc = "Mandos client -- Get and decrypt"
" passwords from a Mandos server" };
ret_errno = argp_parse(&argp, argc, argv,
ARGP_IN_ORDER | ARGP_NO_HELP, 0, NULL);
switch(ret_errno){
case 0:
break;
case ENOMEM:
default:
errno = ret_errno;
perror_plus("argp_parse");
exitcode = EX_OSERR;
goto end;
case EINVAL:
exitcode = EX_USAGE;
goto end;
}
}
{
/* Re-raise privileges */
ret = raise_privileges();
if(ret != 0){
errno = ret;
perror_plus("Failed to raise privileges");
} else {
struct stat st;
/* Work around Debian bug #633582:
*/
if(strcmp(seckey, PATHDIR "/" SECKEY) == 0){
int seckey_fd = open(seckey, O_RDONLY);
if(seckey_fd == -1){
perror_plus("open");
} else {
ret = (int)TEMP_FAILURE_RETRY(fstat(seckey_fd, &st));
if(ret == -1){
perror_plus("fstat");
} else {
if(S_ISREG(st.st_mode)
and st.st_uid == 0 and st.st_gid == 0){
ret = fchown(seckey_fd, uid, gid);
if(ret == -1){
perror_plus("fchown");
}
}
}
close(seckey_fd);
}
}
if(strcmp(pubkey, PATHDIR "/" PUBKEY) == 0){
int pubkey_fd = open(pubkey, O_RDONLY);
if(pubkey_fd == -1){
perror_plus("open");
} else {
ret = (int)TEMP_FAILURE_RETRY(fstat(pubkey_fd, &st));
if(ret == -1){
perror_plus("fstat");
} else {
if(S_ISREG(st.st_mode)
and st.st_uid == 0 and st.st_gid == 0){
ret = fchown(pubkey_fd, uid, gid);
if(ret == -1){
perror_plus("fchown");
}
}
}
close(pubkey_fd);
}
}
if(dh_params_file != NULL
and strcmp(dh_params_file, PATHDIR "/dhparams.pem" ) == 0){
int dhparams_fd = open(dh_params_file, O_RDONLY);
if(dhparams_fd == -1){
perror_plus("open");
} else {
ret = (int)TEMP_FAILURE_RETRY(fstat(dhparams_fd, &st));
if(ret == -1){
perror_plus("fstat");
} else {
if(S_ISREG(st.st_mode)
and st.st_uid == 0 and st.st_gid == 0){
ret = fchown(dhparams_fd, uid, gid);
if(ret == -1){
perror_plus("fchown");
}
}
}
close(dhparams_fd);
}
}
/* Work around Debian bug #981302
*/
if(lstat("/dev/fd", &st) != 0 and errno == ENOENT){
ret = symlink("/proc/self/fd", "/dev/fd");
if(ret == -1){
perror_plus("Failed to create /dev/fd symlink");
}
}
/* Lower privileges */
ret = lower_privileges();
if(ret != 0){
errno = ret;
perror_plus("Failed to lower privileges");
}
}
}
/* Remove invalid interface names (except "none") */
{
char *interface = NULL;
while((interface = argz_next(mc.interfaces, mc.interfaces_size,
interface))){
if(strcmp(interface, "none") != 0
and if_nametoindex(interface) == 0){
if(interface[0] != '\0'){
fprintf_plus(stderr, "Not using nonexisting interface"
" \"%s\"\n", interface);
}
argz_delete(&mc.interfaces, &mc.interfaces_size, interface);
interface = NULL;
}
}
}
/* Run network hooks */
{
if(mc.interfaces != NULL){
interfaces_hooks = malloc(mc.interfaces_size);
if(interfaces_hooks == NULL){
perror_plus("malloc");
goto end;
}
memcpy(interfaces_hooks, mc.interfaces, mc.interfaces_size);
argz_stringify(interfaces_hooks, mc.interfaces_size, (int)',');
}
run_network_hooks("start", interfaces_hooks != NULL ?
interfaces_hooks : "", delay);
}
if(not debug){
avahi_set_log_function(empty_log);
}
/* Initialize Avahi early so avahi_simple_poll_quit() can be called
from the signal handler */
/* Initialize the pseudo-RNG for Avahi */
srand((unsigned int) time(NULL));
simple_poll = avahi_simple_poll_new();
if(simple_poll == NULL){
fprintf_plus(stderr,
"Avahi: Failed to create simple poll object.\n");
exitcode = EX_UNAVAILABLE;
goto end;
}
sigemptyset(&sigterm_action.sa_mask);
ret = sigaddset(&sigterm_action.sa_mask, SIGINT);
if(ret == -1){
perror_plus("sigaddset");
exitcode = EX_OSERR;
goto end;
}
ret = sigaddset(&sigterm_action.sa_mask, SIGHUP);
if(ret == -1){
perror_plus("sigaddset");
exitcode = EX_OSERR;
goto end;
}
ret = sigaddset(&sigterm_action.sa_mask, SIGTERM);
if(ret == -1){
perror_plus("sigaddset");
exitcode = EX_OSERR;
goto end;
}
/* Need to check if the handler is SIG_IGN before handling:
| [[info:libc:Initial Signal Actions]] |
| [[info:libc:Basic Signal Handling]] |
*/
ret = sigaction(SIGINT, NULL, &old_sigterm_action);
if(ret == -1){
perror_plus("sigaction");
return EX_OSERR;
}
if(old_sigterm_action.sa_handler != SIG_IGN){
ret = sigaction(SIGINT, &sigterm_action, NULL);
if(ret == -1){
perror_plus("sigaction");
exitcode = EX_OSERR;
goto end;
}
}
ret = sigaction(SIGHUP, NULL, &old_sigterm_action);
if(ret == -1){
perror_plus("sigaction");
return EX_OSERR;
}
if(old_sigterm_action.sa_handler != SIG_IGN){
ret = sigaction(SIGHUP, &sigterm_action, NULL);
if(ret == -1){
perror_plus("sigaction");
exitcode = EX_OSERR;
goto end;
}
}
ret = sigaction(SIGTERM, NULL, &old_sigterm_action);
if(ret == -1){
perror_plus("sigaction");
return EX_OSERR;
}
if(old_sigterm_action.sa_handler != SIG_IGN){
ret = sigaction(SIGTERM, &sigterm_action, NULL);
if(ret == -1){
perror_plus("sigaction");
exitcode = EX_OSERR;
goto end;
}
}
/* If no interfaces were specified, make a list */
if(mc.interfaces == NULL){
struct dirent **direntries = NULL;
/* Look for any good interfaces */
ret = scandir(sys_class_net, &direntries, good_interface,
alphasort);
if(ret >= 1){
/* Add all found interfaces to interfaces list */
for(int i = 0; i < ret; ++i){
ret_errno = argz_add(&mc.interfaces, &mc.interfaces_size,
direntries[i]->d_name);
if(ret_errno != 0){
errno = ret_errno;
perror_plus("argz_add");
free(direntries[i]);
continue;
}
if(debug){
fprintf_plus(stderr, "Will use interface \"%s\"\n",
direntries[i]->d_name);
}
free(direntries[i]);
}
free(direntries);
} else {
if(ret == 0){
free(direntries);
}
fprintf_plus(stderr, "Could not find a network interface\n");
exitcode = EXIT_FAILURE;
goto end;
}
}
/* Bring up interfaces which are down, and remove any "none"s */
{
char *interface = NULL;
while((interface = argz_next(mc.interfaces, mc.interfaces_size,
interface))){
/* If interface name is "none", stop bringing up interfaces.
Also remove all instances of "none" from the list */
if(strcmp(interface, "none") == 0){
argz_delete(&mc.interfaces, &mc.interfaces_size,
interface);
interface = NULL;
while((interface = argz_next(mc.interfaces,
mc.interfaces_size, interface))){
if(strcmp(interface, "none") == 0){
argz_delete(&mc.interfaces, &mc.interfaces_size,
interface);
interface = NULL;
}
}
break;
}
bool interface_was_up = interface_is_up(interface);
errno = bring_up_interface(interface, delay);
if(not interface_was_up){
if(errno != 0){
fprintf_plus(stderr, "Failed to bring up interface \"%s\":"
" %s\n", interface, strerror(errno));
} else {
errno = argz_add(&interfaces_to_take_down,
&interfaces_to_take_down_size,
interface);
if(errno != 0){
perror_plus("argz_add");
}
}
}
}
if(debug and (interfaces_to_take_down == NULL)){
fprintf_plus(stderr, "No interfaces were brought up\n");
}
}
/* If we only got one interface, explicitly use only that one */
if(argz_count(mc.interfaces, mc.interfaces_size) == 1){
if(debug){
fprintf_plus(stderr, "Using only interface \"%s\"\n",
mc.interfaces);
}
if_index = (AvahiIfIndex)if_nametoindex(mc.interfaces);
}
if(quit_now){
goto end;
}
#if GNUTLS_VERSION_NUMBER >= 0x030606
ret = init_gnutls_global(tls_pubkey, tls_privkey, dh_params_file, &mc);
#elif GNUTLS_VERSION_NUMBER < 0x030600
ret = init_gnutls_global(pubkey, seckey, dh_params_file, &mc);
#else
#error "Needs GnuTLS 3.6.6 or later, or before 3.6.0"
#endif
if(ret == -1){
fprintf_plus(stderr, "init_gnutls_global failed\n");
exitcode = EX_UNAVAILABLE;
goto end;
} else {
gnutls_initialized = true;
}
if(quit_now){
goto end;
}
/* Try /run/tmp before /tmp */
tempdir = mkdtemp(run_tempdir);
if(tempdir == NULL and errno == ENOENT){
if(debug){
fprintf_plus(stderr, "Tempdir %s did not work, trying %s\n",
run_tempdir, old_tempdir);
}
tempdir = mkdtemp(old_tempdir);
}
if(tempdir == NULL){
perror_plus("mkdtemp");
goto end;
}
if(quit_now){
goto end;
}
if(not init_gpgme(pubkey, seckey, tempdir, &mc)){
fprintf_plus(stderr, "init_gpgme failed\n");
exitcode = EX_UNAVAILABLE;
goto end;
} else {
gpgme_initialized = true;
}
if(quit_now){
goto end;
}
if(connect_to != NULL){
/* Connect directly, do not use Zeroconf */
/* (Mainly meant for debugging) */
char *address = strrchr(connect_to, ':');
if(address == NULL){
fprintf_plus(stderr, "No colon in address\n");
exitcode = EX_USAGE;
goto end;
}
if(quit_now){
goto end;
}
in_port_t port;
errno = 0;
tmpmax = strtoimax(address+1, &tmp, 10);
if(errno != 0 or tmp == address+1 or *tmp != '\0'
or tmpmax != (in_port_t)tmpmax){
fprintf_plus(stderr, "Bad port number\n");
exitcode = EX_USAGE;
goto end;
}
if(quit_now){
goto end;
}
port = (in_port_t)tmpmax;
*address = '\0';
/* Colon in address indicates IPv6 */
int af;
if(strchr(connect_to, ':') != NULL){
af = AF_INET6;
/* Accept [] around IPv6 address - see RFC 5952 */
if(connect_to[0] == '[' and address[-1] == ']')
{
connect_to++;
address[-1] = '\0';
}
} else {
af = AF_INET;
}
address = connect_to;
if(quit_now){
goto end;
}
while(not quit_now){
ret = start_mandos_communication(address, port, if_index, af,
&mc);
if(quit_now or ret == 0){
break;
}
if(debug){
fprintf_plus(stderr, "Retrying in %d seconds\n",
(int)retry_interval);
}
sleep((unsigned int)retry_interval);
}
if(not quit_now){
exitcode = EXIT_SUCCESS;
}
goto end;
}
if(quit_now){
goto end;
}
{
AvahiServerConfig config;
/* Do not publish any local Zeroconf records */
avahi_server_config_init(&config);
config.publish_hinfo = 0;
config.publish_addresses = 0;
config.publish_workstation = 0;
config.publish_domain = 0;
/* Allocate a new server */
mc.server = avahi_server_new(avahi_simple_poll_get(simple_poll),
&config, NULL, NULL, &ret);
/* Free the Avahi configuration data */
avahi_server_config_free(&config);
}
/* Check if creating the Avahi server object succeeded */
if(mc.server == NULL){
fprintf_plus(stderr, "Failed to create Avahi server: %s\n",
avahi_strerror(ret));
exitcode = EX_UNAVAILABLE;
goto end;
}
if(quit_now){
goto end;
}
/* Create the Avahi service browser */
sb = avahi_s_service_browser_new(mc.server, if_index,
AVAHI_PROTO_UNSPEC, "_mandos._tcp",
NULL, 0, browse_callback,
(void *)&mc);
if(sb == NULL){
fprintf_plus(stderr, "Failed to create service browser: %s\n",
avahi_strerror(avahi_server_errno(mc.server)));
exitcode = EX_UNAVAILABLE;
goto end;
}
if(quit_now){
goto end;
}
/* Run the main loop */
if(debug){
fprintf_plus(stderr, "Starting Avahi loop search\n");
}
ret = avahi_loop_with_timeout(simple_poll,
(int)(retry_interval * 1000), &mc);
if(debug){
fprintf_plus(stderr, "avahi_loop_with_timeout exited %s\n",
(ret == 0) ? "successfully" : "with error");
}
end:
if(debug){
if(signal_received){
fprintf_plus(stderr, "%s exiting due to signal %d: %s\n",
argv[0], signal_received,
strsignal(signal_received));
} else {
fprintf_plus(stderr, "%s exiting\n", argv[0]);
}
}
/* Cleanup things */
free(mc.interfaces);
if(sb != NULL)
avahi_s_service_browser_free(sb);
if(mc.server != NULL)
avahi_server_free(mc.server);
if(simple_poll != NULL)
avahi_simple_poll_free(simple_poll);
if(gnutls_initialized){
gnutls_certificate_free_credentials(mc.cred);
gnutls_dh_params_deinit(mc.dh_params);
}
if(gpgme_initialized){
gpgme_release(mc.ctx);
}
/* Cleans up the circular linked list of Mandos servers the client
has seen */
if(mc.current_server != NULL){
mc.current_server->prev->next = NULL;
while(mc.current_server != NULL){
server *next = mc.current_server->next;
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcast-qual"
#endif
free((char *)(mc.current_server->ip));
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
free(mc.current_server);
mc.current_server = next;
}
}
/* Re-raise privileges */
{
ret = raise_privileges();
if(ret != 0){
errno = ret;
perror_plus("Failed to raise privileges");
} else {
/* Run network hooks */
run_network_hooks("stop", interfaces_hooks != NULL ?
interfaces_hooks : "", delay);
/* Take down the network interfaces which were brought up */
{
char *interface = NULL;
while((interface = argz_next(interfaces_to_take_down,
interfaces_to_take_down_size,
interface))){
ret = take_down_interface(interface);
if(ret != 0){
errno = ret;
perror_plus("Failed to take down interface");
}
}
if(debug and (interfaces_to_take_down == NULL)){
fprintf_plus(stderr, "No interfaces needed to be taken"
" down\n");
}
}
}
ret = lower_privileges_permanently();
if(ret != 0){
errno = ret;
perror_plus("Failed to lower privileges permanently");
}
}
free(interfaces_to_take_down);
free(interfaces_hooks);
void clean_dir_at(int base, const char * const dirname,
uintmax_t level){
struct dirent **direntries = NULL;
int dret;
int dir_fd = (int)TEMP_FAILURE_RETRY(openat(base, dirname,
O_RDONLY
| O_NOFOLLOW
| O_DIRECTORY
| O_PATH));
if(dir_fd == -1){
perror_plus("open");
return;
}
int numentries = scandirat(dir_fd, ".", &direntries,
notdotentries, alphasort);
if(numentries >= 0){
for(int i = 0; i < numentries; i++){
if(debug){
fprintf_plus(stderr, "Unlinking \"%s/%s\"\n",
dirname, direntries[i]->d_name);
}
dret = unlinkat(dir_fd, direntries[i]->d_name, 0);
if(dret == -1){
if(errno == EISDIR){
dret = unlinkat(dir_fd, direntries[i]->d_name,
AT_REMOVEDIR);
}
if((dret == -1) and (errno == ENOTEMPTY)
and (strcmp(direntries[i]->d_name, "private-keys-v1.d")
== 0) and (level == 0)){
/* Recurse only in this special case */
clean_dir_at(dir_fd, direntries[i]->d_name, level+1);
dret = 0;
}
if((dret == -1) and (errno != ENOENT)){
fprintf_plus(stderr, "unlink(\"%s/%s\"): %s\n", dirname,
direntries[i]->d_name, strerror(errno));
}
}
free(direntries[i]);
}
/* need to clean even if 0 because man page doesn't specify */
free(direntries);
dret = unlinkat(base, dirname, AT_REMOVEDIR);
if(dret == -1 and errno != ENOENT){
perror_plus("rmdir");
}
} else {
perror_plus("scandirat");
}
close(dir_fd);
}
/* Removes the GPGME temp directory and all files inside */
if(tempdir != NULL){
clean_dir_at(-1, tempdir, 0);
}
if(quit_now){
sigemptyset(&old_sigterm_action.sa_mask);
old_sigterm_action.sa_handler = SIG_DFL;
ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received,
&old_sigterm_action,
NULL));
if(ret == -1){
perror_plus("sigaction");
}
do {
ret = raise(signal_received);
} while(ret != 0 and errno == EINTR);
if(ret != 0){
perror_plus("raise");
abort();
}
TEMP_FAILURE_RETRY(pause());
}
return exitcode;
}
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/mandos-client.xml 0000664 0001750 0001750 00000101552 14720643017 017332 0 ustar 00teddy teddy
%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;
8mandos
&COMMANDNAME;
Client for Mandos
&COMMANDNAME;
&COMMANDNAME;
&COMMANDNAME;
&COMMANDNAME;
DESCRIPTION
&COMMANDNAME; is a client program that
communicates with mandos8
to get a password. In slightly more detail, this client program
brings up network interfaces, uses the interfaces’ IPv6
link-local addresses to get network connectivity, uses Zeroconf
to find servers on the local network, and communicates with
servers using TLS with a raw public key to ensure authenticity
and confidentiality. This client program keeps running, trying
all servers on the network, until it receives a satisfactory
reply or a TERM signal. After all servers have been tried, all
servers are periodically retried. If no servers are found it
will wait indefinitely for new servers to appear.
The network interfaces are selected like this: If any interfaces
are specified using the option,
those interface are used. Otherwise,
&COMMANDNAME; will use all interfaces that
are not loopback interfaces, are not point-to-point interfaces,
are capable of broadcasting and do not have the NOARP flag (see
netdevice
7). (If the
option is used, point-to-point
interfaces and non-broadcast interfaces are accepted.) If any
used interfaces are not up and running, they are first taken up
(and later taken down again on program exit).
Before network interfaces are selected, all network
hooks
are run; see .
This program is not meant to be run directly; it is really meant
to be run by other programs in the initial
RAM disk environment; see .
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
This program is commonly not invoked from the command line; it
is normally started by another program as described in . Any command line options this program
accepts are therefore normally provided by the invoking program,
and not directly.
Do not use Zeroconf to locate servers. Connect directly
to only one specified Mandos
server. Note that an IPv6 address has colon characters in
it, so the last colon character is
assumed to separate the address from the port number.
Normally, Zeroconf would be used to locate Mandos servers,
in which case this option would only be used when testing
and debugging.
Comma separated list of network interfaces that will be
brought up and scanned for Mandos servers to connect to.
The default is the empty string, which will automatically
use all appropriate interfaces.
If the option is used, and
exactly one interface name is specified (except
none
), this specifies
the interface to use to connect to the address given.
Note that since this program will normally run in the
initial RAM disk environment, the interface must be an
interface which exists at that stage. Thus, the interface
can normally not be a pseudo-interface such as
br0
or tun0
; such interfaces
will not exist until much later in the boot process, and
can not be used by this program, unless created by a
network hook
— see .
NAME can be the string
none
; this will make
&COMMANDNAME; only bring up interfaces
specified before this string. This
is not recommended, and only meant for advanced users.
OpenPGP public key file name. The default name is
/conf/conf.d/mandos/pubkey.txt
.
OpenPGP secret key file name. The default name is
/conf/conf.d/mandos/seckey.txt
.
TLS raw public key file name. The default name is
/conf/conf.d/mandos/tls-pubkey.pem
.
TLS secret key file name. The default name is
/conf/conf.d/mandos/tls-privkey.pem
.
Sets the number of bits to use for the prime number in the
TLS Diffie-Hellman key exchange. The default value is
selected automatically based on the GnuTLS security
profile set in its priority string. Note that if the
option is used, the values
from that file will be used instead.
Specifies a PEM-encoded PKCS#3 file to read the parameters
needed by the TLS Diffie-Hellman key exchange from. If
this option is not given, or if the file for some reason
could not be used, the parameters will be generated on
startup, which will take some time and processing power.
Those using servers running under time, power or processor
constraints may want to generate such a file in advance
and use this option.
After bringing a network interface up, the program waits
for the interface to arrive in a running
state before proceeding. During this time, the kernel log
level will be lowered to reduce clutter on the system
console, alleviating any other plugins which might be
using the system console. This option sets the upper
limit of seconds to wait. The default is 2.5 seconds.
All Mandos servers are tried repeatedly until a password
is received. This value specifies, in seconds, how long
between each successive try for the same
server. The default is 10 seconds.
Network hook directory. The default directory is
/lib/mandos/network-hooks.d
.
Enable debug mode. This will enable a lot of output to
standard error about what the program is doing. The
program will still perform all other functions normally.
It will also enable debug mode in the Avahi and GnuTLS
libraries, making them print large amounts of debugging
output.
Gives a help message about options and their meanings.
Gives a short usage message.
Prints the program version.
OVERVIEW
This program is the client part. It is run automatically in an
initial RAM disk environment.
In an initial RAM disk environment using
systemd
1, this program is started
by the Mandos
password-agent
8mandos, which in turn is
started automatically by the
systemd1
Password Agent
system.
In the case of a non-
systemd1
environment, this program is started as a plugin
of the Mandos
plugin-runner
8mandos, which runs in the
initial RAM disk environment because it is
specified as a keyscript
in the
crypttab5
file.
This program could, theoretically, be used as a keyscript in
/etc/crypttab, but it would then be
impossible to enter a password for the encrypted root disk at
the console, since this program does not read from the console
at all.
EXIT STATUS
This program will exit with a successful (zero) exit status if a
server could be found and the password received from it could be
successfully decrypted and output on standard output. The
program will exit with a non-zero exit status only if a critical
error occurs. Otherwise, it will forever connect to any
discovered Mandos servers, trying to
get a decryptable password and print it.
ENVIRONMENT
MANDOSPLUGINHELPERDIR
This environment variable will be assumed to contain the
directory containing any helper executables. The use and
nature of these helper executables, if any, is purposely
not documented.
This program does not use any other environment variables, not
even the ones provided by cryptsetup8
.
NETWORK HOOKS
If a network interface like a bridge or tunnel is required to
find a Mandos server, this requires the interface to be up and
running before &COMMANDNAME; starts looking
for Mandos servers. This can be accomplished by creating a
network hook
program, and placing it in a special
directory.
Before the network is used (and again before program exit), any
runnable programs found in the network hook directory are run
with the argument start
or
stop
. This should bring up or
down, respectively, any network interface which
&COMMANDNAME; should use.
REQUIREMENTS
A network hook must be an executable file, and its name must
consist entirely of upper and lower case letters, digits,
underscores, periods, and hyphens.
A network hook will receive one argument, which can be one of
the following:
start
This should make the network hook create (if necessary)
and bring up a network interface.
stop
This should make the network hook take down a network
interface, and delete it if it did not exist previously.
files
This should make the network hook print, one
file per line, all the files needed for it to
run. (These files will be copied into the initial RAM
filesystem.) Typical use is for a network hook which is
a shell script to print its needed binaries.
It is not necessary to print any non-executable files
already in the network hook directory, these will be
copied implicitly if they otherwise satisfy the name
requirements.
modules
This should make the network hook print, on
separate lines, all the kernel modules needed
for it to run. (These modules will be copied into the
initial RAM filesystem.) For instance, a tunnel
interface needs the
tun
module.
The network hook will be provided with a number of environment
variables:
MANDOSNETHOOKDIR
The network hook directory, specified to
&COMMANDNAME; by the
option. Note: this
should always be used by the
network hook to refer to itself or any files in the hook
directory it may require.
DEVICE
The network interfaces, as specified to
&COMMANDNAME; by the
option, combined to one
string and separated by commas. If this is set, and
does not contain the interface a hook will bring up,
there is no reason for a hook to continue.
MODE
This will be the same as the first argument;
i.e. start
,
stop
,
files
, or
modules
.
VERBOSITY
This will be the 1
if
the option is passed to
&COMMANDNAME;, otherwise
0
.
DELAY
This will be the same as the
option passed to &COMMANDNAME;. Is
only set if MODE is
start
or
stop
.
CONNECT
This will be the same as the
option passed to &COMMANDNAME;. Is
only set if is passed and
MODE is
start
or
stop
.
A hook may not read from standard input, and should be
restrictive in printing to standard output or standard error
unless VERBOSITY is
1
.
FILES
/conf/conf.d/mandos/pubkey.txt
/conf/conf.d/mandos/seckey.txt
OpenPGP public and private key files, in ASCII
Armor
format. These are the default file names,
they can be changed with the and
options.
/conf/conf.d/mandos/tls-pubkey.pem
/conf/conf.d/mandos/tls-privkey.pem
Public and private raw key files, in PEM
format. These are the default file names, they can be
changed with the and
options.
/lib/mandos/network-hooks.d
Directory where network hooks are located. Change this
with the option. See
.
BUGS
EXAMPLE
Note that normally, command line options will not be given
directly, but passed on via the program responsible for starting
this program; see .
Normal invocation needs no options, if the network interfaces
can be automatically determined:
&COMMANDNAME;
Search for Mandos servers (and connect to them) using one
specific interface:
&COMMANDNAME; --interface eth1
Run in debug mode, and use custom keys:
&COMMANDNAME; --debug --pubkey keydir/pubkey.txt --seckey keydir/seckey.txt --tls-pubkey keydir/tls-pubkey.pem --tls-privkey keydir/tls-privkey.pem
Run in debug mode, with custom keys, and do not use Zeroconf
to locate a server; connect directly to the IPv6 link-local
address fe80::aede:48ff:fe71:f6f2
, port 4711,
using interface eth2:
&COMMANDNAME; --debug --pubkey keydir/pubkey.txt --seckey keydir/seckey.txt --tls-pubkey keydir/tls-pubkey.pem --tls-privkey keydir/tls-privkey.pem --connect fe80::aede:48ff:fe71:f6f2:4711 --interface eth2
SECURITY
This program assumes that it is set-uid to root, and will switch
back to the original (and presumably non-privileged) user and
group after bringing up the network interface.
To use this program for its intended purpose (see ), the password for the root file system will
have to be given out to be stored in a server computer, after
having been encrypted using an OpenPGP key. This encrypted data
which will be stored in a server can only be decrypted by the
OpenPGP key, and the data will only be given out to those
clients who can prove they actually have that key. This key,
however, is stored unencrypted on the client side in its initial
RAM disk image file system. This is normally
readable by all, but this is normally fixed during installation
of this program; file permissions are set so that no-one is able
to read that file.
The only remaining weak point is that someone with physical
access to the client hard drive might turn off the client
computer, read the OpenPGP and TLS keys directly from the hard
drive, and communicate with the server. To safeguard against
this, the server is supposed to notice the client disappearing
and stop giving out the encrypted data. Therefore, it is
important to set the timeout and checker interval values tightly
on the server. See mandos8.
It will also help if the checker program on the server is
configured to request something from the client which can not be
spoofed by someone else on the network, like SSH server key
fingerprints, and unlike unencrypted ICMP
echo (ping
) replies.
Note: This makes it completely insecure to
have Mandos clients which dual-boot
to another operating system which is not
trusted to keep the initial RAM disk image
confidential.
SEE ALSO
intro
8mandos,
cryptsetup
8,
crypttab
5,
mandos
8,
password-agent
8mandos,
plugin-runner
8mandos
Zeroconf
Zeroconf is the network protocol standard used for finding
Mandos servers on the local network.
Avahi
Avahi is the library this program calls to find Zeroconf
services.
GnuTLS
GnuTLS is the library this client uses to implement TLS for
communicating securely with the server, and at the same time
send the public key to the server.
GPGME
GPGME is the library used to decrypt the OpenPGP data sent
by the server.
RFC 4291: IP Version 6 Addressing
Architecture
Section 2.2: Text Representation of
Addresses
Section 2.5.5.2: IPv4-Mapped IPv6
Address
Section 2.5.6, Link-Local IPv6 Unicast
Addresses
This client uses IPv6 link-local addresses, which are
immediately usable since a link-local addresses is
automatically assigned to a network interface when it
is brought up.
RFC 5246: The Transport Layer Security (TLS)
Protocol Version 1.2
TLS 1.2 is the protocol implemented by GnuTLS.
RFC 4880: OpenPGP Message Format
The data received from the server is binary encrypted
OpenPGP data.
RFC 7250: Using Raw Public Keys in Transport
Layer Security (TLS) and Datagram Transport Layer Security
(DTLS)
This is implemented by GnuTLS in version 3.6.6 and is, if
present, used by this program so that raw public keys can be
used.
RFC 6091: Using OpenPGP Keys for Transport Layer
Security
This is implemented by GnuTLS before version 3.6.0 and is,
if present, used by this program so that OpenPGP keys can be
used.
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/password-prompt.c 0000664 0001750 0001750 00000035400 14720643017 017376 0 ustar 00teddy teddy /* -*- coding: utf-8; mode: c; mode: orgtbl -*- */
/*
* Password-prompt - Read a password from the terminal and print it
*
* Copyright © 2008-2019, 2021-2022 Teddy Hogeborn
* Copyright © 2008-2019, 2021-2022 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 /* vasprintf(),
program_invocation_short_name,
asprintf(), getline() */
#include /* sig_atomic_t, pid_t */
#include /* bool, false, true */
#include /* argp_program_version,
argp_program_bug_address,
struct argp_option,
struct argp_state, argp_state_help,
ARGP_HELP_STD_HELP,
ARGP_HELP_EXIT_ERR,
ARGP_HELP_EXIT_OK, ARGP_HELP_USAGE,
argp_err_exit_status,
ARGP_ERR_UNKNOWN, argp_parse(),
ARGP_IN_ORDER, ARGP_NO_HELP */
#include /* va_list, va_start(), vfprintf() */
#include /* vasprintf(), fprintf(), stderr,
vfprintf(), asprintf(), getline(),
stdin, feof(), clearerr(),
fputc() */
#include /* program_invocation_short_name,
errno, ENOENT, error_t, ENOMEM,
EINVAL, EBADF, ENOTTY, EFAULT,
EFBIG, EIO, ENOSPC, EINTR */
#include /* strerror(), strrchr(), strcmp() */
#include /* error() */
#include /* free(), realloc(), EXIT_SUCCESS,
EXIT_FAILURE, getenv() */
#include /* access(), R_OK, ssize_t, close(),
read(), STDIN_FILENO, write(),
STDOUT_FILENO */
#include /* struct dirent, scandir(),
alphasort() */
#include /* uintmax_t, strtoumax() */
#include /* or, and, not */
#include /* open(), O_RDONLY */
#include /* NULL, size_t */
#include /* struct termios, tcgetattr(),
tcflag_t, ECHO, tcsetattr(),
TCSAFLUSH */
#include /* struct sigaction, sigemptyset(),
sigaddset(), SIGINT, SIGHUP,
SIGTERM, SIG_IGN, SIG_DFL,
raise() */
#include /* EX_OSERR, EX_USAGE, EX_UNAVAILABLE,
EX_IOERR, EX_OSFILE, EX_OK */
volatile sig_atomic_t quit_now = 0;
int signal_received;
bool debug = false;
const char *argp_program_version = "password-prompt " VERSION;
const char *argp_program_bug_address = "";
/* Needed for conflict resolution */
const char plymouth_name[] = "plymouthd";
/* Function to use when printing errors */
__attribute__((format (gnu_printf, 3, 4)))
void error_plus(int status, int errnum, const char *formatstring,
...){
va_list ap;
char *text;
int ret;
va_start(ap, formatstring);
ret = vasprintf(&text, formatstring, ap);
if(ret == -1){
fprintf(stderr, "Mandos plugin %s: ",
program_invocation_short_name);
vfprintf(stderr, formatstring, ap);
fprintf(stderr, ": %s\n", strerror(errnum));
error(status, errno, "vasprintf while printing error");
return;
}
fprintf(stderr, "Mandos plugin ");
error(status, errnum, "%s", text);
free(text);
}
static void termination_handler(int signum){
if(quit_now){
return;
}
quit_now = 1;
signal_received = signum;
}
bool conflict_detection(void){
/* plymouth conflicts with password-prompt since both want to read
from the terminal. Password-prompt will exit if it detects
plymouth since plymouth performs the same functionality.
*/
if(access("/run/plymouth/pid", R_OK) == 0){
return true;
}
__attribute__((nonnull))
int is_plymouth(const struct dirent *proc_entry){
int ret;
int cl_fd;
{
uintmax_t proc_id;
char *tmp;
errno = 0;
proc_id = strtoumax(proc_entry->d_name, &tmp, 10);
if(errno != 0 or *tmp != '\0'
or proc_id != (uintmax_t)((pid_t)proc_id)){
return 0;
}
}
char *cmdline_filename;
ret = asprintf(&cmdline_filename, "/proc/%s/cmdline",
proc_entry->d_name);
if(ret == -1){
error_plus(0, errno, "asprintf");
return 0;
}
/* Open /proc//cmdline */
cl_fd = open(cmdline_filename, O_RDONLY);
free(cmdline_filename);
if(cl_fd == -1){
if(errno != ENOENT){
error_plus(0, errno, "open");
}
return 0;
}
char *cmdline = NULL;
{
size_t cmdline_len = 0;
size_t cmdline_allocated = 0;
char *tmp;
const size_t blocksize = 1024;
ssize_t sret;
do {
/* Allocate more space? */
if(cmdline_len + blocksize + 1 > cmdline_allocated){
tmp = realloc(cmdline, cmdline_allocated + blocksize + 1);
if(tmp == NULL){
error_plus(0, errno, "realloc");
free(cmdline);
close(cl_fd);
return 0;
}
cmdline = tmp;
cmdline_allocated += blocksize;
}
/* Read data */
sret = read(cl_fd, cmdline + cmdline_len,
cmdline_allocated - cmdline_len);
if(sret == -1){
error_plus(0, errno, "read");
free(cmdline);
close(cl_fd);
return 0;
}
cmdline_len += (size_t)sret;
} while(sret != 0);
ret = close(cl_fd);
if(ret == -1){
error_plus(0, errno, "close");
free(cmdline);
return 0;
}
cmdline[cmdline_len] = '\0'; /* Make sure it is terminated */
}
/* we now have cmdline */
/* get basename */
char *cmdline_base = strrchr(cmdline, '/');
if(cmdline_base != NULL){
cmdline_base += 1; /* skip the slash */
} else {
cmdline_base = cmdline;
}
if(strcmp(cmdline_base, plymouth_name) != 0){
if(debug){
fprintf(stderr, "\"%s\" is not \"%s\"\n", cmdline_base,
plymouth_name);
}
free(cmdline);
return 0;
}
if(debug){
fprintf(stderr, "\"%s\" equals \"%s\"\n", cmdline_base,
plymouth_name);
}
free(cmdline);
return 1;
}
struct dirent **direntries = NULL;
int ret;
ret = scandir("/proc", &direntries, is_plymouth, alphasort);
if(ret == -1){
error_plus(1, errno, "scandir");
}
{
int i = ret;
while(i--){
free(direntries[i]);
}
}
free(direntries);
return ret > 0;
}
int main(int argc, char **argv){
ssize_t sret;
int ret;
size_t n;
struct termios t_new, t_old;
char *buffer = NULL;
char *prefix = NULL;
char *prompt = NULL;
int status = EXIT_SUCCESS;
struct sigaction old_action,
new_action = { .sa_handler = termination_handler,
.sa_flags = 0 };
{
struct argp_option options[] = {
{ .name = "prefix", .key = 'p',
.arg = "PREFIX", .flags = 0,
.doc = "Prefix shown before the prompt", .group = 2 },
{ .name = "prompt", .key = 129,
.arg = "PROMPT", .flags = 0,
.doc = "The prompt to show", .group = 2 },
{ .name = "debug", .key = 128,
.doc = "Debug mode", .group = 3 },
/*
* These reproduce what we would get without ARGP_NO_HELP
*/
{ .name = "help", .key = '?',
.doc = "Give this help list", .group = -1 },
{ .name = "usage", .key = -3,
.doc = "Give a short usage message", .group = -1 },
{ .name = "version", .key = 'V',
.doc = "Print program version", .group = -1 },
{ .name = NULL }
};
__attribute__((nonnull(3)))
error_t parse_opt (int key, char *arg, struct argp_state *state){
errno = 0;
switch (key){
case 'p': /* --prefix */
prefix = arg;
break;
case 128: /* --debug */
debug = true;
break;
case 129: /* --prompt */
prompt = arg;
break;
/*
* These reproduce what we would get without ARGP_NO_HELP
*/
case '?': /* --help */
argp_state_help(state, state->out_stream,
(ARGP_HELP_STD_HELP | ARGP_HELP_EXIT_ERR)
& ~(unsigned int)ARGP_HELP_EXIT_OK);
__builtin_unreachable();
case -3: /* --usage */
argp_state_help(state, state->out_stream,
ARGP_HELP_USAGE | ARGP_HELP_EXIT_ERR);
__builtin_unreachable();
case 'V': /* --version */
fprintf(state->out_stream, "%s\n", argp_program_version);
exit(argp_err_exit_status);
break;
default:
return ARGP_ERR_UNKNOWN;
}
return errno;
}
struct argp argp = { .options = options, .parser = parse_opt,
.args_doc = "",
.doc = "Mandos password-prompt -- Read and"
" output a password" };
ret = argp_parse(&argp, argc, argv,
ARGP_IN_ORDER | ARGP_NO_HELP, NULL, NULL);
switch(ret){
case 0:
break;
case ENOMEM:
default:
errno = ret;
error_plus(0, errno, "argp_parse");
return EX_OSERR;
case EINVAL:
return EX_USAGE;
}
}
if(debug){
fprintf(stderr, "Starting %s\n", argv[0]);
}
if(conflict_detection()){
if(debug){
fprintf(stderr, "Stopping %s because of conflict\n", argv[0]);
}
return EXIT_FAILURE;
}
if(debug){
fprintf(stderr, "Storing current terminal attributes\n");
}
if(tcgetattr(STDIN_FILENO, &t_old) != 0){
int e = errno;
error_plus(0, errno, "tcgetattr");
switch(e){
case EBADF:
case ENOTTY:
return EX_UNAVAILABLE;
default:
return EX_OSERR;
}
}
sigemptyset(&new_action.sa_mask);
ret = sigaddset(&new_action.sa_mask, SIGINT);
if(ret == -1){
error_plus(0, errno, "sigaddset");
return EX_OSERR;
}
ret = sigaddset(&new_action.sa_mask, SIGHUP);
if(ret == -1){
error_plus(0, errno, "sigaddset");
return EX_OSERR;
}
ret = sigaddset(&new_action.sa_mask, SIGTERM);
if(ret == -1){
error_plus(0, errno, "sigaddset");
return EX_OSERR;
}
/* Need to check if the handler is SIG_IGN before handling:
| [[info:libc:Initial Signal Actions]] |
| [[info:libc:Basic Signal Handling]] |
*/
ret = sigaction(SIGINT, NULL, &old_action);
if(ret == -1){
error_plus(0, errno, "sigaction");
return EX_OSERR;
}
if(old_action.sa_handler != SIG_IGN){
ret = sigaction(SIGINT, &new_action, NULL);
if(ret == -1){
error_plus(0, errno, "sigaction");
return EX_OSERR;
}
}
ret = sigaction(SIGHUP, NULL, &old_action);
if(ret == -1){
error_plus(0, errno, "sigaction");
return EX_OSERR;
}
if(old_action.sa_handler != SIG_IGN){
ret = sigaction(SIGHUP, &new_action, NULL);
if(ret == -1){
error_plus(0, errno, "sigaction");
return EX_OSERR;
}
}
ret = sigaction(SIGTERM, NULL, &old_action);
if(ret == -1){
error_plus(0, errno, "sigaction");
return EX_OSERR;
}
if(old_action.sa_handler != SIG_IGN){
ret = sigaction(SIGTERM, &new_action, NULL);
if(ret == -1){
error_plus(0, errno, "sigaction");
return EX_OSERR;
}
}
if(debug){
fprintf(stderr, "Removing echo flag from terminal attributes\n");
}
t_new = t_old;
t_new.c_lflag &= ~(tcflag_t)ECHO;
if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_new) != 0){
int e = errno;
error_plus(0, errno, "tcsetattr-echo");
switch(e){
case EBADF:
case ENOTTY:
return EX_UNAVAILABLE;
case EINVAL:
default:
return EX_OSERR;
}
}
if(debug){
fprintf(stderr, "Waiting for input from stdin \n");
}
while(true){
if(quit_now){
if(debug){
fprintf(stderr, "Interrupted by signal, exiting.\n");
}
status = EXIT_FAILURE;
break;
}
if(prefix){
fprintf(stderr, "%s ", prefix);
}
if(prompt != NULL){
fprintf(stderr, "%s: ", prompt);
} else {
const char *cryptsource = getenv("CRYPTTAB_SOURCE");
const char *crypttarget = getenv("CRYPTTAB_NAME");
/* Before cryptsetup 1.1.0~rc2 */
if(cryptsource == NULL){
cryptsource = getenv("cryptsource");
}
if(crypttarget == NULL){
crypttarget = getenv("crypttarget");
}
const char *const prompt1 = "Unlocking the disk";
const char *const prompt2 = "Enter passphrase";
if(cryptsource == NULL){
if(crypttarget == NULL){
fprintf(stderr, "%s to unlock the disk: ", prompt2);
} else {
fprintf(stderr, "%s (%s)\n%s: ", prompt1, crypttarget,
prompt2);
}
} else {
if(crypttarget == NULL){
fprintf(stderr, "%s %s\n%s: ", prompt1, cryptsource,
prompt2);
} else {
fprintf(stderr, "%s %s (%s)\n%s: ", prompt1, cryptsource,
crypttarget, prompt2);
}
}
}
sret = getline(&buffer, &n, stdin);
if(sret > 0){
status = EXIT_SUCCESS;
/* Make n = data size instead of allocated buffer size */
n = (size_t)sret;
/* Strip final newline */
if(n > 0 and buffer[n-1] == '\n'){
buffer[n-1] = '\0'; /* not strictly necessary */
n--;
}
size_t written = 0;
while(written < n){
sret = write(STDOUT_FILENO, buffer + written, n - written);
if(sret < 0){
int e = errno;
error_plus(0, errno, "write");
switch(e){
case EBADF:
case EFAULT:
case EINVAL:
case EFBIG:
case EIO:
case ENOSPC:
default:
status = EX_IOERR;
break;
case EINTR:
status = EXIT_FAILURE;
break;
}
break;
}
written += (size_t)sret;
}
sret = close(STDOUT_FILENO);
if(sret == -1){
int e = errno;
error_plus(0, errno, "close");
switch(e){
case EBADF:
status = EX_OSFILE;
break;
case EIO:
default:
status = EX_IOERR;
break;
}
}
break;
}
if(sret < 0){
int e = errno;
if(errno != EINTR){
if(not feof(stdin)){
error_plus(0, errno, "getline");
switch(e){
case EBADF:
status = EX_UNAVAILABLE;
break;
case EIO:
case EINVAL:
default:
status = EX_IOERR;
break;
}
break;
} else {
clearerr(stdin);
}
}
}
/* if(sret == 0), then the only sensible thing to do is to retry
to read from stdin */
fputc('\n', stderr);
if(debug and not quit_now){
/* If quit_now is nonzero, we were interrupted by a signal, and
will print that later, so no need to show this too. */
fprintf(stderr, "getline() returned 0, retrying.\n");
}
}
free(buffer);
if(debug){
fprintf(stderr, "Restoring terminal attributes\n");
}
if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &t_old) != 0){
error_plus(0, errno, "tcsetattr+echo");
}
if(quit_now){
sigemptyset(&old_action.sa_mask);
old_action.sa_handler = SIG_DFL;
ret = sigaction(signal_received, &old_action, NULL);
if(ret == -1){
error_plus(0, errno, "sigaction");
}
raise(signal_received);
}
if(debug){
fprintf(stderr, "%s is exiting with status %d\n", argv[0],
status);
}
if(status == EXIT_SUCCESS or status == EX_OK){
fputc('\n', stderr);
}
return status;
}
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/password-prompt.xml 0000664 0001750 0001750 00000024304 14720643017 017755 0 ustar 00teddy teddy
%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;
8mandos
&COMMANDNAME;
Prompt for a password and output it.
&COMMANDNAME;
PREFIX
&COMMANDNAME;
&COMMANDNAME;
&COMMANDNAME;
DESCRIPTION
All &COMMANDNAME; does is prompt for a
password and output any given password to standard output.
This program is not very useful on its own. This program is
really meant to run as a plugin in the Mandos client-side system, where it is used as a
fallback and alternative to retrieving passwords from a
Mandos server.
This program is little more than a getpass3
wrapper, although actual use of that function is not guaranteed
or implied.
This program tries to detect if a Plymouth daemon
(plymouthd8)
is running, by looking for a
/run/plymouth/pid file or a process named
plymouthd
. If it is detected,
this process will immediately exit without doing anything.
OPTIONS
This program is commonly not invoked from the command line; it
is normally started by the Mandos
plugin runner, see plugin-runner8mandos
. Any command line options this program accepts
are therefore normally provided by the plugin runner, and not
directly.
Prefix string shown before the password prompt.
The password prompt. Using this option will make this
program ignore the CRYPTTAB_SOURCE and
CRYPTTAB_NAME environment variables.
Enable debug mode. This will enable a lot of output to
standard error about what the program is doing. The
program will still perform all other functions normally.
Gives a help message about options and their meanings.
Gives a short usage message.
Prints the program version.
EXIT STATUS
If exit status is 0, the output from the program is the password
as it was read. Otherwise, if exit status is other than 0, the
program has encountered an error, and any output so far could be
corrupt and/or truncated, and should therefore be ignored.
ENVIRONMENT
CRYPTTAB_SOURCE
CRYPTTAB_NAME
If set, and if the option is not
used, these environment variables will be assumed to
contain the source device name and the target device
mapper name, respectively, and will be shown as part of
the prompt.
These variables will normally be inherited from
plugin-runner
8mandos, which might
have in turn inherited them from its calling process.
This behavior is meant to exactly mirror the behavior of
askpass, the default password prompter
from initramfs-tools.
BUGS
EXAMPLE
Note that normally, command line options will not be given
directly, but via options for the Mandos plugin-runner
8mandos.
Normal invocation needs no options:
&COMMANDNAME;
Show a prefix before the prompt; in this case, a host name.
It might be useful to be reminded of which host needs a
password, in case of KVM switches, etc.
&COMMANDNAME; --prefix=host.example.org:
Run in debug mode.
&COMMANDNAME; --debug
SECURITY
On its own, this program is very simple, and does not exactly
present any security risks. The one thing that could be
considered worthy of note is this: This program is meant to be
run by plugin-runner8mandos
, and will, when run standalone, outside, in a
normal environment, immediately output on its standard output
any presumably secret password it just received. Therefore,
when running this program standalone (which should never
normally be done), take care not to type in any real secret
password by force of habit, since it would then immediately be
shown as output.
To further alleviate any risk of being locked out of a system,
the plugin-runner
8mandos has a fallback
mode which does the same thing as this program, only with less
features.
SEE ALSO
intro
8mandos,
mandos-client
8mandos,
plugin-runner
8mandos,
plymouthd
8
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/plymouth.c 0000664 0001750 0001750 00000041242 14720643017 016077 0 ustar 00teddy teddy /* -*- coding: utf-8 -*- */
/*
* Plymouth - Read a password from Plymouth and output it
*
* Copyright © 2010-2022 Teddy Hogeborn
* Copyright © 2010-2022 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 /* program_invocation_short_name,
vasprintf(), asprintf(),
TEMP_FAILURE_RETRY() */
#include /* sig_atomic_t, pid_t, setuid(),
geteuid(), setsid() */
#include /* argp_program_version,
argp_program_bug_address,
struct argp_option,
struct argp_state,
ARGP_ERR_UNKNOWN, struct argp,
argp_parse(), ARGP_IN_ORDER */
#include /* NULL, size_t */
#include /* bool, false, true */
#include /* FILE, fprintf(), vfprintf(),
vasprintf(), stderr, asprintf(),
fopen(), fscanf(), fclose(),
sscanf() */
#include /* va_list, va_start(), vfprintf() */
#include /* program_invocation_short_name,
errno, ENOMEM, EINTR, ENOENT,
error_t, EINVAL */
#include /* strerror(), strdup(), memcmp() */
#include /* error() */
#include /* free(), getenv(), malloc(),
reallocarray(), realloc(),
EXIT_FAILURE, EXIT_SUCCESS */
#include /* TEMP_FAILURE_RETRY(), setuid(),
geteuid(), setsid(), chdir(),
dup2(), STDERR_FILENO,
STDOUT_FILENO, fork(), _exit(),
execv(), ssize_t, readlink(),
close(), read(), access(), X_OK */
#include /* kill(), SIGTERM, struct sigaction,
sigemptyset(), SIGINT, SIGHUP,
sigaddset(), SIG_IGN */
#include /* waitpid(), WIFEXITED(),
WEXITSTATUS(), WIFSIGNALED(),
WTERMSIG() */
#include /* not, and, or */
#include /* EX_OSERR, EX_USAGE,
EX_UNAVAILABLE */
#include /* SIZE_MAX */
#include /* struct dirent, scandir(),
alphasort() */
#include /* uintmax_t, strtoumax(), SCNuMAX,
PRIuMAX */
#include /* struct stat, lstat(), S_ISLNK() */
#include /* open(), O_RDONLY */
#include /* argz_count(), argz_extract() */
sig_atomic_t interrupted_by_signal = 0;
const char *argp_program_version = "plymouth " VERSION;
const char *argp_program_bug_address = "";
/* Used by Ubuntu 11.04 (Natty Narwahl) */
const char plymouth_old_old_pid[] = "/dev/.initramfs/plymouth.pid";
/* Used by Ubuntu 11.10 (Oneiric Ocelot) */
const char plymouth_old_pid[] = "/run/initramfs/plymouth.pid";
/* Used by Debian 9 (stretch) */
const char plymouth_pid[] = "/run/plymouth/pid";
const char plymouth_path[] = "/bin/plymouth";
const char plymouthd_path[] = "/sbin/plymouthd";
const char *plymouthd_default_argv[] = {"/sbin/plymouthd",
"--mode=boot",
"--attach-to-session",
NULL };
bool debug = false;
static void termination_handler(__attribute__((unused))int signum){
if(interrupted_by_signal){
return;
}
interrupted_by_signal = 1;
}
__attribute__((format (gnu_printf, 2, 3), nonnull))
int fprintf_plus(FILE *stream, const char *format, ...){
va_list ap;
va_start (ap, format);
fprintf(stream, "Mandos plugin %s: ", program_invocation_short_name);
return vfprintf(stream, format, ap);
}
/* Function to use when printing errors */
__attribute__((format (gnu_printf, 3, 4)))
void error_plus(int status, int errnum, const char *formatstring,
...){
va_list ap;
char *text;
int ret;
va_start(ap, formatstring);
ret = vasprintf(&text, formatstring, ap);
if(ret == -1){
fprintf(stderr, "Mandos plugin %s: ",
program_invocation_short_name);
vfprintf(stderr, formatstring, ap);
fprintf(stderr, ": ");
fprintf(stderr, "%s\n", strerror(errnum));
error(status, errno, "vasprintf while printing error");
return;
}
fprintf(stderr, "Mandos plugin ");
error(status, errnum, "%s", text);
free(text);
}
/* Create prompt string */
char *makeprompt(void){
int ret = 0;
char *prompt;
const char *const cryptsource = getenv("cryptsource");
const char *const crypttarget = getenv("crypttarget");
const char prompt_start[] = "Unlocking the disk";
const char prompt_end[] = "Enter passphrase";
if(cryptsource == NULL){
if(crypttarget == NULL){
ret = asprintf(&prompt, "%s\n%s", prompt_start, prompt_end);
} else {
ret = asprintf(&prompt, "%s (%s)\n%s", prompt_start,
crypttarget, prompt_end);
}
} else {
if(crypttarget == NULL){
ret = asprintf(&prompt, "%s %s\n%s", prompt_start, cryptsource,
prompt_end);
} else {
ret = asprintf(&prompt, "%s %s (%s)\n%s", prompt_start,
cryptsource, crypttarget, prompt_end);
}
}
if(ret == -1){
return NULL;
}
return prompt;
}
void kill_and_wait(pid_t pid){
TEMP_FAILURE_RETRY(kill(pid, SIGTERM));
TEMP_FAILURE_RETRY(waitpid(pid, NULL, 0));
}
bool become_a_daemon(void){
int ret = setuid(geteuid());
if(ret == -1){
error_plus(0, errno, "setuid");
}
setsid();
ret = chdir("/");
if(ret == -1){
error_plus(0, errno, "chdir");
return false;
}
ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */
if(ret == -1){
error_plus(0, errno, "dup2");
return false;
}
return true;
}
__attribute__((nonnull (2, 3)))
bool exec_and_wait(pid_t *pid_return, const char *path,
const char * const * const argv, bool interruptable,
bool daemonize){
int status;
int ret;
pid_t pid;
if(debug){
for(const char * const *arg = argv; *arg != NULL; arg++){
fprintf_plus(stderr, "exec_and_wait arg: %s\n", *arg);
}
fprintf_plus(stderr, "exec_and_wait end of args\n");
}
pid = fork();
if(pid == -1){
error_plus(0, errno, "fork");
return false;
}
if(pid == 0){
/* Child */
if(daemonize){
if(not become_a_daemon()){
_exit(EX_OSERR);
}
}
char **new_argv = malloc(sizeof(const char *));
if(new_argv == NULL){
error_plus(0, errno, "malloc");
_exit(EX_OSERR);
}
char **tmp;
int i = 0;
for (; argv[i] != NULL; i++){
#if defined(__GLIBC_PREREQ) and __GLIBC_PREREQ(2, 26)
tmp = reallocarray(new_argv, ((size_t)i + 2),
sizeof(const char *));
#else
if(((size_t)i + 2) > (SIZE_MAX / sizeof(const char *))){
/* overflow */
tmp = NULL;
errno = ENOMEM;
} else {
tmp = realloc(new_argv, ((size_t)i + 2) * sizeof(const char *));
}
#endif
if(tmp == NULL){
error_plus(0, errno, "reallocarray");
free(new_argv);
_exit(EX_OSERR);
}
new_argv = tmp;
new_argv[i] = strdup(argv[i]);
}
new_argv[i] = NULL;
execv(path, (char *const *)new_argv);
error_plus(0, errno, "execv");
_exit(EXIT_FAILURE);
}
if(pid_return != NULL){
*pid_return = pid;
}
do {
ret = waitpid(pid, &status, 0);
} while(ret == -1 and errno == EINTR
and ((not interrupted_by_signal)
or (not interruptable)));
if(interrupted_by_signal and interruptable){
if(debug){
fprintf_plus(stderr, "Interrupted by signal\n");
}
return false;
}
if(ret == -1){
error_plus(0, errno, "waitpid");
return false;
}
if(debug){
if(WIFEXITED(status)){
fprintf_plus(stderr, "exec_and_wait exited: %d\n",
WEXITSTATUS(status));
} else if(WIFSIGNALED(status)) {
fprintf_plus(stderr, "exec_and_wait signaled: %d\n",
WTERMSIG(status));
}
}
if(WIFEXITED(status) and (WEXITSTATUS(status) == 0)){
return true;
}
return false;
}
__attribute__((nonnull))
int is_plymouth(const struct dirent *proc_entry){
int ret;
{
uintmax_t proc_id;
char *tmp;
errno = 0;
proc_id = strtoumax(proc_entry->d_name, &tmp, 10);
if(errno != 0 or *tmp != '\0'
or proc_id != (uintmax_t)((pid_t)proc_id)){
return 0;
}
}
char exe_target[sizeof(plymouthd_path)];
char *exe_link;
ret = asprintf(&exe_link, "/proc/%s/exe", proc_entry->d_name);
if(ret == -1){
error_plus(0, errno, "asprintf");
return 0;
}
struct stat exe_stat;
ret = lstat(exe_link, &exe_stat);
if(ret == -1){
free(exe_link);
if(errno != ENOENT){
error_plus(0, errno, "lstat");
}
return 0;
}
if(not S_ISLNK(exe_stat.st_mode)
or exe_stat.st_uid != 0
or exe_stat.st_gid != 0){
free(exe_link);
return 0;
}
ssize_t sret = readlink(exe_link, exe_target, sizeof(exe_target));
free(exe_link);
if((sret != (ssize_t)sizeof(plymouthd_path)-1) or
(memcmp(plymouthd_path, exe_target,
sizeof(plymouthd_path)-1) != 0)){
return 0;
}
return 1;
}
pid_t get_pid(void){
int ret;
uintmax_t proc_id = 0;
FILE *pidfile = fopen(plymouth_pid, "r");
/* Try the new pid file location */
if(pidfile != NULL){
ret = fscanf(pidfile, "%" SCNuMAX, &proc_id);
if(ret != 1){
proc_id = 0;
}
fclose(pidfile);
}
/* Try the old pid file location */
if(proc_id == 0){
pidfile = fopen(plymouth_old_pid, "r");
if(pidfile != NULL){
ret = fscanf(pidfile, "%" SCNuMAX, &proc_id);
if(ret != 1){
proc_id = 0;
}
fclose(pidfile);
}
}
/* Try the old old pid file location */
if(proc_id == 0){
pidfile = fopen(plymouth_old_old_pid, "r");
if(pidfile != NULL){
ret = fscanf(pidfile, "%" SCNuMAX, &proc_id);
if(ret != 1){
proc_id = 0;
}
fclose(pidfile);
}
}
/* Look for a plymouth process */
if(proc_id == 0){
struct dirent **direntries = NULL;
ret = scandir("/proc", &direntries, is_plymouth, alphasort);
if(ret == -1){
error_plus(0, errno, "scandir");
}
if(ret > 0){
const int num_entries = ret;
for(int i = 0; i < num_entries; i++){
if(proc_id == 0){
ret = sscanf(direntries[i]->d_name, "%" SCNuMAX, &proc_id);
if(ret < 0){
error_plus(0, errno, "sscanf");
}
}
free(direntries[i]);
}
}
/* scandir might preallocate for this variable (man page unclear).
even if ret == 0, therefore we need to free it. */
free(direntries);
}
pid_t pid;
pid = (pid_t)proc_id;
if((uintmax_t)pid == proc_id){
return pid;
}
return 0;
}
char **getargv(pid_t pid){
int cl_fd;
char *cmdline_filename;
ssize_t sret;
int ret;
ret = asprintf(&cmdline_filename, "/proc/%" PRIuMAX "/cmdline",
(uintmax_t)pid);
if(ret == -1){
error_plus(0, errno, "asprintf");
return NULL;
}
/* Open /proc//cmdline */
cl_fd = open(cmdline_filename, O_RDONLY);
free(cmdline_filename);
if(cl_fd == -1){
error_plus(0, errno, "open");
return NULL;
}
size_t cmdline_allocated = 0;
size_t cmdline_len = 0;
char *cmdline = NULL;
char *tmp;
const size_t blocksize = 1024;
do {
/* Allocate more space? */
if(cmdline_len + blocksize > cmdline_allocated){
tmp = realloc(cmdline, cmdline_allocated + blocksize);
if(tmp == NULL){
error_plus(0, errno, "realloc");
free(cmdline);
close(cl_fd);
return NULL;
}
cmdline = tmp;
cmdline_allocated += blocksize;
}
/* Read data */
sret = read(cl_fd, cmdline + cmdline_len,
cmdline_allocated - cmdline_len);
if(sret == -1){
error_plus(0, errno, "read");
free(cmdline);
close(cl_fd);
return NULL;
}
cmdline_len += (size_t)sret;
} while(sret != 0);
ret = close(cl_fd);
if(ret == -1){
error_plus(0, errno, "close");
free(cmdline);
return NULL;
}
/* we got cmdline and cmdline_len, ignore rest... */
char **argv = malloc((argz_count(cmdline, cmdline_len) + 1)
* sizeof(char *)); /* Get number of args */
if(argv == NULL){
error_plus(0, errno, "argv = malloc()");
free(cmdline);
return NULL;
}
argz_extract(cmdline, cmdline_len, argv); /* Create argv */
return argv;
}
int main(__attribute__((unused))int argc,
__attribute__((unused))char **argv){
char *prompt = NULL;
char *prompt_arg;
pid_t plymouth_command_pid;
int ret;
bool bret;
{
struct argp_option options[] = {
{ .name = "prompt", .key = 128, .arg = "PROMPT",
.doc = "The prompt to show" },
{ .name = "debug", .key = 129,
.doc = "Debug mode" },
{ .name = NULL }
};
__attribute__((nonnull(3)))
error_t parse_opt (int key, char *arg, __attribute__((unused))
struct argp_state *state){
errno = 0;
switch (key){
case 128: /* --prompt */
prompt = arg;
if(debug){
fprintf_plus(stderr, "Custom prompt \"%s\"\n", prompt);
}
break;
case 129: /* --debug */
debug = true;
break;
default:
return ARGP_ERR_UNKNOWN;
}
return errno;
}
struct argp argp = { .options = options, .parser = parse_opt,
.args_doc = "",
.doc = "Mandos plymouth -- Read and"
" output a password" };
ret = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, NULL, NULL);
switch(ret){
case 0:
break;
case ENOMEM:
default:
errno = ret;
error_plus(0, errno, "argp_parse");
return EX_OSERR;
case EINVAL:
error_plus(0, errno, "argp_parse");
return EX_USAGE;
}
}
/* test -x /bin/plymouth */
ret = access(plymouth_path, X_OK);
if(ret == -1){
/* Plymouth is probably not installed. Don't print an error
message, just exit. */
if(debug){
fprintf_plus(stderr, "Plymouth (%s) not found\n",
plymouth_path);
}
exit(EX_UNAVAILABLE);
}
{ /* Add signal handlers */
struct sigaction old_action,
new_action = { .sa_handler = termination_handler,
.sa_flags = 0 };
sigemptyset(&new_action.sa_mask);
for(int *sig = (int[]){ SIGINT, SIGHUP, SIGTERM, 0 };
*sig != 0; sig++){
ret = sigaddset(&new_action.sa_mask, *sig);
if(ret == -1){
error_plus(EX_OSERR, errno, "sigaddset");
}
ret = sigaction(*sig, NULL, &old_action);
if(ret == -1){
error_plus(EX_OSERR, errno, "sigaction");
}
if(old_action.sa_handler != SIG_IGN){
ret = sigaction(*sig, &new_action, NULL);
if(ret == -1){
error_plus(EX_OSERR, errno, "sigaction");
}
}
}
}
/* plymouth --ping */
bret = exec_and_wait(&plymouth_command_pid, plymouth_path,
(const char *[])
{ plymouth_path, "--ping", NULL },
true, false);
if(not bret){
if(interrupted_by_signal){
kill_and_wait(plymouth_command_pid);
exit(EXIT_FAILURE);
}
/* Plymouth is probably not running. Don't print an error
message, just exit. */
if(debug){
fprintf_plus(stderr, "Plymouth not running\n");
}
exit(EX_UNAVAILABLE);
}
if(prompt != NULL){
ret = asprintf(&prompt_arg, "--prompt=%s", prompt);
} else {
char *made_prompt = makeprompt();
ret = asprintf(&prompt_arg, "--prompt=%s", made_prompt);
free(made_prompt);
}
if(ret == -1){
error_plus(EX_OSERR, errno, "asprintf");
}
/* plymouth ask-for-password --prompt="$prompt" */
if(debug){
fprintf_plus(stderr, "Prompting for password via Plymouth\n");
}
bret = exec_and_wait(&plymouth_command_pid,
plymouth_path, (const char *[])
{ plymouth_path, "ask-for-password",
prompt_arg, NULL },
true, false);
free(prompt_arg);
if(bret){
exit(EXIT_SUCCESS);
}
if(not interrupted_by_signal){
/* exec_and_wait failed for some other reason */
exit(EXIT_FAILURE);
}
kill_and_wait(plymouth_command_pid);
char **plymouthd_argv = NULL;
pid_t pid = get_pid();
if(pid == 0){
error_plus(0, 0, "plymouthd pid not found");
} else {
plymouthd_argv = getargv(pid);
}
bret = exec_and_wait(NULL, plymouth_path, (const char *[])
{ plymouth_path, "quit", NULL },
false, false);
if(not bret){
if(plymouthd_argv != NULL){
free(*plymouthd_argv);
free(plymouthd_argv);
}
exit(EXIT_FAILURE);
}
bret = exec_and_wait(NULL, plymouthd_path,
(plymouthd_argv != NULL)
? (const char * const *)plymouthd_argv
: plymouthd_default_argv,
false, true);
if(plymouthd_argv != NULL){
free(*plymouthd_argv);
free(plymouthd_argv);
}
if(not bret){
exit(EXIT_FAILURE);
}
exec_and_wait(NULL, plymouth_path, (const char *[])
{ plymouth_path, "show-splash", NULL },
false, false);
exit(EXIT_FAILURE);
}
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/plymouth.xml 0000664 0001750 0001750 00000030477 14720643017 016465 0 ustar 00teddy teddy
%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;
8mandos
&COMMANDNAME;
Mandos plugin to use plymouth to get a
password.
&COMMANDNAME;
&COMMANDNAME;
&COMMANDNAME;
&COMMANDNAME;
DESCRIPTION
This program prompts for a password using
plymouth8
and outputs any given password to standard
output. If no plymouth8
process can be found, this program will immediately exit with an
exit code indicating failure.
This program is not very useful on its own. This program is
really meant to run as a plugin in the Mandos client-side system, where it is used as a
fallback and alternative to retrieving passwords from a
Mandos server.
If this program is killed (presumably by
plugin-runner
8mandos because some other
plugin provided the password), it cannot tell
plymouth8
to abort requesting a password, because
plymouth
8 does not support this.
Therefore, this program will then kill the
running plymouth
8 process and start a
new one using the same command line
arguments as the old one was using.
OPTIONS
This program is commonly not invoked from the command line; it
is normally started by the Mandos
plugin runner, see plugin-runner8mandos
. Any command line options this program accepts
are therefore normally provided by the plugin runner, and not
directly.
The password prompt. Note that using this option will
make this program ignore the cryptsource
and crypttarget environment variables.
Enable debug mode. This will enable a lot of output to
standard error about what the program is doing. The
program will still perform all other functions normally.
Gives a help message about options and their meanings.
Gives a short usage message.
Prints the program version.
EXIT STATUS
If exit status is 0, the output from the program is the password
as it was read. Otherwise, if exit status is other than 0, the
program was interrupted or encountered an error, and any output
so far could be corrupt and/or truncated, and should therefore
be ignored.
ENVIRONMENT
cryptsource
crypttarget
If set, and if the option is not
used, these environment variables will be assumed to
contain the source device name and the target device
mapper name, respectively, and will be shown as part of
the prompt.
These variables will normally be inherited from
plugin-runner
8mandos, which might
have in turn inherited them from its calling process.
This behavior is meant to exactly mirror the behavior of
askpass, the default password prompter
from initramfs-tools.
FILES
/bin/plymouth
This is the command run to retrieve a password from
plymouth
8.
/proc
To find the running plymouth8
, this directory will be searched for
numeric entries which will be assumed to be directories.
In all those directories, the exe and
cmdline entries will be used to
determine the name of the running binary, effective user
and group ID, and the command line
arguments. See proc5
.
/sbin/plymouthd
This is the name of the binary which will be searched for
in the process list. See plymouth8
.
BUGS
Killing the plymouth8
daemon and starting a new one is ugly, but necessary as long as
it does not support aborting a password request.
EXAMPLE
Note that normally, this program will not be invoked directly,
but instead started by the Mandos plugin-runner8mandos
.
Normal invocation needs no options:
&COMMANDNAME;
Show a different prompt.
&COMMANDNAME; --prompt=Password
SECURITY
If this program is killed by a signal, it will kill the process
ID which at the start of this program was
determined to run plymouth8
as root (see also ). There is a very
slight risk that, in the time between those events, that process
ID was freed and then taken up by another
process; the wrong process would then be killed. Now, this
program can only be killed by the user who started it; see
plugin-runner
8mandos. This program
should therefore be started by a completely separate
non-privileged user, and no other programs should be allowed to
run as that special user. This means that it is not recommended
to use the user "nobody" to start this program, as other
possibly less trusted programs could be running as "nobody", and
they would then be able to kill this program, triggering the
killing of the process ID which may or may not
be plymouth
8.
The only other thing that could be considered worthy of note is
this: This program is meant to be run by
plugin-runner8mandos, and will, when run
standalone, outside, in a normal environment, immediately output
on its standard output any presumably secret password it just
received. Therefore, when running this program standalone
(which should never normally be done), take care not to type in
any real secret password by force of habit, since it would then
immediately be shown as output.
SEE ALSO
intro
8mandos,
plugin-runner
8mandos,
proc
5,
plymouth
8
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/splashy.c 0000664 0001750 0001750 00000027601 14720643017 015704 0 ustar 00teddy teddy /* -*- coding: utf-8 -*- */
/*
* Splashy - Read a password from splashy and output it
*
* Copyright © 2008-2018, 2021-2022 Teddy Hogeborn
* Copyright © 2008-2018, 2021-2022 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 /* vasprintf(),
program_invocation_short_name,
asprintf(), TEMP_FAILURE_RETRY() */
#include /* sig_atomic_t, pid_t, setuid(),
geteuid(), setsid() */
#include /* va_list, va_start(), vfprintf() */
#include /* vasprintf(), fprintf(), stderr,
vfprintf(), asprintf() */
#include /* program_invocation_short_name,
errno, EACCES, ENOTDIR, ELOOP,
ENOENT, ENAMETOOLONG, EMFILE,
ENFILE, ENOMEM, ENOEXEC, EINVAL,
E2BIG, EFAULT, EIO, ETXTBSY,
EISDIR, ELIBBAD, EPERM, EINTR,
ECHILD */
#include /* strerror(), memcmp() */
#include /* error() */
#include /* free(), EXIT_FAILURE, getenv(),
EXIT_SUCCESS, abort() */
#include /* NULL */
#include /* DIR, opendir(), struct dirent,
readdir(), closedir() */
#include /* EX_OSERR, EX_OSFILE,
EX_UNAVAILABLE */
#include /* intmax_t, strtoimax() */
#include /* or, not, and */
#include /* ssize_t, readlink(), fork(),
execl(), _exit(),
TEMP_FAILURE_RETRY(), sleep(),
setuid(), geteuid(), setsid(),
chdir(), dup2(), STDERR_FILENO,
STDOUT_FILENO, pause() */
#include /* struct stat, lstat(), S_ISLNK() */
#include /* struct sigaction, sigemptyset(),
sigaddset(), SIGINT, SIGHUP,
SIGTERM, SIG_IGN, kill(), SIGKILL,
SIG_DFL, raise() */
#include /* waitpid(), WIFEXITED(),
WEXITSTATUS() */
sig_atomic_t interrupted_by_signal = 0;
int signal_received;
/* Function to use when printing errors */
__attribute__((format (gnu_printf, 3, 4)))
void error_plus(int status, int errnum, const char *formatstring,
...){
va_list ap;
char *text;
int ret;
va_start(ap, formatstring);
ret = vasprintf(&text, formatstring, ap);
if(ret == -1){
fprintf(stderr, "Mandos plugin %s: ",
program_invocation_short_name);
vfprintf(stderr, formatstring, ap);
fprintf(stderr, ": ");
fprintf(stderr, "%s\n", strerror(errnum));
error(status, errno, "vasprintf while printing error");
return;
}
fprintf(stderr, "Mandos plugin ");
error(status, errnum, "%s", text);
free(text);
}
static void termination_handler(int signum){
if(interrupted_by_signal){
return;
}
interrupted_by_signal = 1;
signal_received = signum;
}
int main(__attribute__((unused))int argc,
__attribute__((unused))char **argv){
int ret = 0;
char *prompt = NULL;
DIR *proc_dir = NULL;
pid_t splashy_pid = 0;
pid_t splashy_command_pid = 0;
int exitstatus = EXIT_FAILURE;
/* Create prompt string */
{
const char *const cryptsource = getenv("cryptsource");
const char *const crypttarget = getenv("crypttarget");
const char *const prompt_start = "getpass "
"Enter passphrase to unlock the disk";
if(cryptsource == NULL){
if(crypttarget == NULL){
ret = asprintf(&prompt, "%s: ", prompt_start);
} else {
ret = asprintf(&prompt, "%s (%s): ", prompt_start,
crypttarget);
}
} else {
if(crypttarget == NULL){
ret = asprintf(&prompt, "%s %s: ", prompt_start, cryptsource);
} else {
ret = asprintf(&prompt, "%s %s (%s): ", prompt_start,
cryptsource, crypttarget);
}
}
if(ret == -1){
prompt = NULL;
exitstatus = EX_OSERR;
goto failure;
}
}
/* Find splashy process */
{
const char splashy_name[] = "/sbin/splashy";
proc_dir = opendir("/proc");
if(proc_dir == NULL){
int e = errno;
error_plus(0, errno, "opendir");
switch(e){
case EACCES:
case ENOTDIR:
case ELOOP:
case ENOENT:
default:
exitstatus = EX_OSFILE;
break;
case ENAMETOOLONG:
case EMFILE:
case ENFILE:
case ENOMEM:
exitstatus = EX_OSERR;
break;
}
goto failure;
}
for(struct dirent *proc_ent = readdir(proc_dir);
proc_ent != NULL;
proc_ent = readdir(proc_dir)){
pid_t pid;
{
intmax_t tmpmax;
char *tmp;
errno = 0;
tmpmax = strtoimax(proc_ent->d_name, &tmp, 10);
if(errno != 0 or tmp == proc_ent->d_name or *tmp != '\0'
or tmpmax != (pid_t)tmpmax){
/* Not a process */
continue;
}
pid = (pid_t)tmpmax;
}
/* Find the executable name by doing readlink() on the
/proc//exe link */
char exe_target[sizeof(splashy_name)];
ssize_t sret;
{
char *exe_link;
ret = asprintf(&exe_link, "/proc/%s/exe", proc_ent->d_name);
if(ret == -1){
error_plus(0, errno, "asprintf");
exitstatus = EX_OSERR;
goto failure;
}
/* Check that it refers to a symlink owned by root:root */
struct stat exe_stat;
ret = lstat(exe_link, &exe_stat);
if(ret == -1){
if(errno == ENOENT){
free(exe_link);
continue;
}
int e = errno;
error_plus(0, errno, "lstat");
free(exe_link);
switch(e){
case EACCES:
case ENOTDIR:
case ELOOP:
default:
exitstatus = EX_OSFILE;
break;
case ENAMETOOLONG:
exitstatus = EX_OSERR;
break;
}
goto failure;
}
if(not S_ISLNK(exe_stat.st_mode)
or exe_stat.st_uid != 0
or exe_stat.st_gid != 0){
free(exe_link);
continue;
}
sret = readlink(exe_link, exe_target, sizeof(exe_target));
free(exe_link);
}
if((sret == ((ssize_t)sizeof(exe_target)-1))
and (memcmp(splashy_name, exe_target,
sizeof(exe_target)-1) == 0)){
splashy_pid = pid;
break;
}
}
closedir(proc_dir);
proc_dir = NULL;
}
if(splashy_pid == 0){
exitstatus = EX_UNAVAILABLE;
goto failure;
}
/* Set up the signal handler */
{
struct sigaction old_action,
new_action = { .sa_handler = termination_handler,
.sa_flags = 0 };
sigemptyset(&new_action.sa_mask);
ret = sigaddset(&new_action.sa_mask, SIGINT);
if(ret == -1){
error_plus(0, errno, "sigaddset");
exitstatus = EX_OSERR;
goto failure;
}
ret = sigaddset(&new_action.sa_mask, SIGHUP);
if(ret == -1){
error_plus(0, errno, "sigaddset");
exitstatus = EX_OSERR;
goto failure;
}
ret = sigaddset(&new_action.sa_mask, SIGTERM);
if(ret == -1){
error_plus(0, errno, "sigaddset");
exitstatus = EX_OSERR;
goto failure;
}
ret = sigaction(SIGINT, NULL, &old_action);
if(ret == -1){
error_plus(0, errno, "sigaction");
exitstatus = EX_OSERR;
goto failure;
}
if(old_action.sa_handler != SIG_IGN){
ret = sigaction(SIGINT, &new_action, NULL);
if(ret == -1){
error_plus(0, errno, "sigaction");
exitstatus = EX_OSERR;
goto failure;
}
}
ret = sigaction(SIGHUP, NULL, &old_action);
if(ret == -1){
error_plus(0, errno, "sigaction");
exitstatus = EX_OSERR;
goto failure;
}
if(old_action.sa_handler != SIG_IGN){
ret = sigaction(SIGHUP, &new_action, NULL);
if(ret == -1){
error_plus(0, errno, "sigaction");
exitstatus = EX_OSERR;
goto failure;
}
}
ret = sigaction(SIGTERM, NULL, &old_action);
if(ret == -1){
error_plus(0, errno, "sigaction");
exitstatus = EX_OSERR;
goto failure;
}
if(old_action.sa_handler != SIG_IGN){
ret = sigaction(SIGTERM, &new_action, NULL);
if(ret == -1){
error_plus(0, errno, "sigaction");
exitstatus = EX_OSERR;
goto failure;
}
}
}
if(interrupted_by_signal){
goto failure;
}
/* Fork off the splashy command to prompt for password */
splashy_command_pid = fork();
if(splashy_command_pid != 0 and interrupted_by_signal){
goto failure;
}
if(splashy_command_pid == -1){
error_plus(0, errno, "fork");
exitstatus = EX_OSERR;
goto failure;
}
/* Child */
if(splashy_command_pid == 0){
if(not interrupted_by_signal){
const char splashy_command[] = "/sbin/splashy_update";
execl(splashy_command, splashy_command, prompt, (char *)NULL);
int e = errno;
error_plus(0, errno, "execl");
switch(e){
case EACCES:
case ENOENT:
case ENOEXEC:
case EINVAL:
_exit(EX_UNAVAILABLE);
case ENAMETOOLONG:
case E2BIG:
case ENOMEM:
case EFAULT:
case EIO:
case EMFILE:
case ENFILE:
case ETXTBSY:
default:
_exit(EX_OSERR);
case ENOTDIR:
case ELOOP:
case EISDIR:
#ifdef ELIBBAD
case ELIBBAD: /* Linux only */
#endif
case EPERM:
_exit(EX_OSFILE);
}
}
free(prompt);
_exit(EXIT_FAILURE);
}
/* Parent */
free(prompt);
prompt = NULL;
if(interrupted_by_signal){
goto failure;
}
/* Wait for command to complete */
{
int status;
do {
ret = waitpid(splashy_command_pid, &status, 0);
} while(ret == -1 and errno == EINTR
and not interrupted_by_signal);
if(interrupted_by_signal){
goto failure;
}
if(ret == -1){
error_plus(0, errno, "waitpid");
if(errno == ECHILD){
splashy_command_pid = 0;
}
} else {
/* The child process has exited */
splashy_command_pid = 0;
if(WIFEXITED(status) and WEXITSTATUS(status) == 0){
return EXIT_SUCCESS;
}
}
}
failure:
free(prompt);
if(proc_dir != NULL){
TEMP_FAILURE_RETRY(closedir(proc_dir));
}
if(splashy_command_pid != 0){
TEMP_FAILURE_RETRY(kill(splashy_command_pid, SIGTERM));
TEMP_FAILURE_RETRY(kill(splashy_pid, SIGTERM));
sleep(2);
while(TEMP_FAILURE_RETRY(kill(splashy_pid, 0)) == 0){
TEMP_FAILURE_RETRY(kill(splashy_pid, SIGKILL));
sleep(1);
}
pid_t new_splashy_pid = (pid_t)TEMP_FAILURE_RETRY(fork());
if(new_splashy_pid == 0){
/* Child; will become new splashy process */
/* Make the effective user ID (root) the only user ID instead of
the real user ID (_mandos) */
ret = setuid(geteuid());
if(ret == -1){
error_plus(0, errno, "setuid");
}
setsid();
ret = chdir("/");
if(ret == -1){
error_plus(0, errno, "chdir");
}
/* if(fork() != 0){ */
/* _exit(EXIT_SUCCESS); */
/* } */
ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace stdout */
if(ret == -1){
error_plus(0, errno, "dup2");
_exit(EX_OSERR);
}
execl("/sbin/splashy", "/sbin/splashy", "boot", (char *)NULL);
{
int e = errno;
error_plus(0, errno, "execl");
switch(e){
case EACCES:
case ENOENT:
case ENOEXEC:
default:
_exit(EX_UNAVAILABLE);
case ENAMETOOLONG:
case E2BIG:
case ENOMEM:
_exit(EX_OSERR);
case ENOTDIR:
case ELOOP:
_exit(EX_OSFILE);
}
}
}
}
if(interrupted_by_signal){
struct sigaction signal_action;
sigemptyset(&signal_action.sa_mask);
signal_action.sa_handler = SIG_DFL;
ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received,
&signal_action, NULL));
if(ret == -1){
error_plus(0, errno, "sigaction");
}
do {
ret = raise(signal_received);
} while(ret != 0 and errno == EINTR);
if(ret != 0){
error_plus(0, errno, "raise");
abort();
}
TEMP_FAILURE_RETRY(pause());
}
return exitstatus;
}
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/splashy.xml 0000664 0001750 0001750 00000023647 14720643017 016270 0 ustar 00teddy teddy
%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;
8mandos
&COMMANDNAME;
Mandos plugin to use splashy to get a
password.
&COMMANDNAME;
DESCRIPTION
This program prompts for a password using
splashy_update
8 and outputs any given
password to standard output. If no splashy8
process can be found, this program will immediately exit with an
exit code indicating failure.
This program is not very useful on its own. This program is
really meant to run as a plugin in the Mandos client-side system, where it is used as a
fallback and alternative to retrieving passwords from a
Mandos server.
If this program is killed (presumably by
plugin-runner
8mandos because some other
plugin provided the password), it cannot tell
splashy8
to abort requesting a password, because
splashy
8 does not support this.
Therefore, this program will then kill the
running splashy
8 process and start a
new one, using boot
as the only argument.
OPTIONS
This program takes no options.
EXIT STATUS
If exit status is 0, the output from the program is the password
as it was read. Otherwise, if exit status is other than 0, the
program was interrupted or encountered an error, and any output
so far could be corrupt and/or truncated, and should therefore
be ignored.
ENVIRONMENT
cryptsource
crypttarget
If set, these environment variables will be assumed to
contain the source device name and the target device
mapper name, respectively, and will be shown as part of
the prompt.
These variables will normally be inherited from
plugin-runner
8mandos, which might
have in turn inherited them from its calling process.
This behavior is meant to exactly mirror the behavior of
askpass, the default password prompter.
FILES
/sbin/splashy_update
This is the command run to retrieve a password from
splashy
8. See
splashy_update8
.
/proc
To find the running splashy8
, this directory will be searched for
numeric entries which will be assumed to be directories.
In all those directories, the exe
entry will be used to determine the name of the running
binary and the effective user and group
ID of the process. See
proc5.
/sbin/splashy
This is the name of the binary which will be searched for
in the process list. See splashy8
.
BUGS
Killing splashy
8 and starting a new one
is ugly, but necessary as long as it does not support aborting a
password request.
EXAMPLE
Note that normally, this program will not be invoked directly,
but instead started by the Mandos plugin-runner8mandos
.
This program takes no options.
&COMMANDNAME;
SECURITY
If this program is killed by a signal, it will kill the process
ID which at the start of this program was
determined to run splashy8
as root (see also ). There is a very
slight risk that, in the time between those events, that process
ID was freed and then taken up by another
process; the wrong process would then be killed. Now, this
program can only be killed by the user who started it; see
plugin-runner
8mandos. This program
should therefore be started by a completely separate
non-privileged user, and no other programs should be allowed to
run as that special user. This means that it is not recommended
to use the user "nobody" to start this program, as other
possibly less trusted programs could be running as "nobody", and
they would then be able to kill this program, triggering the
killing of the process ID which may or may not
be splashy
8.
The only other thing that could be considered worthy of note is
this: This program is meant to be run by
plugin-runner8mandos, and will, when run
standalone, outside, in a normal environment, immediately output
on its standard output any presumably secret password it just
received. Therefore, when running this program standalone
(which should never normally be done), take care not to type in
any real secret password by force of habit, since it would then
immediately be shown as output.
SEE ALSO
intro
8mandos,
plugin-runner
8mandos,
proc
5,
splashy
8,
splashy_update
8
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/usplash.c 0000664 0001750 0001750 00000040657 14720643017 015706 0 ustar 00teddy teddy /* -*- coding: utf-8 -*- */
/*
* Usplash - Read a password from usplash and output it
*
* Copyright © 2008-2018, 2021-2022 Teddy Hogeborn
* Copyright © 2008-2018, 2021-2022 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 /* vasprintf(),
program_invocation_short_name,
asprintf(), TEMP_FAILURE_RETRY() */
#include /* sig_atomic_t, pid_t, setuid(),
geteuid(), setsid() */
#include /* va_list, va_start(), vfprintf() */
#include /* vasprintf(), fprintf(), stderr,
vfprintf(), asprintf() */
#include /* program_invocation_short_name,
errno, ENOENT, EINTR */
#include /* strerror(), strlen(), memcmp() */
#include /* error() */
#include /* free(), getenv(), realloc(),
EXIT_FAILURE, EXIT_SUCCESS,
malloc(), abort() */
#include /* bool, false, true */
#include /* open(), O_WRONLY, O_RDONLY */
#include /* size_t, NULL */
#include /* close(), ssize_t, write(),
readlink(), read(), STDOUT_FILENO,
sleep(), fork(), setuid(),
geteuid(), setsid(), chdir(),
_exit(), dup2(), STDERR_FILENO,
execv(), TEMP_FAILURE_RETRY(),
pause() */
#include /* DIR, opendir(), struct dirent,
readdir(), closedir() */
#include /* intmax_t, strtoimax() */
#include /* or, not, and */
#include /* struct stat, lstat(), S_ISLNK() */
#include /* EX_OSERR, EX_UNAVAILABLE */
#include /* struct sigaction, sigemptyset(),
sigaddset(), SIGINT, SIGHUP,
SIGTERM, SIG_IGN, kill(), SIGKILL,
SIG_DFL, raise() */
#include /* argz_count(), argz_extract() */
sig_atomic_t interrupted_by_signal = 0;
int signal_received;
const char usplash_name[] = "/sbin/usplash";
/* Function to use when printing errors */
__attribute__((format (gnu_printf, 3, 4)))
void error_plus(int status, int errnum, const char *formatstring,
...){
va_list ap;
char *text;
int ret;
va_start(ap, formatstring);
ret = vasprintf(&text, formatstring, ap);
if(ret == -1){
fprintf(stderr, "Mandos plugin %s: ",
program_invocation_short_name);
vfprintf(stderr, formatstring, ap);
fprintf(stderr, ": ");
fprintf(stderr, "%s\n", strerror(errnum));
error(status, errno, "vasprintf while printing error");
return;
}
fprintf(stderr, "Mandos plugin ");
error(status, errnum, "%s", text);
free(text);
}
static void termination_handler(int signum){
if(interrupted_by_signal){
return;
}
interrupted_by_signal = 1;
signal_received = signum;
}
static bool usplash_write(int *fifo_fd_r,
const char *cmd, const char *arg){
/*
* usplash_write(&fd, "TIMEOUT", "15") will write "TIMEOUT 15\0"
* usplash_write(&fd, "PULSATE", NULL) will write "PULSATE\0"
* SEE ALSO
* usplash_write(8)
*/
int ret;
if(*fifo_fd_r == -1){
ret = open("/dev/.initramfs/usplash_fifo", O_WRONLY);
if(ret == -1){
return false;
}
*fifo_fd_r = ret;
}
const char *cmd_line;
size_t cmd_line_len;
char *cmd_line_alloc = NULL;
if(arg == NULL){
cmd_line = cmd;
cmd_line_len = strlen(cmd) + 1;
} else {
do {
ret = asprintf(&cmd_line_alloc, "%s %s", cmd, arg);
if(ret == -1){
int e = errno;
close(*fifo_fd_r);
errno = e;
return false;
}
} while(ret == -1);
cmd_line = cmd_line_alloc;
cmd_line_len = (size_t)ret + 1;
}
size_t written = 0;
ssize_t sret = 0;
while(written < cmd_line_len){
sret = write(*fifo_fd_r, cmd_line + written,
cmd_line_len - written);
if(sret == -1){
int e = errno;
close(*fifo_fd_r);
free(cmd_line_alloc);
errno = e;
return false;
}
written += (size_t)sret;
}
free(cmd_line_alloc);
return true;
}
/* Create prompt string */
char *makeprompt(void){
int ret = 0;
char *prompt;
const char *const cryptsource = getenv("cryptsource");
const char *const crypttarget = getenv("crypttarget");
const char prompt_start[] = "Enter passphrase to unlock the disk";
if(cryptsource == NULL){
if(crypttarget == NULL){
ret = asprintf(&prompt, "%s: ", prompt_start);
} else {
ret = asprintf(&prompt, "%s (%s): ", prompt_start,
crypttarget);
}
} else {
if(crypttarget == NULL){
ret = asprintf(&prompt, "%s %s: ", prompt_start, cryptsource);
} else {
ret = asprintf(&prompt, "%s %s (%s): ", prompt_start,
cryptsource, crypttarget);
}
}
if(ret == -1){
return NULL;
}
return prompt;
}
pid_t find_usplash(char **cmdline_r, size_t *cmdline_len_r){
int ret = 0;
ssize_t sret = 0;
char *cmdline = NULL;
size_t cmdline_len = 0;
DIR *proc_dir = opendir("/proc");
if(proc_dir == NULL){
error_plus(0, errno, "opendir");
return -1;
}
errno = 0;
for(struct dirent *proc_ent = readdir(proc_dir);
proc_ent != NULL;
proc_ent = readdir(proc_dir)){
pid_t pid;
{
intmax_t tmpmax;
char *tmp;
tmpmax = strtoimax(proc_ent->d_name, &tmp, 10);
if(errno != 0 or tmp == proc_ent->d_name or *tmp != '\0'
or tmpmax != (pid_t)tmpmax){
/* Not a process */
errno = 0;
continue;
}
pid = (pid_t)tmpmax;
}
/* Find the executable name by doing readlink() on the
/proc//exe link */
char exe_target[sizeof(usplash_name)];
{
/* create file name string */
char *exe_link;
ret = asprintf(&exe_link, "/proc/%s/exe", proc_ent->d_name);
if(ret == -1){
error_plus(0, errno, "asprintf");
goto fail_find_usplash;
}
/* Check that it refers to a symlink owned by root:root */
struct stat exe_stat;
ret = lstat(exe_link, &exe_stat);
if(ret == -1){
if(errno == ENOENT){
free(exe_link);
continue;
}
error_plus(0, errno, "lstat");
free(exe_link);
goto fail_find_usplash;
}
if(not S_ISLNK(exe_stat.st_mode)
or exe_stat.st_uid != 0
or exe_stat.st_gid != 0){
free(exe_link);
continue;
}
sret = readlink(exe_link, exe_target, sizeof(exe_target));
free(exe_link);
}
/* Compare executable name */
if((sret != ((ssize_t)sizeof(exe_target)-1))
or (memcmp(usplash_name, exe_target,
sizeof(exe_target)-1) != 0)){
/* Not it */
continue;
}
/* Found usplash */
/* Read and save the command line of usplash in "cmdline" */
{
/* Open /proc//cmdline */
int cl_fd;
{
char *cmdline_filename;
ret = asprintf(&cmdline_filename, "/proc/%s/cmdline",
proc_ent->d_name);
if(ret == -1){
error_plus(0, errno, "asprintf");
goto fail_find_usplash;
}
cl_fd = open(cmdline_filename, O_RDONLY);
free(cmdline_filename);
if(cl_fd == -1){
error_plus(0, errno, "open");
goto fail_find_usplash;
}
}
size_t cmdline_allocated = 0;
char *tmp;
const size_t blocksize = 1024;
do {
/* Allocate more space? */
if(cmdline_len + blocksize > cmdline_allocated){
tmp = realloc(cmdline, cmdline_allocated + blocksize);
if(tmp == NULL){
error_plus(0, errno, "realloc");
close(cl_fd);
goto fail_find_usplash;
}
cmdline = tmp;
cmdline_allocated += blocksize;
}
/* Read data */
sret = read(cl_fd, cmdline + cmdline_len,
cmdline_allocated - cmdline_len);
if(sret == -1){
error_plus(0, errno, "read");
close(cl_fd);
goto fail_find_usplash;
}
cmdline_len += (size_t)sret;
} while(sret != 0);
ret = close(cl_fd);
if(ret == -1){
error_plus(0, errno, "close");
goto fail_find_usplash;
}
}
/* Close directory */
ret = closedir(proc_dir);
if(ret == -1){
error_plus(0, errno, "closedir");
goto fail_find_usplash;
}
/* Success */
*cmdline_r = cmdline;
*cmdline_len_r = cmdline_len;
return pid;
}
fail_find_usplash:
free(cmdline);
if(proc_dir != NULL){
int e = errno;
closedir(proc_dir);
errno = e;
}
return 0;
}
int main(__attribute__((unused))int argc,
__attribute__((unused))char **argv){
int ret = 0;
ssize_t sret;
int fifo_fd = -1;
int outfifo_fd = -1;
char *buf = NULL;
size_t buf_len = 0;
pid_t usplash_pid = -1;
bool usplash_accessed = false;
int status = EXIT_FAILURE; /* Default failure exit status */
char *prompt = makeprompt();
if(prompt == NULL){
status = EX_OSERR;
goto failure;
}
/* Find usplash process */
char *cmdline = NULL;
size_t cmdline_len = 0;
usplash_pid = find_usplash(&cmdline, &cmdline_len);
if(usplash_pid == 0){
status = EX_UNAVAILABLE;
goto failure;
}
/* Set up the signal handler */
{
struct sigaction old_action,
new_action = { .sa_handler = termination_handler,
.sa_flags = 0 };
sigemptyset(&new_action.sa_mask);
ret = sigaddset(&new_action.sa_mask, SIGINT);
if(ret == -1){
error_plus(0, errno, "sigaddset");
status = EX_OSERR;
goto failure;
}
ret = sigaddset(&new_action.sa_mask, SIGHUP);
if(ret == -1){
error_plus(0, errno, "sigaddset");
status = EX_OSERR;
goto failure;
}
ret = sigaddset(&new_action.sa_mask, SIGTERM);
if(ret == -1){
error_plus(0, errno, "sigaddset");
status = EX_OSERR;
goto failure;
}
ret = sigaction(SIGINT, NULL, &old_action);
if(ret == -1){
if(errno != EINTR){
error_plus(0, errno, "sigaction");
status = EX_OSERR;
}
goto failure;
}
if(old_action.sa_handler != SIG_IGN){
ret = sigaction(SIGINT, &new_action, NULL);
if(ret == -1){
if(errno != EINTR){
error_plus(0, errno, "sigaction");
status = EX_OSERR;
}
goto failure;
}
}
ret = sigaction(SIGHUP, NULL, &old_action);
if(ret == -1){
if(errno != EINTR){
error_plus(0, errno, "sigaction");
status = EX_OSERR;
}
goto failure;
}
if(old_action.sa_handler != SIG_IGN){
ret = sigaction(SIGHUP, &new_action, NULL);
if(ret == -1){
if(errno != EINTR){
error_plus(0, errno, "sigaction");
status = EX_OSERR;
}
goto failure;
}
}
ret = sigaction(SIGTERM, NULL, &old_action);
if(ret == -1){
if(errno != EINTR){
error_plus(0, errno, "sigaction");
status = EX_OSERR;
}
goto failure;
}
if(old_action.sa_handler != SIG_IGN){
ret = sigaction(SIGTERM, &new_action, NULL);
if(ret == -1){
if(errno != EINTR){
error_plus(0, errno, "sigaction");
status = EX_OSERR;
}
goto failure;
}
}
}
usplash_accessed = true;
/* Write command to FIFO */
if(not usplash_write(&fifo_fd, "TIMEOUT", "0")){
if(errno != EINTR){
error_plus(0, errno, "usplash_write");
status = EX_OSERR;
}
goto failure;
}
if(interrupted_by_signal){
goto failure;
}
if(not usplash_write(&fifo_fd, "INPUTQUIET", prompt)){
if(errno != EINTR){
error_plus(0, errno, "usplash_write");
status = EX_OSERR;
}
goto failure;
}
if(interrupted_by_signal){
goto failure;
}
free(prompt);
prompt = NULL;
/* Read reply from usplash */
/* Open FIFO */
outfifo_fd = open("/dev/.initramfs/usplash_outfifo", O_RDONLY);
if(outfifo_fd == -1){
if(errno != EINTR){
error_plus(0, errno, "open");
status = EX_OSERR;
}
goto failure;
}
if(interrupted_by_signal){
goto failure;
}
/* Read from FIFO */
size_t buf_allocated = 0;
const size_t blocksize = 1024;
do {
/* Allocate more space */
if(buf_len + blocksize > buf_allocated){
char *tmp = realloc(buf, buf_allocated + blocksize);
if(tmp == NULL){
if(errno != EINTR){
error_plus(0, errno, "realloc");
status = EX_OSERR;
}
goto failure;
}
buf = tmp;
buf_allocated += blocksize;
}
sret = read(outfifo_fd, buf + buf_len,
buf_allocated - buf_len);
if(sret == -1){
if(errno != EINTR){
error_plus(0, errno, "read");
status = EX_OSERR;
}
close(outfifo_fd);
goto failure;
}
if(interrupted_by_signal){
break;
}
buf_len += (size_t)sret;
} while(sret != 0);
ret = close(outfifo_fd);
if(ret == -1){
if(errno != EINTR){
error_plus(0, errno, "close");
status = EX_OSERR;
}
goto failure;
}
outfifo_fd = -1;
if(interrupted_by_signal){
goto failure;
}
if(not usplash_write(&fifo_fd, "TIMEOUT", "15")){
if(errno != EINTR){
error_plus(0, errno, "usplash_write");
status = EX_OSERR;
}
goto failure;
}
if(interrupted_by_signal){
goto failure;
}
ret = close(fifo_fd);
if(ret == -1){
if(errno != EINTR){
error_plus(0, errno, "close");
status = EX_OSERR;
}
goto failure;
}
fifo_fd = -1;
/* Print password to stdout */
size_t written = 0;
while(written < buf_len){
do {
sret = write(STDOUT_FILENO, buf + written, buf_len - written);
if(sret == -1){
if(errno != EINTR){
error_plus(0, errno, "write");
status = EX_OSERR;
}
goto failure;
}
} while(sret == -1);
if(interrupted_by_signal){
goto failure;
}
written += (size_t)sret;
}
free(buf);
buf = NULL;
if(interrupted_by_signal){
goto failure;
}
free(cmdline);
return EXIT_SUCCESS;
failure:
free(buf);
free(prompt);
/* If usplash was never accessed, we can stop now */
if(not usplash_accessed){
return status;
}
/* Close FIFO */
if(fifo_fd != -1){
ret = close(fifo_fd);
if(ret == -1 and errno != EINTR){
error_plus(0, errno, "close");
}
fifo_fd = -1;
}
/* Close output FIFO */
if(outfifo_fd != -1){
ret = close(outfifo_fd);
if(ret == -1){
error_plus(0, errno, "close");
}
}
/* Create argv for new usplash*/
char **cmdline_argv = malloc((argz_count(cmdline, cmdline_len) + 1)
* sizeof(char *)); /* Count args */
if(cmdline_argv == NULL){
error_plus(0, errno, "malloc");
return status;
}
argz_extract(cmdline, cmdline_len, cmdline_argv); /* Create argv */
/* Kill old usplash */
kill(usplash_pid, SIGTERM);
sleep(2);
while(kill(usplash_pid, 0) == 0){
kill(usplash_pid, SIGKILL);
sleep(1);
}
pid_t new_usplash_pid = fork();
if(new_usplash_pid == 0){
/* Child; will become new usplash process */
/* Make the effective user ID (root) the only user ID instead of
the real user ID (_mandos) */
ret = setuid(geteuid());
if(ret == -1){
error_plus(0, errno, "setuid");
}
setsid();
ret = chdir("/");
if(ret == -1){
error_plus(0, errno, "chdir");
_exit(EX_OSERR);
}
/* if(fork() != 0){ */
/* _exit(EXIT_SUCCESS); */
/* } */
ret = dup2(STDERR_FILENO, STDOUT_FILENO); /* replace our stdout */
if(ret == -1){
error_plus(0, errno, "dup2");
_exit(EX_OSERR);
}
execv(usplash_name, cmdline_argv);
if(not interrupted_by_signal){
error_plus(0, errno, "execv");
}
free(cmdline);
free(cmdline_argv);
_exit(EX_OSERR);
}
free(cmdline);
free(cmdline_argv);
sleep(2);
if(not usplash_write(&fifo_fd, "PULSATE", NULL)){
if(errno != EINTR){
error_plus(0, errno, "usplash_write");
}
}
/* Close FIFO (again) */
if(fifo_fd != -1){
ret = close(fifo_fd);
if(ret == -1 and errno != EINTR){
error_plus(0, errno, "close");
}
fifo_fd = -1;
}
if(interrupted_by_signal){
struct sigaction signal_action = { .sa_handler = SIG_DFL };
sigemptyset(&signal_action.sa_mask);
ret = (int)TEMP_FAILURE_RETRY(sigaction(signal_received,
&signal_action, NULL));
if(ret == -1){
error_plus(0, errno, "sigaction");
}
do {
ret = raise(signal_received);
} while(ret != 0 and errno == EINTR);
if(ret != 0){
error_plus(0, errno, "raise");
abort();
}
TEMP_FAILURE_RETRY(pause());
}
return status;
}
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1732462094.5118842
mandos-1.8.18/plugins.d/usplash.xml 0000664 0001750 0001750 00000024705 14720643017 016260 0 ustar 00teddy teddy
%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;
8mandos
&COMMANDNAME;
Mandos plugin to use usplash to get a
password.
&COMMANDNAME;
DESCRIPTION
This program prompts for a password using
usplash8
and outputs any given password to standard
output. If no usplash8
process can be found, this program will immediately exit with an
exit code indicating failure.
This program is not very useful on its own. This program is
really meant to run as a plugin in the Mandos client-side system, where it is used as a
fallback and alternative to retrieving passwords from a
Mandos server.
If this program is killed (presumably by
plugin-runner
8mandos because some other
plugin provided the password), it cannot tell
usplash8
to abort requesting a password, because
usplash
8 does not support this.
Therefore, this program will then kill the
running usplash
8 process and start a
new one using the same command line
arguments as the old one was using.
OPTIONS
This program takes no options.
EXIT STATUS
If exit status is 0, the output from the program is the password
as it was read. Otherwise, if exit status is other than 0, the
program was interrupted or encountered an error, and any output
so far could be corrupt and/or truncated, and should therefore
be ignored.
ENVIRONMENT
cryptsource
crypttarget
If set, these environment variables will be assumed to
contain the source device name and the target device
mapper name, respectively, and will be shown as part of
the prompt.
These variables will normally be inherited from