Compare commits

..

3 Commits

Author SHA1 Message Date
Brian Picciano
3f3ad43cb2 WIP
Currently a bit stuck on getting the IPMask to `nebula.NewHostCert`.
Probably the best way would be to limit number of CIDRs to just 1 for
now, and include that in the `nebula.CACert`. Or maybe we can just
provide a way to parse it out of the `ca.crt`. Either way CIDRs should
get removed from `admin.CreationParams`.
2022-10-16 17:51:51 +02:00
Brian Picciano
15c5c904a2 Re-arrange sub-commands in entrypoint somewhat 2022-10-16 17:18:50 +02:00
Brian Picciano
81d4a35b24 Introduce admin.CreationParams 2022-10-16 17:11:14 +02:00
196 changed files with 4786 additions and 13216 deletions

4
.gitignore vendored
View File

@ -1,4 +1,4 @@
*-bin
*admin.yml*
*bootstrap.yml*
*-admin.tgz*
*-bootstrap.tgz
result

4
AppDir/AppRun Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
export PATH=$APPDIR/bin
exec cryptic-net-main entrypoint "$@"

9
AppDir/bin/wait-for-ip Normal file
View File

@ -0,0 +1,9 @@
ip="$1"
shift;
echo "waiting for $ip to become available..."
while true; do ping -c1 -W1 "$ip" &> /dev/null && break; done
exec "$@"

View File

@ -1,6 +1,6 @@
[Desktop Entry]
Name=Isle
Name[en]=Isle
Name=Cryptic Net
Name[en]=Cryptic Net
Exec=AppRun
Icon=cryptic-logo

76
AppDir/etc/daemon.yml Normal file
View File

@ -0,0 +1,76 @@
#
# This file defines all configuration directives which can be modified for
# the cryptic-net daemon at runtime. All values specified here are the
# default values.
#
################################################################################
# A DNS service runs as part of every cryptic-net process.
dns:
# list of IPs that the DNS service will use to resolve non-cryptic.io
# hostnames.
resolvers:
- 1.1.1.1
- 8.8.8.8
# A VPN service runs as part of every cryptic-net process.
vpn:
# Enable this field if the vpn will be made to be publicly accessible at a
# particular IP or hostname. At least one host must have a publicly accessible
# VPN process at any given moment.
#public_addr: "host:port"
# Firewall directives, as described here:
# https://github.com/slackhq/nebula/blob/v1.6.1/examples/config.yml#L260
firewall:
conntrack:
tcp_timeout: 12m
udp_timeout: 3m
default_timeout: 10m
max_connections: 100000
outbound:
# Allow all outbound traffic from this node.
- port: any
proto: any
host: any
inbound:
# If any storage allocations are declared below, the ports used will be
# allowed here automatically.
# Allow ICMP between hosts.
- port: any
proto: icmp
host: any
# That's it.
storage:
# Allocations defined here are used to store data in the distributed storage
# network. If no allocations are defined then no data is replicated to this
# node.
#
# The data directory of each allocation should be on a different drive, while
# the meta directories can be anywhere (ideally on an SSD).
#
# Capacity declares how many gigabytes can be stored in each allocation, and
# is required. It must be a multiple of 100.
#
# The various ports are all required and must all be unique within and across
# allocations.
allocations:
#- data_path: /foo/bar/data
# meta_path: /foo/bar/meta
# capacity: 1200
# api_port: 3900
# rpc_port: 3901
# web_port: 3902

View File

@ -1,661 +0,0 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are 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.
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.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
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 Affero 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. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
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 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 work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero 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 Affero 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 Affero 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 Affero 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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
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 AGPL, see
<https://www.gnu.org/licenses/>.

123
README.md
View File

@ -4,43 +4,110 @@ rely on it for anything._**
-----
# Isle
# cryptic-net
Welcome to Isle's technical documentation. You can find a less technical
entrypoint to Isle on [the Micropelago website][isle].
The cryptic-net project provides the foundation for an **autonomous community
cloud infrastructure**.
Isle runs on a host as a server daemon, and connects to other isle instances to
form a peer-to-peer network. Isle networks are completely self-hosted; no
third-parties are required for a network to function.
This project targets communities of individuals, where certain members of the
community would like to host services and applications from servers running in
their homes or offices. These servers can range from simple Raspberry Pis to
full-sized home PCs.
Members of a network are able to build upon the capabilities provided by Isle to
host services for themselves and others. These capabilities include:
The core components of cryptic-net, currently, are:
* A VPN which enables direct peer-to-peer communication between network members.
Even if most hosts in the network are on a private LAN (e.g. their home WiFi
network) or have a dynamic IP, they can still communicate directly with each
other.
* A VPN which enables direct peer-to-peer communication. Even if most hosts in
the network are on a private LAN (e.g. their home WiFi network) or have a
dynamic IP, they can still communicate directly with each other.
* An S3-compatible network filesystem. Each member can provide as much storage
as they care to, if any. Stored data is sharded and replicated across all
hosts that choose to provide storage.
* An S3-compatible network filesystem. Each participant can provide as much
storage as they care to, if any. Stored data is sharded and replicated across
all hosts that choose to provide storage.
* A DNS server which provides automatic host discovery within the network.
These components are wrapped into a single binary, with all setup being
automated. cryptic-net takes "just works" very seriously.
Every isle daemon is able to create or join multiple independent networks. In
this case the networks remain siloed from each other, such that members of one
network are unable to access resources or communicate with members of the other.
Participants are able to build upon these foundations to host services for
themselves and others. They can be assured that their communications are private
and their storage is reliable, all with zero administrative overhead and zero
third parties involved.
[isle]: https://micropelago.net/isle/
[nebula]: https://github.com/slackhq/nebula
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/
## Getting Started
## Documentation
The following pages will guide you through setup of Isle, joining an existing
network, and all other functionality available via the command-line.
_NOTE: There is currently only a single live cryptic-net which can be joined,
though generalizing the bootstrap process so others can create their own network
is [planned][roadmap]. If you do not know the admins of this cryptic-net then
unfortunately there's not much you can do right now._
* [Installation](./docs/install.md)
* [Command-line Usage](./docs/command-line.md)
* [Join a Network](./docs/user/join-a-network.md)
cryptic-net users fall into different roles, depending on their level of
involvement and expertise within their particular network. The documentation for
cryptic-net is broken down by these categories, so that the reader can easily
know which documents they need to care about.
Those who want to dive in and contribute to the Isle codebase should check out
the [Developer Documentation](./docs/dev/index.md).
### User Docs
Users are participants who use cryptic-net resources, but do not provide any
network or storage resources themselves. Users may be accessing the network from
a laptop, and so are not expected to be online at any particular moment.
Documentation for users:
* [Getting Started](docs/user/getting-started.md)
* [Creating a daemon.yml File](docs/user/creating-a-daemonyml-file.md)
* [Using DNS](docs/user/using-dns.md) (advanced)
* Restic example (TODO)
### Operator Docs
Operators are participants who own a dedicated host which they can expect to be
always-online (to the extent that's possible in a residential environment).
Operator hosts will need at least one of the following to be useful:
* A static public IP, or a dynamic public IP with [dDNS][ddns] set up.
* At least 100GB of unused storage which can be reserved for the network.
Operators are expected to be familiar with server administration, and to not be
afraid of a terminal.
Documentation for operators:
* [Contributing Storage](docs/operator/contributing-storage.md)
* [Contributing a Lighthouse](docs/operator/contributing-a-lighthouse.md)
* [Managing garage](docs/operator/managing-garage.md)
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
### Admin Docs
Admins are participants who control membership within the network. They are
likely operators as well.
Documentation for admins:
* [Adding a Host to the Network](docs/admin/adding-a-host-to-the-network.md)
* Removing a Host From the Network (TODO)
### Dev Docs
Dev may or may not be participants in any particular cryptic-net. They instead
are those who work on the actual code for cryptic-net.
Documentation for devs:
* [Design Principles](docs/dev/design-principles.md)
* [`cryptic-net daemon` process tree](docs/dev/daemon-process-tree.svg): Diagram
describing the [pmux](https://github.com/cryptic-io/pmux) process tree created
by `cryptic-net daemon` at runtime.
* [Rebuilding Documentation](docs/dev/rebuilding-documentation.md)
## Misc
Besides documentation, there are a few other pages which might be useful:
* [Roadmap][roadmap]
[roadmap]: docs/roadmap.md

View File

@ -1,170 +1,116 @@
{
buildSystem ? builtins.currentSystem,
hostSystem ? buildSystem,
pkgsNix ? (import ./nix/pkgs.nix),
revision ? "dev",
releaseName ? "dev",
}: let
pkgs ? (import ./nix/pkgs.nix).stable,
bootstrap ? null,
pkgs = pkgsNix.default {
inherit buildSystem hostSystem;
}: rec {
rootedBootstrap = pkgs.stdenv.mkDerivation {
name = "cryptic-net-rooted-bootstrap";
src = bootstrap;
builder = builtins.toFile "builder.sh" ''
source $stdenv/setup
mkdir -p "$out"/share
cp "$src" "$out"/share/bootstrap.tgz
'';
};
pkgsNative = pkgsNix.default {
inherit buildSystem;
hostSystem = buildSystem;
};
garageNix = (import ./nix/garage.nix);
in rec {
version = pkgs.stdenv.mkDerivation {
name = "isle-version";
name = "cryptic-net-version";
inherit buildSystem hostSystem revision releaseName;
nativeBuildInputs = [ pkgsNative.git ];
goVersion = pkgs.go.version;
garageVersion = garageNix.version;
nixpkgsVersion = pkgsNix.version;
buildInputs = [ pkgs.git pkgs.go ];
src = ./.;
inherit bootstrap;
builder = builtins.toFile "builder.sh" ''
source $stdenv/setup
versionFile=version
echo "Release: $releaseName" >> "$versionFile"
echo "Platform: $hostSystem" >> "$versionFile"
echo "Git Revision: $revision" >> "$versionFile"
echo "Go Version: $goVersion" >> "$versionFile"
echo "Garage Version: $garageVersion" >> "$versionFile"
echo "NixPkgs Version: $nixpkgsVersion" >> "$versionFile"
echo "Build Platform: $buildSystem" >> "$versionFile"
if [ "$bootstrap" != "" ]; then
hostName=$(tar -xzf "$bootstrap" --to-stdout ./hostname)
echo "Built for host: $hostName" >> "$versionFile"
fi
echo "Build date: $(date)" >> "$versionFile"
echo "Git status: $(cd "$src" && git describe --always --long --dirty=' (dirty)')" >> "$versionFile"
echo "Go version: $(go version)" >> "$versionFile"
echo "Build host info: $(uname -srvm)" >> "$versionFile"
mkdir -p "$out"/share
cp "$versionFile" "$out"/share
'';
};
goBinaries = pkgs.buildGoModule {
pname = "isle-go-binaries";
version = "unstable";
goWorkspace = pkgs.callPackage ./go-workspace {};
# If this seems pointless, that's because it is! buildGoModule doesn't like
# it if the src derivation's name ends in "-go". So this mkDerivation here
# only serves to give buildGoModule a src derivation with a name it likes.
src = pkgs.stdenv.mkDerivation {
name = "isle-go-src";
src = ./go;
builder = builtins.toFile "builder.sh" ''
source $stdenv/setup
cp -r "$src" "$out"
'';
};
dnsmasq = (pkgs.callPackage ./dnsmasq {
glibcStatic = pkgs.glibc.static;
}).env;
vendorHash = "sha256-CYnZNk1wTw/88L6SxNJTUojWartbGdL44c4GKFc8s2k=";
garage = (pkgs.callPackage ./garage {}).env;
subPackages = [
"./cmd/entrypoint"
];
};
waitFor = pkgs.callPackage ./nix/wait-for.nix {};
dnsmasq = (pkgs.callPackage ./nix/dnsmasq.nix {
stdenv = pkgs.pkgsStatic.stdenv;
});
nebula = pkgs.callPackage ./nix/nebula.nix {};
garage = let
hostPlatform = pkgs.stdenv.hostPlatform.parsed;
in pkgs.callPackage garageNix.package {
inherit buildSystem;
hostSystem = "${hostPlatform.cpu.name}-unknown-${hostPlatform.kernel.name}-musl";
pkgsSrc = pkgsNix.src;
};
appDirBase = pkgs.buildEnv {
name = "isle-AppDir-base";
appDir = pkgs.buildEnv {
name = "cryptic-net-AppDir";
paths = [
pkgs.pkgsStatic.bash
pkgs.pkgsStatic.coreutils
pkgs.pkgsStatic.unixtools.ping
pkgs.pkgsStatic.netcat # required by waitFor
pkgs.pkgsStatic.gnutar
pkgs.pkgsStatic.gzip
# custom packages from ./pkgs.nix
pkgs.yq-go
pkgs.nebula
./AppDir
version
dnsmasq
nebula
garage
pkgs.minio-client
];
waitFor
goWorkspace.crypticNetMain
] ++ (if bootstrap != null then [ rootedBootstrap ] else []);
};
appDir = pkgs.stdenv.mkDerivation {
name = "isle-AppDir";
appimagetool = pkgs.callPackage ./nix/appimagetool.nix {};
src = appDirBase;
inherit goBinaries;
appImage = pkgs.stdenv.mkDerivation {
name = "cryptic-net-AppImage";
src = appDir;
buildInputs = [ appimagetool ];
ARCH = "x86_64";
builder = builtins.toFile "build.sh" ''
source $stdenv/setup
cp -rL "$src" "$out"
chmod +w "$out" -R
cd "$out"
cp $goBinaries/bin/entrypoint ./AppRun
cp -rL "$src" cryptic-net
chmod +w cryptic-net -R
mkdir $out
appimagetool cryptic-net "$out/cryptic-net"
'';
};
devShell = pkgs.mkShell {
buildInputs = [
pkgs.go
pkgs.golangci-lint
pkgs.gopls
(pkgs.callPackage ./nix/gowrap.nix {})
pkgs.go-mockery
];
shellHook = ''
true # placeholder
'';
};
service = pkgs.writeText "cryptic-service" ''
[Unit]
Description=cryptic nebula
Requires=network.target
After=network.target
testShell = pkgs.mkShell {
APPDIR = appDirBase;
buildInputs = [
pkgs.go
];
};
[Service]
Restart=always
RestartSec=1s
User=root
ExecStart=${appImage}/cryptic-net
build = rec {
appImage = pkgs.stdenv.mkDerivation {
name = "isle-AppImage";
src = appDir;
nativeBuildInputs = [
(pkgsNative.callPackage ./nix/appimagetool.nix {})
];
ARCH = pkgs.stdenv.hostPlatform.parsed.cpu.name;
builder = builtins.toFile "build.sh" ''
source $stdenv/setup
cp -rL "$src" isle.AppDir
chmod +w isle.AppDir -R
export VERSION=debug
# https://github.com/probonopd/go-appimage/issues/155
unset SOURCE_DATE_EPOCH
appimagetool ./isle.AppDir
mkdir -p "$out"/bin
mv Isle-* "$out"/bin/isle
'';
};
archPkg = ((import ./dist/linux/arch) {
inherit hostSystem releaseName appImage;
pkgs = pkgsNative;
});
};
[Install]
WantedBy=multi-user.target
'';
}

View File

@ -1,86 +0,0 @@
{
pkgs,
hostSystem,
releaseName,
appImage,
}: let
cpuArch = (pkgs.lib.systems.parse.mkSystemFromString hostSystem).cpu.name;
pkgbuild = pkgs.writeText "isle-arch-PKGBUILD-${releaseName}-${cpuArch}" ''
pkgname=isle
pkgver=${builtins.replaceStrings ["-"] ["_"] releaseName}
pkgrel=0
pkgdesc="The foundation for an autonomous community cloud infrastructure."
arch=('${cpuArch}')
url="https://code.betamike.com/micropelago/isle"
license=('AGPL-3.0-or-later')
depends=(
'fuse2'
)
# The appImage is deliberately kept separate from the src.tar.zst. For some
# reason including the appImage within the archive results in a large part
# of the binary being stripped away and some weird skeleton appImage comes
# out the other end.
source=('isle' 'src.tar.zst')
md5sums=('SKIP' 'SKIP')
noextract=('isle')
package() {
cp -r etc "$pkgdir"/etc
cp -r usr "$pkgdir"/usr
mkdir -p "$pkgdir"/usr/bin/
cp isle "$pkgdir"/usr/bin/
}
'';
in
pkgs.stdenv.mkDerivation {
name = "isle-arch-pkg-${releaseName}-${cpuArch}";
nativeBuildInputs = [
pkgs.zstd
pkgs.pacman
pkgs.fakeroot
pkgs.libarchive
];
inherit pkgbuild;
src = appImage;
defaultDaemonYml = ../../../go/daemon/daecommon/daemon.yml;
systemdService = ../isle.service;
dontUnpack = true;
buildPhase = ''
mkdir -p root/etc/isle/
cp "$defaultDaemonYml" root/etc/isle/daemon.yml
mkdir -p root/usr/lib/sysusers.d/
cat >root/usr/lib/sysusers.d/isle.conf <<EOF
u isle - "isle Daemon"
EOF
mkdir -p root/usr/lib/systemd/system
cp "$systemdService" root/usr/lib/systemd/system/isle.service
cp $pkgbuild PKGBUILD
tar -cf src.tar.zst --zstd --mode=a+rX,u+w -C root .
cp "$src"/bin/isle isle
PKGEXT=".pkg.tar.zst" CARCH="${cpuArch}" makepkg \
--nodeps \
--config ${pkgs.pacman}/etc/makepkg.conf
'';
installPhase = ''
mkdir -p $out
cp *.pkg.tar.zst $out/
'';
# NOTE if https://github.com/NixOS/nixpkgs/issues/241911 is ever addressed
# it'd be nice to add an automatic check using namcap here.
}

View File

@ -1,16 +0,0 @@
[Unit]
Description=Isle
Requires=network.target
After=network.target
[Service]
User=isle
ExecStart=/usr/bin/isle daemon -c /etc/isle/daemon.yml
RuntimeDirectory=isle
RuntimeDirectoryMode=0700
StateDirectory=isle
StateDirectoryMode=0700
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,36 @@
# TODO implement this in go
set -e -o pipefail
cd "$APPDIR"
conf_path="$_RUNTIME_DIR_PATH"/dnsmasq.conf
cat etc/dnsmasq/base.conf > "$conf_path"
tmp="$(mktemp -d -t cryptic-net-dnsmasq-entrypoint-XXX)"
( trap "rm -rf '$tmp'" EXIT
tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./hosts
thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname)
thisHostIP=$(cat "$tmp"/hosts/"$thisHostName".yml | yq '.nebula.ip')
domain=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./admin/creation-params.yml | yq '.domain')
echo "listen-address=$thisHostIP" >> "$conf_path"
ls -1 "$tmp"/hosts | while read hostYml; do
hostName=$(echo "$hostYml" | cut -d. -f1)
hostIP=$(cat "$tmp"/hosts/"$hostYml" | yq '.nebula.ip')
echo "address=/${hostName}.hosts.$domain/$hostIP" >> "$conf_path"
done
)
cat "$_DAEMON_YML_PATH" | \
yq '.dns.resolvers | .[] | "server=" + .' \
>> "$conf_path"
exec bin/dnsmasq -d -C "$conf_path"

39
dnsmasq/default.nix Normal file
View File

@ -0,0 +1,39 @@
{
stdenv,
buildEnv,
glibcStatic,
rebase,
}: rec {
dnsmasq = stdenv.mkDerivation rec {
pname = "dnsmasq";
version = "2.85";
src = builtins.fetchurl {
url = "https://www.thekelleys.org.uk/dnsmasq/${pname}-${version}.tar.xz";
sha256 = "sha256-rZjTgD32h+W5OAgPPSXGKP5ByHh1LQP7xhmXh/7jEvo=";
};
nativeBuildInputs = [ glibcStatic ];
makeFlags = [
"LDFLAGS=-static"
"DESTDIR="
"BINDIR=$(out)/bin"
"MANDIR=$(out)/man"
"LOCALEDIR=$(out)/share/locale"
];
};
env = buildEnv {
name = "cryptic-net-dnsmasq";
paths = [
(rebase "cryptic-net-dnsmasq-bin" ./bin "bin")
(rebase "cryptic-net-dnsmasq-etc" ./etc "etc/dnsmasq")
dnsmasq
];
};
}

41
dnsmasq/etc/base.conf Normal file
View File

@ -0,0 +1,41 @@
# Configuration file for dnsmasq.
#
# Format is one option per line, legal options are the same
# as the long options legal on the command line. See
# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details.
# Listen on this specific port instead of the standard DNS port
# (53). Setting this to zero completely disables DNS function,
# leaving only DHCP and/or TFTP.
port=53
# If you don't want dnsmasq to read /etc/resolv.conf or any other
# file, getting its servers from this file instead (see below), then
# uncomment this.
no-resolv
# On systems which support it, dnsmasq binds the wildcard address,
# even when it is listening on only some interfaces. It then discards
# requests that it shouldn't reply to. This has the advantage of
# working even when interfaces come and go and change address. If you
# want dnsmasq to really bind only the interfaces it is listening on,
# uncomment this option. About the only time you may need this is when
# running another nameserver on the same machine.
bind-interfaces
# If you don't want dnsmasq to read /etc/hosts, uncomment the
# following line.
no-hosts
# Unset user and group so that dnsmasq doesn't drop privileges to another user.
# If this isn't done then dnsmasq fails to start up, since it fails to access
# /etc/passwd correctly, probably due to nix.
user=
group=
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#
# Everything below is generated dynamically based on runtime configuration
#
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

View File

@ -4,6 +4,20 @@ This document guides an admin through adding a single host to the network. Keep
in mind that the steps described here must be done for _each_ host the user
wishes to add.
There are two ways for a user to add a host to the cryptic-net network.
- If the user is savy enough to obtain their own `cryptic-net` binary, they can
do so. The admin can then generate a `bootstrap.tgz` file for their host,
give that to the user, and the user can run `cryptic-net daemon` using that
bootstrap file.
- If the user is not so savy, the admin can generate a custom `cryptic-net`
binary with the `bootstrap.tgz` embedded into it. The user can be given this
binary and run `cryptic-net daemon` without any configuration on their end.
From the admin's perspective the only difference between these cases is one
extra step.
## Step 1: Choose Hostname
The user will need to provide you with a name for their host. The name should
@ -15,20 +29,66 @@ conform to the following rules:
* It should end with a letter or number.
## Step 2: Create a `bootstrap.json` File
## Step 2: Add Host to Network
To create a `bootstrap.json` file for the new host, the admin should perform the
The admin should choose an IP for the host. The IP you choose for the new host
should be one which is not yet used by any other host, and which is in the VPN's
set of allowed IPs.
The admin should perform the following command from their own host:
```
cryptic-net hosts add --name <name> --ip <ip>
```
## Step 3: Create a `bootstrap.tgz` File
Access to an `admin.tgz` file is required for this step.
To create a `bootstrap.tgz` file for the new host, the admin should perform the
following command from their own host:
```
isle hosts create --hostname <name> >bootstrap.json
cryptic-net hosts make-bootstrap \
--name <name> \
--admin-path <path to admin.tgz> \
> bootstrap.tgz
```
The resulting `bootstrap.json` file should be treated as a secret file and
shared only with the user it was generated for. The `bootstrap.json` file should
not be re-used between hosts.
The resulting `bootstrap.tgz` file should be treated as a secret file that is
shared only with the user it was generated for. The `bootstrap.tgz` file should
not be re-used between hosts either.
The user can now proceed with calling `isle network join`, as described in the
[Getting Started][getting-started] document.
If the user already has access to a `cryptic-net` binary then the new
`bootstrap.tgz` file can be given to them as-is, and they can proceed with
running their host's `cryptic-net daemon`.
[getting-started]: ../user/getting-started.md
### Encrypted `admin.tgz`
If `admin.tgz` is kept in an encrypted format on disk (it should be!) then the
decrypted form can be piped into `make-bootstrap` over stdin. For example, if
GPG is being used to secure `admin.tgz` then the following could be used to
generate a `bootstrap.tgz`:
```
gpg -d <path to admin.tgz.gpg> | cryptic-net hosts make-boostrap \
--name <name> \
--admin-path - \
> bootstrap.tgz
```
Note that the value of `--admin-path` is `-`, indicating that `admin.tgz` should
be read from stdin.
## Step 4: Optionally, Build Binary
If you wish to embed the `bootstrap.tgz` into a custom binary for the user (to
make installation _extremely_ easy for them) then you can run the following:
```
nix-build --arg bootstrap <path to bootstrap.tgz> -A appImage
```
The resulting binary can be found in the `result` directory which is created.
Note that this binary should be treated like a `bootstrap.tgz` in terms of its
uniqueness and sensitivity.

View File

@ -1,101 +0,0 @@
# Creating a New Network
This guide is for those who wish to start a new isle network of their own.
By starting a new isle network, you are becoming the administrator of a
network. Be aware that being a network administrator is not necessarily easy,
and the users of your network will frequently need your help in order to have a
good experience. It can be helpful to have others with which you are
administering the network, in order to share responsibilities.
## Requirements
Creating a network is done using a single host, which will become the first host
in the network.
The configuration used during network creation will be identical to that used
during normal operation of the host, so be prepared to commit to that
configuration for a non-trivial amount of time.
The requirements for this host are:
* A public static IP, or a dynamic public IP with [dDNS][ddns] set up.
* There should be UDP port which is accessible publicly over that IP/DNS name.
This may involve forwarding the UDP port in your gateway if the host is
behind a NAT, and/or allowing traffic on that UDP port in your hosts
firewall.
* At least 3 GB of disk storage space.
* At least 3 directories should be chosen, each of which will be committing at
least 1GB. Ideally these directories should be on different physical disks,
but if that's not possible it's ok. See the Next Steps section.
* None of the resources being used for this network (the UDP port or storage
locations) should be being used by other networks.
## Step 1: Configure the isle Daemon
Open `/etc/isle/daemon.yml` in a text editor and perform the following changes:
* Set the `vpn.public_addr` field to the `host:port` your host is accessible on,
where `host` is the static public IP/DNS name of your host, and `port` is the
UDP port which is publicly accessible.
* Configure 3 (at least) allocations in the `storage.allocations` section.
Save and close the file.
Run the following to restart the daemon with the new configuration:
```
sudo systemctl restart isle
```
## Step 2: Choose Parameters
There are some key parameters which must be chosen when creating a new network.
These will remain constant throughout the lifetime of the network, and so should
be chosen with care.
* Name: A human-readable name for the network. This will only be used for
display purposes.
* Subnet: The IP subnet (or CIDR) will look something like `10.10.0.0/16`, where
the `/16` indicates that all IPs from `10.10.0.0` to `10.10.255.255` are
included. It's recommended to choose from the [ranges reserved for private
networks](https://en.wikipedia.org/wiki/IPv4#Private_networks), but within
that selection the choice is up to you.
* Domain: isle is shipped with a DNS server which will automatically
configure itself with all hosts in the network, with each DNS entry taking the
form of `hostname.hosts.domain`, where `domain` is the domain chosen in this
step. The domain may be a valid public domain or not, it's up to you.
* Hostname: The hostname of your host, which will be the first host in the
network, must be chosen at this point. You can reference the [Adding a Host to
the Network](./adding-a-host-to-the-network.md) document for the constraints
on the hostname.
* IP: The IP of your host, which will be the first host in the network. This IP
must be within the chosen subnet range.
## Step 3: Create the Network
To create the network, run:
```
sudo isle network create \
--name <name> \
--ip-net <subnet> \
--domain <domain> \
--hostname <hostname>
```
At this point your host, and your network, are ready to go! To add other hosts
to the network you can reference the [Adding a Host to the Network][add-host]
document.
[add-host]: ./adding-a-host-to-the-network.md
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/

View File

@ -1,12 +0,0 @@
# Removing a Host from the Network
Removing a host from the network is as simple as
```
isle host remove --hostname <name>
```
Keep in mind that if the host being removed had storage allocations set on it
then the network will require some time to rebalance the stored data across the
remaining storage hosts. This means that when removing multiple storage hosts
from the network it is a good idea to wait a while in between each removal.

View File

@ -1,54 +0,0 @@
# Command-line Usage
This documents provides examples of how to accomplish various tasks from Isle's
command-line interface.
Isle network members fall into different roles, depending on their level of
involvement and expertise within their particular network. The documentation is
broken down by these categories, so that the reader can easily decide which
documents they need to care about.
### User Docs
Users are members who use network resources, but do not provide any network
resources themselves. Users may be accessing the network from a mobile device,
and so are not expected to be online at any particular moment.
Documentation for users:
* [Joining a Network](./user/join-a-network.md)
* [Using DNS](./user/using-dns.md) (advanced)
* Restic example (TODO)
### Operator Docs
Operators are members who own a dedicated host which they can expect to be
always-online (to the extent that's possible in a residential environment).
Operator hosts will need at least one of the following to be useful:
* A static public IP, or a dynamic public IP with [dDNS][ddns] set up.
* At least 1GB of unused storage which can be reserved for the network.
Operators are expected to be familiar with server administration, and to not be
afraid of a terminal.
Documentation for operators:
* [Contributing Storage](./operator/contributing-storage.md)
* [Contributing a Lighthouse](./operator/contributing-a-lighthouse.md)
* [Managing garage](./operator/managing-garage.md)
* [Firewalls](./operator/firewall.md)
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
### Admin Docs
Admins are members who control membership within the network. They are likely
operators as well.
Documentation for admins:
* [Creating a New Network](./admin/creating-a-new-network.md)
* [Adding a Host to the Network](./admin/adding-a-host-to-the-network.md)
* [Removing a Host from the Network](./admin/removing-a-host-from-the-network.md)

View File

@ -1,25 +0,0 @@
# Architecture
The isle daemon is the central point around which all operations occur. Users of
the isle command-line tool (or, soon, a GUI) communicate with the daemon over a
local RPC socket to issue commands, for example to tell it to join a new network
or to retrieve the names of hosts in an already-joined network.
For every network which is joined, the isle daemon will create and manage
sub-processes to provide certain parts of its functionality. These include:
* A [nebula][nebula] instance to provide VPN functionality.
* A [dnsmasq][dnsmasq] instance to act as DNS server.
* Zero or more [garage][garage] instances, depending on how storage is
configured on the host, to provide the S3 storage layer.
The isle daemon considers the configuration and management of these
sub-processes to be an implementation detail. In other words, its RPC interface,
and therefore all user interfaces, do not expose members to the fact that these
sub-processes exist, or even that these projects are being used under the hood.
![Architectural diagram](./architecture.svg)
[nebula]: https://github.com/slackhq/nebula
[garage]: https://garagehq.deuxfleurs.fr/
[dnsmasq]: https://dnsmasq.org/doc.html

View File

@ -1,49 +0,0 @@
@startuml
skinparam componentStyle rectangle
[isle command line] as isleCommand
() "daemon.RPC" as isleDaemonRPC
[daemon.Client] as daemonClient
frame "isle daemon process" {
portin "RPC Socket" as rpcSocket
[RPC Server] as rpcServer
() "daemon.RPC" as daemonRPC
[daemon.Daemon] as daemon
[network.Network (A)] as networkA
[network.Network (B)] as networkB
rpcServer --> rpcSocket : handle
rpcServer ..> daemonRPC : dispatch
daemon - daemonRPC
daemon --> networkA
daemon --> networkB
}
isleCommand ..> isleDaemonRPC : issue commands
daemonClient - isleDaemonRPC
daemonClient --> rpcSocket
package "network A child processes" {
[nebula] as networkANebula
[garage (alloc 1)] as networkAGarage1
[garage (alloc 2)] as networkAGarage2
[dnsmasq] as networkADNSMasq
}
networkA --> networkANebula
networkA --> networkAGarage1
networkA --> networkAGarage2
networkA --> networkADNSMasq
package "network B child processes" {
[nebula] as networkBNebula
[dnsmasq] as networkBDNSMasq
}
networkB --> networkBNebula
networkB --> networkBDNSMasq
@enduml

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,47 +0,0 @@
# Building Isle
Building from source requires [nix][nix].
(*NOTE* The first time you run some of these builds a lot of things will be
built from scratch. If you have not otherwise configured it, nix might be using
a tmpfs as its build directory, and the capacity of this tmpfs will probably be
exceeded by this build. You can change your build directory to somewhere on-disk
by setting the TMPDIR environment variable for `nix-daemon` (see [this github
issue][tmpdir-gh].))
[nix]: https://nixos.wiki/wiki/Nix_package_manager
[tmpdir-gh]: https://github.com/NixOS/nix/issues/2098#issuecomment-383243838
## Current System
You can build an AppImage for your current system by running the following from
the project's root:
```
nix-build -A build.appImage
```
The resulting binary can be found under `result/bin`.
## Cross-Compile
An AppImage can be cross-compiled by passing the `hostSystem` argument:
```
nix-build --argstr hostSystem "x86_64-linux" -A build.appImage
```
Supported system strings are enumerated in `nix/pkgs.nix`.
## Alternative Build Targets
Besides AppImage, Isle supports building alternative package types which are
targetted at different specific operating systems. Each of these has its own
`build.*` target.
* AppImage (all OSs): `nix-build -A build.appImage`
* Arch Linux package: `nix-build -A build.archPkg`
All targets theoretically support passing in a `hostSystem` argument to
cross-compile to a different architecture or OS, but some targets may not make
sense with some `hostSystem` values.

View File

@ -0,0 +1,68 @@
@startuml
hide empty description
state "./cryptic-net daemon -c ./daemon.yml" as init
state AppDir {
note "All relative paths are relative to the root of the AppDir" as N1
state "./AppRun" as AppRun {
AppRun : * Set PATH to APPDIR/bin
}
state "./bin/cryptic-net-main entrypoint daemon -c ./daemon.yml" as entrypoint {
entrypoint : * Create runtime dir at $_RUNTIME_DIR_PATH
entrypoint : * Lock runtime dir
entrypoint : * Merge given and default daemon.yml files
entrypoint : * Copy bootstrap.tgz into $_DATA_DIR_PATH, if it's not there
entrypoint : * Merge daemon.yml config into bootstrap.tgz
entrypoint : * Run child processes
}
init --> AppRun : exec
AppRun --> entrypoint : exec
state "./bin/dnsmasq-entrypoint" as dnsmasqEntrypoint {
dnsmasqEntrypoint : * Create $_RUNTIME_DIR_PATH/dnsmasq.conf
}
state "./bin/dnsmasq -d -C $_RUNTIME_DIR_PATH/dnsmasq.conf" as dnsmasq
entrypoint --> dnsmasqEntrypoint : child
dnsmasqEntrypoint --> dnsmasq : exec
state "./bin/cryptic-net-main nebula-entrypoint" as nebulaEntrypoint {
nebulaEntrypoint : * Create $_RUNTIME_DIR_PATH/nebula.yml
}
state "./bin/nebula -config $_RUNTIME_DIR_PATH/nebula.yml" as nebula
entrypoint --> nebulaEntrypoint : child
nebulaEntrypoint --> nebula : exec
state "./bin/cryptic-net-main garage-entrypoint" as garageEntrypoint {
garageEntrypoint : * Create $_RUNTIME_DIR_PATH/garage-N.toml\n (one per storage allocation)
garageEntrypoint : * Run child processes
}
state "./bin/garage -c $_RUNTIME_DIR_PATH/garage-N.toml server" as garage
state "./bin/garage-apply-layout-diff" as garageApplyLayoutDiff {
garageApplyLayoutDiff : * Runs once then exits
garageApplyLayoutDiff : * Updates cluster topo
}
entrypoint --> garageEntrypoint : child (only if >1 storage allocation defined in daemon.yml)
garageEntrypoint --> garage : child (one per storage allocation)
garageEntrypoint --> garageApplyLayoutDiff : child
state "./bin/cryptic-net-main update-global-bucket" as updateGlobalBucket {
updateGlobalBucket : * Runs once then exits
updateGlobalBucket : * Updates the bootstrap data for the host in garage for other hosts to query
}
entrypoint --> updateGlobalBucket : child
}
@enduml

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,26 +1,21 @@
# Design Principles
The following points form the basis for all design decisions made within the
Isle project.
cryptic-net project.
* "Sometimes, magic is just someone spending more time on something than anyone
else might reasonably expect." - Teller
Isle should feel magical, in that it's making the seemingly impossible feel
easy. Accomplishing this requires a lot of care, precision, and time on the
part of Isle developers.
* The UX is aggressively optimized to eliminate manual intervention by members.
* The UX is aggressively optimized to eliminate manual intervention by users.
All other concerns are secondary. The concept of "UX" extends beyond GUI
interfaces, and encompasses all interactions of any sort with an isle
interfaces, and encompasses all interactions of any sort with a cryptic-net
process.
* All resources within an isle network are expected to be hosted on hardware
owned by community members, for example home media servers or gaming rigs.
Thus, an isle network is fully autonomous.
* All resources within a cryptic-net are expected to be hosted on hardware owned
by community members, for example home media servers or gaming rigs. Thus, a
cryptic-net is fully autonomous.
* Hardware resources are expected to be heterogenous and geographically
dispersed.
* It is expected that a single host might be a part of multiple, independent
isle networks. These should not conflict with each other, nor share resources.
cryptic-net networks. These should not conflict with each other, nor share
resources.

View File

@ -1,25 +0,0 @@
# Glossary
The purpose of this document is define the specific terms which should be used
for various concepts, with the goal of establishing consistency throughout
documentation and source code.
- "Isle" - The name of this project, which is a proper noun and so should always
be capitalized.
- "isle" - The name of the binary or program produced by the Isle project. isle
is the name of the file itself, as well as an instance of the process, and so
is always lower-case.
- "host" - A computer or device on which isle is run.
- "isle network", "network" - A collection of hosts which communicate and share
resources with each other via the Isle project.
- "garage cluster" - Garage is one of the sub-processes which isle is able to
run. These garage process connect together to form a cluster. We use the
term "cluster" in the context of garage to stay consistent with garage's
documentation and command-line.
- "member" - A person who takes part in the usage, operation, or administration
of an isle network.

View File

@ -1,16 +0,0 @@
# Developer Documentation
This section of the documentation is targeted at those who are contributing to
the Isle codebase. It is recommended to start with the following pages, in order
to better understand how to navigate and work on the codebase.
* [Glossary](./glossary.md)
* [Design Principles](./design-principles.md)
* [Architecture](./architecture.md)
These pages can be helpful in specific situations.
* [Building Isle](./building.md)
* [Testing Isle](./testing.md)
* [Rebuilding Documentation](./rebuilding-documentation.md)
* [Releases](./releases.md)

View File

@ -1,6 +1,6 @@
# Rebuilding Documentation
Most documentation for Isle takes the form of markdown (`.md`) files,
Most documentation for cryptic-net takes the form of markdown (`.md`) files,
which do not require any build step. There are a few other kinds of files, such
as `.plantuml` files, which do require a build step. If these are changed then
their artifacts should be rebuilt by doing:

View File

@ -1,32 +0,0 @@
# Releases
A release consists of:
- A full set of `isle` binaries for all supported platforms, compiled from the
same source.
- A text file containing hashes of each binary.
- A file containing a signature of the hash file, created by whoever is building
the release.
## Building
*NOTE: This has only been tested from an x86_64 linux machine*
To create a release only a functional nix installation is required. Simply run
the `./release.sh` script, and input a release name when prompted.
From here an `isle` binary will be cross-compiled for all supported
platforms. This will take a long time the first time you perform it on your
machine.
Once compilation is completed, the release will be signed using the default GPG
key on your machine, and you will be prompted for its password in order to
create the signature.
## Releasing
Releases are uploaded to the repository's Releases page, and release naming
follows the conventional semantic versioning system. Each release should be
accompanied by a set of changes which have occurred since the last release,
described both in the `CHANGELOG.md` file and in the description on the Release
itself.

View File

@ -1,39 +0,0 @@
# Testing Isle
All tests are currently written as go tests, and as such can be run from the
`go` directory using the normal go testing tool.
```
cd go
go test -run Foo ./daemon
go test ./... # Test everything
```
## Integration Tests
Integration tests are those which require processes or state external to the
test itself. Integration tests are marked using the
`toolkit.MarkIntegrationTest` function, which will cause them to be skipped
unless being run in the integration test environment.
Besides a normal nix installation (like all Isle development needs), integration
tests also require `sudo` and [capsh][capsh] to be installed on the system.
[capsh]: https://www.man7.org/linux/man-pages/man1/capsh.1.html
By running tests using the `go/integration_test.sh` script the tests will be
automatically run in the integration test environment. All arguments will be
passed directly to the go testing tool.
```
cd go
./integration_test.sh -run Foo ./daemon
```
`integration_test.sh` wraps a call to `go test` in a bash shell which has all
required binaries available to it, and which has acquired necessary
[capabilities][capabilities] to use the binaries as needed. Acquiring
capabilities is done by elevating the user to root using `sudo`, and then
dropping them back down to a shell of the original user with capabilities set.
[capabilities]: https://wiki.archlinux.org/title/Capabilities

View File

@ -1,86 +0,0 @@
# Installation
This document will guide you through the process of obtaining and installing
Isle on your machine.
NOTE currently only linux machines with the following architectures are
supported:
- `x86_64` (aka `amd64`)
- `aarch64` (aka `arm64`)
- `i686`
(`i686` has not been tested.)
More OSs and architectures coming soon!
## Install isle
How isle gets installed depends on which Linux distribution you are using.
### Archlinux (also Manjaro)
Download the latest `.pkg.tar.zst` package file for your platform from
[this link][latest].
Install the package using pacman:
```
sudo pacman -U /path/to/isle-*.pkg.tar.zst
```
### Other Distributions
If a package file is not available for your distribution you can still install
an AppImage directly. It is assumed that all commands below are run as root.
Download the latest `.AppImage` binary for your platform from
[this link][latest], and place it in your `/usr/bin` directory.
Create a `daemon.yml` file using default values by doing:
```
mkdir -p /etc/isle/
isle daemon --dump-config > /etc/isle/daemon.yml
```
Create a system user for the isle daemon to run as:
```
useradd -r -s /bin/false -C "isle Daemon" isle
```
If your distro uses systemd, download [the latest systemd service
file][serviceFile] and place it in `/etc/systemd/system`. Run `systemctl
daemon-reload` to ensure systemd has seen the new service file.
If your distro uses an init system other than systemd then you will need to
configure that yourself. You can use the systemd service file linked above as a
reference.
[latest]: https://code.betamike.com/micropelago/isle/releases/latest
[serviceFile]: https://code.betamike.com/micropelago/isle/src/branch/main/dist/linux/isle.service
### From Source
If you'd like to build your own `isle` binary from scratch, see the [Building
Isle](./dev/building.md) document.
## Add Users to the `isle` Group (Optional)
If you wish to run isle commands as a user other than root, you can add that
user to the `isle` group:
```
sudo usermod -aG isle username
```
## Start the isle Service
Once installed and bootstrapped you can enable and start the isle service by
doing:
```
sudo systemctl enable --now isle
```
(NOTE If your distro uses an init system other than systemd then you will need
to instead start isle according to that system's requirements.)

View File

@ -1,7 +1,7 @@
# Contributing a Lighthouse
The [nebula][nebula] project provides the VPN component which is used by
Isle. Every nebula network requires at least one (but preferably more)
cryptic-net. Every nebula network requires at least one (but preferably more)
publicly accessible hosts. These hosts are called lighthouses.
Lighthouses do _not_ route traffic between hosts on the VPN. Rather, they
@ -10,7 +10,7 @@ NAT punching through any NATs that hosts might be behind. As such, they are very
lightweight to run, and require no storage resources at all.
If your host machine has a public static IP, or a dynamic public IP with
[dDNS][ddns] set up, then it can contribute a lighthouse.
[dDNS][ddns] set up, then it can contribute a lighthouse for cryptic-net.
[nebula]: https://github.com/slackhq/nebula
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
@ -26,14 +26,21 @@ traffic on that port to your host.
Configure your host's firewall to allow all UDP traffic on that port.
## Create daemon.yml
First, if you haven't already, [create a `daemon.yml`
file](../user/creating-a-daemonyml-file.md). This will be used to
configure your `cryptic-net daemon` process with the public address that other
hosts can find your daemon on.
## Edit daemon.yml
Open your `/etc/isle/daemon.yml` file in a text editor, and find the
`vpn.public_addr` field. Update that field to reflect your host's IP/DNS name
and your chosen UDP port.
Open your `daemon.yml` file in a text editor, and find the `vpn.public_addr`
field. Update that field to reflect your host's IP/DNS name and your chosen UDP
port.
## Restart the Daemon
With the `daemon.yml` configured, you should restart your `isle daemon`
With the `daemon.yml` configured, you should restart your `cryptic-net daemon`
process. On startup the daemon will add its public address to the global
configuration, which other hosts will pick up on and begin using.

View File

@ -1,12 +1,19 @@
# Contributing Storage
This document is for you if your host machine can be reliably be online at all
times and has 1GB or more of unused drive space you'd like to contribute to the
network.
If your host machine can be reasonably sure of being online most, if not all, of
the time, and has 100GB or more of unused drive space you'd like to contribute
to the network, then this document is for you.
## Edit `daemon.yml`
## Create daemon.yml
Open your `/etc/isle/daemon.yml` file in a text editor, and find the
First, if you haven't already, [create a `daemon.yml`
file](../user/creating-a-daemonyml-file.md). This will be used to
configure your `cryptic-net daemon` process with the storage locations and
capacities you want to contribute.
## Edit daemon.yml
Open your `daemon.yml` file in a text editor, and find the
`storage.allocations` section.
Each allocation in the allocations list describes the space being contributed
@ -16,7 +23,7 @@ one allocation listed.
The comments in the file should be self-explanatory, but ask your admin if you
need any clarification.
Here is an example set of allocations for a host which is contributing space
Here are an example set of allocations for a host which is contributing space
from two separate drives:
```
@ -24,44 +31,37 @@ storage:
allocations:
# 1.2 TB are being shared from drive1
- data_path: /mnt/drive1/isle/data
meta_path: /mnt/drive1/isle/meta
- data_path: /mnt/drive1/cryptic-net/data
meta_path: /mnt/drive1/cryptic-net/meta
capacity: 1200
api_port: 3900
rpc_port: 3901
web_port: 3902
# 100 GB are being shared from drive2
- data_path: /mnt/drive2/isle/data
meta_path: /mnt/drive2/isle/meta
# 100 GB (the minimum) are being shared from drive2
- data_path: /mnt/drive2/cryptic-net/data
meta_path: /mnt/drive2/cryptic-net/meta
capacity: 100
api_port: 3910
rpc_port: 3911
web_port: 3912
```
## Set Up Your Firewall
## Setup Firewall
See the doc on [Firewalls](./firewalls.md), to be sure that your host's firewall
is properly set up for providing storage.
You will need to configure your hosts's firewall to allow traffic from
cryptic-net IPs on the ports you specified in your allocations.
## Restart the Daemon
With the `daemon.yml` configured, you should restart your `isle daemon`
With the `daemon.yml` configured, you should restart your `cryptic-net daemon`
process.
The `isle daemon` will automatically allow the ports used for your
storage allocations in the vpn firewall.
## Removing Allocations
If you later decide to no longer provide storage simply remove the
`storage.allocations` item from your `/etc/isle/daemon.yml` file and restart the
`isle daemon` process.
Once removed, it is advisable to wait some time before removing storage
allocations from other hosts. This ensures that all data which was previously
on this host has had a chance to fully replicate to multiple other hosts.
## Further Reading
Isle uses the [garage][garage] project for its storage system. See the
cryptic-net uses the [garage][garage] project for its storage system. See the
[Managing Garage](managing-garage.md) document for more
information on how to interact directly with the garage instance being run by
isle.
cryptic-net.
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/

View File

@ -1,48 +0,0 @@
# Firewalls
When providing services on your host, whether
[network](./contributing-a-lighthouse.md) or
[storage](./contributing-storage.md), you will need to ensure that your host's
firewall is configured correctly to do so.
To make matters even more confusing, there are actually two firewalls at play:
the host's firewall, and the VPN firewall.
## VPN Firewall
Isle uses the [nebula](https://github.com/slackhq/nebula) project to
provide its VPN layer. Nebula ships with its own [builtin
firewall](https://nebula.defined.net/docs/config/firewall), which only applies
to connections coming in over the virtual network interface which it creates.
This firewall can be manually configured as part of the `/etc/isle/daemon.yml`
file.
Any storage instances which are defined as part of the `daemon.yml` file will
have their network ports automatically added to the VPN firewall by isle.
This means that you only need to configure the VPN firewall if you are hosting
services for your isle network besides storage.
## Host Firewall
The host you are running isle on will almost definitely have a firewall
running, separate from the VPN firewall. If you wish to provide services for
your isle network from your host, you will need to allow their ports in your
host's firewall.
**isle does _not_ automatically configure your host's firewall to any extent!**
One option is to open your host to all traffic from your isle network, and
allow the VPN firewall to be fully responsible for filtering traffic. To do this
on Linux using iptables, for example, you would add something like this to your
iptables configuration:
```
-A INPUT --source <network CIDR> --jump ACCEPT
```
being sure to replace the network CIDR with the one for you network.
If you don't feel comfortable allowing nebula to deal with all packet filtering,
you will need to manually determine and add the ports for each nebula service to
your host's firewall. It is recommended that you manually specify any storage
allocation ports defined in your `daemon.yml` if this is the approach you take.

View File

@ -1,11 +1,11 @@
# Managing Garage
The garage project provides the network storage component for
Isle. If you're reading this document then you would likely benefit
cryptic-net. If you're reading this document then you would likely benefit
greatly from reading the [garage documentation][garage] on their website. It's
extremely well written and concise.
Note that the `isle daemon` process will handle all setup steps described
Note that the `cryptic-net daemon` process will handle all setup steps described
in that documentation, but it's still good to have an understanding of how
garage works and what it can do.
@ -13,12 +13,12 @@ garage works and what it can do.
## Garage Runtime Note
There is an important thing to note regarding how isle runs garage. As
There is an important thing to note regarding how cryptic-net runs garage. As
described in the [Contributing Storage](contributing-storage.md) document, a
single `isle daemon` process can be configured to provide any number of
single `cryptic-net daemon` process can be configured to provide any number of
storage allocations.
For each allocation which is configured, `isle daemon` will configure and
For each allocation which is configured, `cryptic-net daemon` will configure and
run a separate `garage server` instance as a sub-process. Each garage will use
the host's name as its zone in the garage cluster layout, which means that the
cluster will prefer to not replicate the same data within the same host, but may
@ -26,14 +26,14 @@ do so if necessary.
## Garage CLI
Every `isle` binary contains a full `garage` binary embedded into it.
Every `cryptic-net` binary contains a full `garage` binary embedded into it.
This binary can be accessed directly like so:
```
sudo isle garage cli <subcmd> <args>
sudo cryptic-net garage cli <subcmd> <args>
```
Before handing off execution to the `garage` binary, the `isle` process
Before handing off execution to the `garage` binary, the `cryptic-net` process
will automatically set up the RPC host and secret environment variables.
If the host which is running the command has more than one allocation
@ -47,7 +47,7 @@ connected to.
To display the current layout of the garage cluster:
```
sudo isle garage cli layout show
sudo cryptic-net garage cli layout show
```
**(DO NOT CHANGE THE CLUSTER LAYOUT UNLESS YOU KNOW WHAT YOU'RE DOING!)**
@ -55,11 +55,11 @@ sudo isle garage cli layout show
To create a new bucket:
```
sudo isle garage cli bucket create new-bucket
sudo cryptic-net garage cli bucket create new-bucket
```
To list existing buckets:
```
sudo isle garage cli bucket list
sudo cryptic-net garage cli bucket list
```

100
docs/roadmap.md Normal file
View File

@ -0,0 +1,100 @@
# Roadmap
The following are rough outlines of upcoming work on the roadmap, roughly in the
order they will be implemented.
## Main quest
These items are listed more or less in the order they need to be completed, as
they generally depend on the items previous to them.
### Cross Compilation
Currently the only supported OS/CPU is Linux/amd64. This can be expanded
_theoretically_ quite easily, using nix's cross compilation tools. First target
should be OSX/arm64, but windows would also be quite the get.
### Bootstrap
This will be difficult. There's currently no way to bootstrap a new cryptic-net
network from scratch, only the currently existing one is supported. Support for
IPv6 internal CIDR should also be a part of this effort.
### Testing
Once bootstrap is generalized, we'll be able to write some automated tests.
## Side quests
These items aren't necessarily required by the main quest, and aren't dependent
on any other items being completed. They are nice-to-haves that we do want to
eventually complete, but aren't the main focus.
### DNS resolver settings
The daemon should update the resolver settings of the host, so that it
automatically serves DNS queries, unless set to not do so in `daemon.yml`.
### Install sub-command
It would be great to have a `cryptic-net install` sub-command which would
auto-detect the installed operating system and install the daemon automatically.
### Web server + interface
One idea is to have every `cryptic-net daemon` run a webserver as one of its
sub-processes. This server could serve multiple functions:
- Possible public gateway, if the host is configured to be public, into internal
cryptic-net services. This will require some sort of service discovery
mechanism that other hosts in the network can easily hook into via their
`daemon.yml`s. Ideally this mechanism would involve little beyond updating
entries in garage, which the public gateways then pick up on.
One wrinkle here will be TLS certificates. Ideally internal hosts could host
websites publicly via the gateways on their network, using arbitrary domains
that the users own. The gateways will need some way of obtaining certs for
these domains automatically (or as automatically as possible).
- Local interface for the `cryptic-net daemon` process. For example, status and
connectivity information for the local host could be provided via a simple web
interface, which the user can open in their browser. This saves us the effort
of needing to develop UIs for individual OSs. This could also make remotely
debugging hosts easier for admins.
### Mobile app
To start with a simple mobile app which provided connectivity to the network
would be great. Such a mobile app could be based on the existing
[mobile_nebula](https://github.com/DefinedNet/mobile_nebula). The main changes
needed would be:
- Allow importing a `bootstrap.tgz` file, rather than requiring manual setup by
users.
- Set device's DNS settings. There is an [open
PR](https://github.com/DefinedNet/mobile_nebula/pull/18) for android to do
this upstream.
- Rebranding and possibly submitting to Apple app store (bleh).
### Don't run as root
It's currently a pretty hard requirement for `cryptic-net daemon` to run as
root. This is due to:
- nebula's network interface root to be started.
- dnsmasq listening on port 53, generally a protected port.
If we can't figure out how to get these things running from the start as
non-privileged users, we at least need to get cryptic-net to drop priveleges
from root after initial startup.
### Plugins
It would not be difficult to spec out a plugin system using nix commands.
Existing components could be rigged to use this plugin system, and we could then
use the system to add future components which might prove useful. Once the
project is public such a system would be much appreciated I think, as it would
let other groups rig their binaries with all sorts of new functionality.

View File

@ -1,20 +1,14 @@
{
buildSystem ? builtins.currentSystem,
hostSystem ? buildSystem,
pkgsNix ? (import ../nix/pkgs.nix),
pkgs ? (import ../nix/pkgs.nix).stable,
}: let
pkgs = pkgsNix.default {
inherit buildSystem hostSystem;
};
in
pkgs.mkShell {
name = "isle-build-docs";
buildInputs = [ pkgs.plantuml ];
shellHook = ''
set -e
plantuml -tsvg ./dev/*.plantuml
exit 0
'';
}
}: pkgs.mkShell {
name = "cryptic-net-build-docs";
buildInputs = [ pkgs.plantuml ];
shellHook = ''
set -e
plantuml -tsvg ./dev/*.plantuml
exit 0
'';
}

View File

@ -0,0 +1,32 @@
# Creating a daemon.yml File
The `cryptic-net daemon` process has generally sane defaults and does not need
to be configured for most users. However, in some cases it does, so this
document describes how to use the `daemon.yml` file to handle those cases.
## Create daemon.yml
First, create a `daemon.yml` file. You can create a new `daemon.yml` with
default values filled in by doing:
```
cryptic-net daemon --dump-config > /path/to/daemon.yml
```
If you open that file in a text editor you can view all default values that
`cryptic-net daemon` ships with, as well as documentation for all configurable
parameters. Feel free to edit this file as needed.
## Using daemon.yml
With the `daemon.yml` created and configured, you can configure your daemon
process to use it by passing it as the `-c` argument:
```
sudo cryptic-net daemon -c /path/to/daemon.yml
```
If you are an operator then your host should be running its `cryptic-net daemon`
process in systemd (see [Getting Started](getting-started.md) if
not), and you will need to modify the `cryptic-net.service` accordingly.

View File

@ -0,0 +1,117 @@
# Getting Started
This document will guide you through the process of obtaining a cryptic-net
binary and joining the network.
NOTE currently only linux machines with amd64/x86_64 processors are supported.
More OSs and architectures coming soon!
## Obtaining a cryptic-net Binary
Every host can have a binary built for it which has all configuration for that
host embedded directly into it. Such binaries require no extra configuration by
the user to use, and have no dependencies on anything else in the user's system.
The process of obtaining a custom binary for your host is quite simple: ask an
admin of the network you'd like to join to give you one!
Note that if you'd like to join the network on multiple devices, each device
will needs its own binary, so be sure to tell your admin how many you want to
add and their names.
### Obtaining a cryptic-net Binary, the Hard Way
Alternatively, you can build your own binary by running the following from the
project's root:
```
nix-build -A appImage
```
(*NOTE* Dependencies of `cryptic-net` seemingly compile all of musl and rust
from scratch (it's not clear why, blame garage!). If you have not otherwise
configured it, nix might be using a tmpfs as its build directory, and the
capacity of this tmpfs will probably be exceeded by this build. You can change
your build directory to somewhere on-disk by setting the TMPDIR environment
variable for `nix-daemon` (see [this github issue][tmpdir-gh].))
The resulting binary can be found in the `result` directory which is created.
In this case you will need an admin to provide you with a `bootstrap.tgz` for
your host, rather than a custom binary. When running the daemon in the following
steps you will need to provide the `--bootstrap-path` CLI argument to the daemon
process.
[tmpdir-gh]: https://github.com/NixOS/nix/issues/2098#issuecomment-383243838
## Running the Daemon
Once you have a binary, you will need to run the `daemon` sub-command as the
root user. This can most easily be done using the `sudo` command, in a terminal:
```
sudo /path/to/cryptic-net daemon
```
This will start the daemon process, which will keep running until you kill it
with `ctrl-c`.
You can double check that the daemon is running properly by pinging a private IP
from the network in a separate terminal:
```
ping 10.10.0.1
```
If the pings are successful then your daemon is working!
## Installing the Daemon as a Systemd Service
NOTE in the future we will introduce an `install` sub-command which will
automate most of this section.
Rather than running the daemon manually, you can install it as a systemd
service. This way your daemon will automatically start in the background on
startup, and will be restarted if it has any issues.
To do so, create a file at `/etc/systemd/system/cryptic-net.service` with the
following contents:
```
[Unit]
Description=cryptic-net
Requires=network.target
After=network.target
[Service]
Restart=always
RestartSec=1s
User=root
ExecStart=/path/to/cryptic-net daemon
[Install]
WantedBy=multi-user.target
```
Remember to change the `/path/to/cryptic-net` part to the actual absolute path
to your binary!
Once created, perform the following commands in a terminal to enable the
service:
```
sudo systemctl daemon-reload
sudo systemctl enable --now cryptic-net
```
You can check the service's status by doing:
```
sudo systemctl status cryptic-net
```
and you can view its full logs by doing:
```
sudo journalctl -lu cryptic-net
```

View File

@ -1,20 +0,0 @@
# Join a Network
This document guide you through the process of joining an existing network
of isle hosts. If instead you wish to create a new network for others to join
then see the [Creating a New Network][creating-a-new-network] page.
To join an existing network you will need to first obtain a `bootstrap.json`
file. The `bootstrap.json` file contains all information required for your
particular host to join the network, and must be generated and provided to you
by an admin for the network.
Once obtained, you can join the network by doing:
```
isle network join --bootstrap-path /path/to/bootstrap.json
```
After a few moments you will have successfully joined the network!
[creating-a-new-network]: ../admin/creating-a-new-network.md

View File

@ -1,35 +1,36 @@
# Using DNS
Every `isle daemon` process ships with a DNS server which runs
Every `cryptic-net daemon` process ships with a DNS server which runs
automatically. This server will listen on port 53 on the VPN IP of that
particular host.
The server will serve requests for `<hostname>.hosts.<domain>` hostnames,
where `<hostname>` is the name of any host in the network, and `<domain`> is the
network's domain name.
The server will serve requests for `<hostname>.hosts.cryptic.io` hostnames,
where `<hostname>` is any host's name in the `bootstrap/nebula/hosts` directory.
The returned IP will be the corresponding IP for the host, as listed in the
host's `bootstrap/nebula/hosts` file.
If a request for a hostname not within the network's domain is received then the
server will forward the request to a pre-configured public resolver. The set of
public resolvers used can be configured in the `/etc/isle/daemon.yml` file.
If a request for a non `.cryptic.io` hostname is received then the server will
forward the request to a pre-configured public resolver. The set of public
resolvers used can be configured using the
[daemon.yml](creating-a-daemonyml-file.md) file.
This DNS server is an optional feature of Isle, and not required in general for
making use of the network.
This DNS server is an optional feature of cryptic-net, and not required in
general for making use of the network.
## Example
As an example of how to make use of this DNS server, let's say my host's IP on
the network is `10.10.1.1`, and my network's domain is `cool.internal`.
In order to configure the host to use the isle DNS server for all DNS
requests, I could do something like this:
the network is `10.10.1.1`. In order to configure the host to use the
cryptic-net DNS server for all DNS requests, I could do something like this:
```
sudo su
echo "nameserver 10.10.1.1" > /etc/resolv.conf
```
From that point, all DNS requests on my host would hit the isle DNS
server. If I request `my-host.hosts.cool.internal`, it would respond with the
appropriate private IP.
From that point, all DNS requests on my host would hit the cryptic-net DNS
server. If I request `my-host.cryptic.io`, it would respond with the appropriate
private IP.
NOTE that configuration of dns resolvers is very OS-specific, even amongst Linux
distributions, so ensure you know how your resolver configuration works before

View File

@ -1,7 +0,0 @@
{
"nodes": {
"root": {}
},
"root": "root",
"version": 7
}

View File

@ -1,38 +0,0 @@
# TODO this is currently broken, garage is using builtins.currentSystem for some
# reason.
{
description = "isle provides the foundation for an autonomous community cloud infrastructure";
outputs = {
self,
}: let
supportedSystems = (import ./nix/pkgs.nix).supportedSystems;
mkPkg = (buildSystem: hostSystem: let
defaultAttrs = (import ./default.nix) {
inherit hostSystem buildSystem;
revision = if self ? rev then self.rev else "dirty";
releaseName = "flake";
};
in
defaultAttrs.build.appImage
);
pkgsForBuildSystem = (buildSystem: {
default = mkPkg buildSystem buildSystem;
});
in {
packages = (builtins.foldl'
(pkgs: buildSystem:
pkgs // { "${buildSystem}" = pkgsForBuildSystem buildSystem; })
{}
supportedSystems
);
};
}

34
garage/default.nix Normal file
View File

@ -0,0 +1,34 @@
{
fetchgit,
buildEnv,
minio-client,
}: let
version = "0.8.0-rc1";
src = fetchgit {
name = "garage-v${version}";
url = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git";
rev = "2197753dfdb25944e55c25d911ae6d14b8506c4d";
sha256 = "sha256-Rzwx1/vl3xg5bj4Chxj8VLBZ25zlPawOc+uMl3AHhkw=";
};
in rec {
garage = (import "${src}/default.nix") { git_version = version; };
minioClient = minio-client;
env = buildEnv {
name = "cryptic-net-garage";
paths = [
garage
minioClient
./src
];
};
}

View File

@ -0,0 +1,24 @@
set -e -o pipefail
tmp="$(mktemp -d -t cryptic-net-garage-apply-layout-diff-XXX)"
( trap "rm -rf '$tmp'" EXIT
tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./hosts
thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname)
thisHostIP=$(cat "$tmp"/hosts/"$thisHostName".yml | yq '.nebula.ip')
firstRPCPort=$(cat "$_DAEMON_YML_PATH" | yq '.storage.allocations[0].rpc_port')
firstPeerID=$(cryptic-net-main garage-peer-keygen -danger -ip "$thisHostIP" -port "$firstRPCPort")
export GARAGE_RPC_HOST="$firstPeerID"@"$thisHostIP":"$firstRPCPort"
export GARAGE_RPC_SECRET=$(tar -xzf "$_BOOTSTRAP_PATH" --to-stdout "./garage/rpc-secret.txt")
garage layout show | cryptic-net-main garage-layout-diff | while read diffLine; do
echo "> $diffLine"
$diffLine
done
)

9
go-workspace/README.md Normal file
View File

@ -0,0 +1,9 @@
# go-workspace
This module is used for building all custom go binaries within the cryptic-net
project.
The reason binaries are contained here, and not under the sub-directory for the
sub-process the correspond to like most other code in this project, is that nix
makes it difficult to compose multiple modules defined locally. If nix ever
fixes this we should split this out.

18
go-workspace/default.nix Normal file
View File

@ -0,0 +1,18 @@
{
buildGoModule,
}: let
build = subPackage: buildGoModule {
pname = "cryptic-net-" + (builtins.baseNameOf subPackage);
version = "unstable";
src = ./src;
vendorSha256 = "sha256-UqMxbu/v/zDR4yJgUYiLs7HuHkvsZc/MJiWgw/8g+xk=";
subPackages = [
subPackage
];
};
in {
crypticNetMain = build "cmd/cryptic-net-main";
}

View File

@ -0,0 +1,135 @@
// Package admin deals with the parsing and creation of admin.tgz files.
package admin
import (
"cryptic-net/garage"
"cryptic-net/nebula"
"cryptic-net/tarutil"
"cryptic-net/yamlutil"
"fmt"
"io"
"io/fs"
"gopkg.in/yaml.v3"
)
const (
creationParamsPath = "admin/creation-params.yml"
nebulaCertsCACertPath = "nebula/certs/ca.crt"
nebulaCertsCAKeyPath = "nebula/certs/ca.key"
garageGlobalBucketKeyYmlPath = "garage/cryptic-net-global-bucket-key.yml"
garageAdminBucketKeyYmlPath = "garage/cryptic-net-admin-bucket-key.yml"
garageRPCSecretPath = "garage/rpc-secret.txt"
)
// CreationParams are general parameters used when creating a new network. These
// are available to all hosts within the network via their bootstrap files.
type CreationParams struct {
Domain string `yaml:"domain"`
CIDRs []string `yaml:"cidrs"`
}
// Admin is used for accessing all information contained within an admin.tgz.
type Admin struct {
CreationParams CreationParams
NebulaCACert nebula.CACert
GarageRPCSecret string
GarageGlobalBucketS3APICredentials garage.S3APICredentials
GarageAdminBucketS3APICredentials garage.S3APICredentials
}
// FromFS loads an Admin instance from the given fs.FS, which presumably
// represents the file structure of an admin.tgz file.
func FromFS(adminFS fs.FS) (Admin, error) {
var a Admin
filesToLoadAsYAML := []struct {
into interface{}
path string
}{
{&a.CreationParams, creationParamsPath},
{&a.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
{&a.GarageAdminBucketS3APICredentials, garageAdminBucketKeyYmlPath},
}
for _, f := range filesToLoadAsYAML {
if err := yamlutil.LoadYamlFSFile(f.into, adminFS, f.path); err != nil {
return Admin{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
}
}
filesToLoadAsString := []struct {
into *string
path string
}{
{&a.NebulaCACert.CACert, nebulaCertsCACertPath},
{&a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
{&a.GarageRPCSecret, garageRPCSecretPath},
}
for _, f := range filesToLoadAsString {
body, err := fs.ReadFile(adminFS, f.path)
if err != nil {
return Admin{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
}
*f.into = string(body)
}
return a, nil
}
// FromReader reads an admin.tgz file from the given io.Reader.
func FromReader(r io.Reader) (Admin, error) {
fs, err := tarutil.FSFromReader(r)
if err != nil {
return Admin{}, fmt.Errorf("reading admin.tgz: %w", err)
}
return FromFS(fs)
}
// WriteTo writes the Admin as a new admin.tgz to the given io.Writer.
func (a Admin) WriteTo(into io.Writer) error {
w := tarutil.NewTGZWriter(into)
filesToWriteAsYAML := []struct {
value interface{}
path string
}{
{a.CreationParams, creationParamsPath},
{a.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
{a.GarageAdminBucketS3APICredentials, garageAdminBucketKeyYmlPath},
}
for _, f := range filesToWriteAsYAML {
b, err := yaml.Marshal(f.value)
if err != nil {
return fmt.Errorf("yaml encoding data for %q: %w", f.path, err)
}
w.WriteFileBytes(f.path, b)
}
filesToWriteAsString := []struct {
value string
path string
}{
{a.NebulaCACert.CACert, nebulaCertsCACertPath},
{a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
{a.GarageRPCSecret, garageRPCSecretPath},
}
for _, f := range filesToWriteAsString {
w.WriteFileBytes(f.path, []byte(f.value))
}
return w.Close()
}

View File

@ -0,0 +1,212 @@
// Package bootstrap deals with the parsing and creation of bootstrap.tgz files.
// It also contains some helpers which rely on bootstrap data.
package bootstrap
import (
"cryptic-net/admin"
"cryptic-net/garage"
"cryptic-net/nebula"
"cryptic-net/tarutil"
"cryptic-net/yamlutil"
"crypto/sha512"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"gopkg.in/yaml.v3"
)
// Paths within the bootstrap FS which for general data.
const (
adminCreationParamsPath = "admin/creation-params.yml"
hostNamePath = "hostname"
)
// Bootstrap is used for accessing all information contained within a
// bootstrap.tgz file.
type Bootstrap struct {
AdminCreationParams admin.CreationParams
Hosts map[string]Host
HostName string
NebulaHostCert nebula.HostCert
GarageRPCSecret string
GarageGlobalBucketS3APICredentials garage.S3APICredentials
}
// FromFS loads a Boostrap instance from the given fs.FS, which presumably
// represents the file structure of a bootstrap.tgz file.
func FromFS(bootstrapFS fs.FS) (Bootstrap, error) {
var (
b Bootstrap
err error
)
if b.Hosts, err = loadHosts(bootstrapFS); err != nil {
return Bootstrap{}, fmt.Errorf("loading hosts info from fs: %w", err)
}
filesToLoadAsYAML := []struct {
into interface{}
path string
}{
{&b.AdminCreationParams, adminCreationParamsPath},
{&b.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
}
for _, f := range filesToLoadAsYAML {
if err := yamlutil.LoadYamlFSFile(f.into, bootstrapFS, f.path); err != nil {
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
}
}
filesToLoadAsString := []struct {
into *string
path string
}{
{&b.HostName, hostNamePath},
{&b.NebulaHostCert.CACert, nebulaCertsCACertPath},
{&b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
{&b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
{&b.GarageRPCSecret, garageRPCSecretPath},
}
for _, f := range filesToLoadAsString {
body, err := fs.ReadFile(bootstrapFS, f.path)
if err != nil {
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
}
*f.into = string(body)
}
return b, nil
}
// FromReader reads a bootstrap.tgz file from the given io.Reader.
func FromReader(r io.Reader) (Bootstrap, error) {
fs, err := tarutil.FSFromReader(r)
if err != nil {
return Bootstrap{}, fmt.Errorf("reading bootstrap.tgz: %w", err)
}
return FromFS(fs)
}
// FromFile reads a bootstrap.tgz from a file at the given path.
func FromFile(path string) (Bootstrap, error) {
f, err := os.Open(path)
if err != nil {
return Bootstrap{}, fmt.Errorf("opening file: %w", err)
}
defer f.Close()
return FromReader(f)
}
// WriteTo writes the Bootstrap as a new bootstrap.tgz to the given io.Writer.
func (b Bootstrap) WriteTo(into io.Writer) error {
w := tarutil.NewTGZWriter(into)
for _, host := range b.Hosts {
hostB, err := yaml.Marshal(host)
if err != nil {
return fmt.Errorf("yaml encoding host %#v: %w", host, err)
}
path := filepath.Join(hostsDirPath, host.Name+".yml")
w.WriteFileBytes(path, hostB)
}
filesToWriteAsYAML := []struct {
value interface{}
path string
}{
{b.AdminCreationParams, adminCreationParamsPath},
{b.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
}
for _, f := range filesToWriteAsYAML {
b, err := yaml.Marshal(f.value)
if err != nil {
return fmt.Errorf("yaml encoding data for %q: %w", f.path, err)
}
w.WriteFileBytes(f.path, b)
}
filesToWriteAsString := []struct {
value string
path string
}{
{b.HostName, hostNamePath},
{b.NebulaHostCert.CACert, nebulaCertsCACertPath},
{b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
{b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
{b.GarageRPCSecret, garageRPCSecretPath},
}
for _, f := range filesToWriteAsString {
w.WriteFileBytes(f.path, []byte(f.value))
}
return w.Close()
}
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
// HostName isn't found in the Hosts map.
func (b Bootstrap) ThisHost() Host {
host, ok := b.Hosts[b.HostName]
if !ok {
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.HostName))
}
return host
}
// Hash returns a deterministic hash of the given hosts map.
func HostsHash(hostsMap map[string]Host) ([]byte, error) {
hosts := make([]Host, 0, len(hostsMap))
for _, host := range hostsMap {
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
h := sha512.New()
if err := yaml.NewEncoder(h).Encode(hosts); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// WithHosts returns a copy of the Bootstrap with the given set of Hosts applied
// to it. It will _not_ overwrite the Host for _this_ host, however.
func (b Bootstrap) WithHosts(hosts map[string]Host) Bootstrap {
hostsCopy := make(map[string]Host, len(hosts))
for name, host := range hosts {
hostsCopy[name] = host
}
hostsCopy[b.HostName] = b.ThisHost()
b.Hosts = hostsCopy
return b
}

View File

@ -0,0 +1,86 @@
package bootstrap
import (
"cryptic-net/garage"
"fmt"
)
// Paths within the bootstrap FS related to garage.
const (
garageGlobalBucketKeyYmlPath = "garage/cryptic-net-global-bucket-key.yml"
garageRPCSecretPath = "garage/rpc-secret.txt"
)
// GaragePeers returns a Peer for each known garage instance in the network.
func (b Bootstrap) GaragePeers() []garage.Peer {
var peers []garage.Peer
for _, host := range b.Hosts {
if host.Garage == nil {
continue
}
for _, instance := range host.Garage.Instances {
peer := garage.Peer{
IP: host.Nebula.IP,
RPCPort: instance.RPCPort,
S3APIPort: instance.S3APIPort,
}
peers = append(peers, peer)
}
}
return peers
}
// GarageRPCPeerAddrs returns the full RPC peer address for each known garage
// instance in the network.
func (b Bootstrap) GarageRPCPeerAddrs() []string {
var addrs []string
for _, peer := range b.GaragePeers() {
addrs = append(addrs, peer.RPCPeerAddr())
}
return addrs
}
// ChooseGaragePeer returns a Peer for a garage instance from the network. It
// will prefer a garage instance on this particular host, if there is one, but
// will otherwise return a random endpoint.
func (b Bootstrap) ChooseGaragePeer() garage.Peer {
thisHost := b.ThisHost()
if thisHost.Garage != nil && len(thisHost.Garage.Instances) > 0 {
inst := thisHost.Garage.Instances[0]
return garage.Peer{
IP: thisHost.Nebula.IP,
RPCPort: inst.RPCPort,
S3APIPort: inst.S3APIPort,
}
}
for _, peer := range b.GaragePeers() {
return peer
}
panic("no garage instances configured")
}
// GlobalBucketS3APIClient returns an S3 client pre-configured with access to
// the global bucket.
func (b Bootstrap) GlobalBucketS3APIClient() (garage.S3APIClient, error) {
addr := b.ChooseGaragePeer().S3APIAddr()
creds := b.GarageGlobalBucketS3APICredentials
client, err := garage.NewS3APIClient(addr, creds)
if err != nil {
return nil, fmt.Errorf("connecting to garage S3 API At %q: %w", addr, err)
}
return client, err
}

View File

@ -0,0 +1,111 @@
package bootstrap
import (
"bytes"
"context"
"cryptic-net/garage"
"fmt"
"log"
"path/filepath"
"github.com/minio/minio-go/v7"
"gopkg.in/yaml.v3"
)
// Paths within garage's global bucket
const (
garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts"
)
// PutGarageBoostrapHost places the <hostname>.yml file for the given host into
// garage so that other hosts are able to see relevant configuration for it.
//
// The given client should be for the global bucket.
func PutGarageBoostrapHost(
ctx context.Context, client garage.S3APIClient, host Host,
) error {
buf := new(bytes.Buffer)
if err := yaml.NewEncoder(buf).Encode(host); err != nil {
log.Fatalf("yaml encoding host data: %v", err)
}
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, host.Name+".yml")
_, err := client.PutObject(
ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()),
minio.PutObjectOptions{},
)
if err != nil {
return fmt.Errorf("writing to %q in global bucket: %w", filePath, err)
}
return nil
}
// RemoveGarageBootstrapHost removes the <hostname>.yml for the given host from
// garage.
//
// The given client should be for the global bucket.
func RemoveGarageBootstrapHost(
ctx context.Context, client garage.S3APIClient, hostName string,
) error {
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, hostName+".yml")
return client.RemoveObject(
ctx, garage.GlobalBucket, filePath,
minio.RemoveObjectOptions{},
)
}
// GetGarageBootstrapHosts loads the <hostname>.yml file for all hosts stored in
// garage.
//
// The given client should be for the global bucket.
func GetGarageBootstrapHosts(
ctx context.Context, client garage.S3APIClient,
) (
map[string]Host, error,
) {
hosts := map[string]Host{}
objInfoCh := client.ListObjects(
ctx, garage.GlobalBucket,
minio.ListObjectsOptions{
Prefix: garageGlobalBucketBootstrapHostsDirPath,
Recursive: true,
},
)
for objInfo := range objInfoCh {
if objInfo.Err != nil {
return nil, fmt.Errorf("listing objects: %w", objInfo.Err)
}
obj, err := client.GetObject(
ctx, garage.GlobalBucket, objInfo.Key, minio.GetObjectOptions{},
)
if err != nil {
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
}
var host Host
err = yaml.NewDecoder(obj).Decode(&host)
obj.Close()
if err != nil {
return nil, fmt.Errorf("yaml decoding object %q: %w", objInfo.Key, err)
}
hosts[host.Name] = host
}
return hosts, nil
}

View File

@ -0,0 +1,82 @@
package bootstrap
import (
"fmt"
"io/fs"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
const (
hostsDirPath = "hosts"
)
// NebulaHost describes the nebula configuration of a Host which is relevant for
// other hosts to know.
type NebulaHost struct {
IP string `yaml:"ip"`
PublicAddr string `yaml:"public_addr,omitempty"`
}
// GarageHost describes a single garage instance in the GarageHost.
type GarageHostInstance struct {
RPCPort int `yaml:"rpc_port"`
S3APIPort int `yaml:"s3_api_port"`
WebPort int `yaml:"web_port"`
}
// GarageHost describes the garage configuration of a Host which is relevant for
// other hosts to know.
type GarageHost struct {
Instances []GarageHostInstance `yaml:"instances"`
}
// Host consolidates all information about a single host from the bootstrap
// file.
type Host struct {
Name string `yaml:"name"`
Nebula NebulaHost `yaml:"nebula"`
Garage *GarageHost `yaml:"garage,omitempty"`
}
func loadHosts(bootstrapFS fs.FS) (map[string]Host, error) {
hosts := map[string]Host{}
readAsYaml := func(into interface{}, path string) error {
b, err := fs.ReadFile(bootstrapFS, path)
if err != nil {
return fmt.Errorf("reading file from fs: %w", err)
}
return yaml.Unmarshal(b, into)
}
globPath := filepath.Join(hostsDirPath, "*.yml")
hostPaths, err := fs.Glob(bootstrapFS, globPath)
if err != nil {
return nil, fmt.Errorf("listing host files at %q in fs: %w", globPath, err)
}
for _, hostPath := range hostPaths {
hostName := filepath.Base(hostPath)
hostName = strings.TrimSuffix(hostName, filepath.Ext(hostName))
var host Host
if err := readAsYaml(&host, hostPath); err != nil {
return nil, fmt.Errorf("reading %q as yaml: %w", hostPath, err)
}
hosts[hostName] = host
}
if len(hosts) == 0 {
return nil, fmt.Errorf("failed to load any hosts from fs")
}
return hosts, nil
}

View File

@ -0,0 +1,8 @@
package bootstrap
// Paths within the bootstrap FS related to nebula.
const (
nebulaCertsCACertPath = "nebula/certs/ca.crt"
nebulaCertsHostCertPath = "nebula/certs/host.crt"
nebulaCertsHostKeyPath = "nebula/certs/host.key"
)

View File

@ -0,0 +1,81 @@
package main
//
// This binary acts as a wrapper around other programs which would otherwise
// form their own binaries. We do this for two reasons:
//
// * Nix makes it difficult to determine which individuals binaries need to be
// rebuilt upon changes, so it rebuilds all of them no matter what changed. This
// makes development slow. By wrapping everything in a sinble binary we only
// ever have to build that binary.
//
// * If we have N binaries total, then we have N copies of the go runtime in our
// final AppImage. By bundling the binaries into a single one we can reduce the
// number go runtime copies to 1.
//
import (
"cryptic-net/cmd/entrypoint"
garage_entrypoint "cryptic-net/cmd/garage-entrypoint"
garage_layout_diff "cryptic-net/cmd/garage-layout-diff"
garage_peer_keygen "cryptic-net/cmd/garage-peer-keygen"
nebula_entrypoint "cryptic-net/cmd/nebula-entrypoint"
update_global_bucket "cryptic-net/cmd/update-global-bucket"
"fmt"
"os"
)
type mainFn struct {
name string
fn func()
}
var mainFns = []mainFn{
{"entrypoint", entrypoint.Main},
{"garage-entrypoint", garage_entrypoint.Main},
{"garage-layout-diff", garage_layout_diff.Main},
{"garage-peer-keygen", garage_peer_keygen.Main},
{"nebula-entrypoint", nebula_entrypoint.Main},
{"update-global-bucket", update_global_bucket.Main},
}
var mainFnsMap = func() map[string]mainFn {
m := map[string]mainFn{}
for _, mainFn := range mainFns {
m[mainFn.name] = mainFn
}
return m
}()
func usage() {
fmt.Fprintf(os.Stderr, "USAGE: %s <cmd>\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Commands:\n\n")
for _, mainFn := range mainFns {
fmt.Fprintf(os.Stderr, "%s\n", mainFn.name)
}
os.Stderr.Sync()
os.Exit(1)
}
func main() {
if len(os.Args) < 2 {
usage()
}
mainFn, ok := mainFnsMap[os.Args[1]]
if !ok {
usage()
}
// remove os.Args[1] from the arg list, so that other commands which consume
// args don't get confused
os.Args = append(os.Args[:1], os.Args[2:]...)
mainFn.fn()
}

View File

@ -0,0 +1,208 @@
package entrypoint
import (
"cryptic-net/admin"
"cryptic-net/bootstrap"
"cryptic-net/nebula"
"errors"
"fmt"
"net"
"os"
"strings"
)
func readAdmin(path string) (admin.Admin, error) {
if path == "-" {
adm, err := admin.FromReader(os.Stdin)
if err != nil {
return admin.Admin{}, fmt.Errorf("parsing admin.tgz from stdin: %w", err)
}
return adm, nil
}
f, err := os.Open(path)
if err != nil {
return admin.Admin{}, fmt.Errorf("opening file: %w", err)
}
defer f.Close()
return admin.FromReader(f)
}
var subCmdAdminCreateNetwork = subCmd{
name: "create-network",
descr: "Creates a new cryptic-net network, outputting the resulting admin.tgz to stdout",
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
daemonYmlPath := flags.StringP(
"config-path", "c", "",
"Optional path to a daemon.yml file to load configuration from.",
)
dumpConfig := flags.Bool(
"dump-config", false,
"Write the default configuration file to stdout and exit.",
)
domain := flags.StringP(
"domain", "d", "",
"Domain name that should be used as the root domain in the network.",
)
ipCIDRStrs := flags.StringP(
"ip-cidrs", "i", "",
"Comma-separated list of CIDRs which denote what IPs hosts on the network can be assigned.",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
env := subCmdCtx.env
if *dumpConfig {
return writeBuiltinDaemonYml(env, os.Stdout)
}
if *domain == "" {
return errors.New("--domain is required")
}
*domain = strings.TrimRight(strings.TrimLeft(*domain, "."), ".")
var (
ipCIDRs []*net.IPNet
thisIP net.IP
)
for _, ipCIDRStr := range strings.Split(*ipCIDRStrs, ",") {
ipCIDRStr = strings.TrimSpace(ipCIDRStr)
if ipCIDRStr == "" {
continue
}
ip, ipCIDR, err := net.ParseCIDR(ipCIDRStr)
if err != nil {
return fmt.Errorf("could not parse CIDR %q: %w", ipCIDRStr, err)
}
thisIP = ip // we just need one IP from a CIDR, it doesn't matter which.
ipCIDRs = append(ipCIDRs, ipCIDR)
}
runtimeDirPath := env.RuntimeDirPath
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath)
if err := os.MkdirAll(runtimeDirPath, 0700); err != nil {
return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err)
}
defer func() {
fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", runtimeDirPath)
if err := os.RemoveAll(runtimeDirPath); err != nil {
fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", runtimeDirPath, err)
}
}()
if err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil {
return fmt.Errorf("merging and writing daemon.yml file: %w", err)
}
daemon := env.ThisDaemon()
if len(daemon.Storage.Allocations) < 3 {
return fmt.Errorf("daemon.yml with at least 3 allocations was not provided")
}
},
}
var subCmdAdminMakeBootstrap = subCmd{
name: "make-bootstrap",
descr: "Creates a new bootstrap.tgz file for a particular host and writes it to stdout",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
name := flags.StringP(
"name", "n", "",
"Name of the host to generate bootstrap.tgz for",
)
adminPath := flags.StringP(
"admin-path", "a", "",
`Path to admin.tgz file. If the given path is "-" then stdin is used.`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" || *adminPath == "" {
return errors.New("--name and --admin-path are required")
}
env := subCmdCtx.env
adm, err := readAdmin(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
}
client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
// NOTE this isn't _technically_ required, but if the `hosts add`
// command for this host has been run recently then it might not have
// made it into the bootstrap file yet, and so won't be in
// `env.Bootstrap`.
hosts, err := bootstrap.GetGarageBootstrapHosts(env.Context, client)
if err != nil {
return fmt.Errorf("retrieving host info from garage: %w", err)
}
host, ok := hosts[*name]
if !ok {
return fmt.Errorf("couldn't find host into for %q in garage, has `cryptic-net hosts add` been run yet?", *name)
}
nebulaHostCert, err := nebula.NewHostCert(adm.NebulaCACert, host.Name, host.Nebula.IP)
if err != nil {
return fmt.Errorf("creating new nebula host key/cert: %w", err)
}
newBootstrap := bootstrap.Bootstrap{
AdminCreationParams: adm.CreationParams,
Hosts: hosts,
HostName: *name,
NebulaHostCert: nebulaHostCert,
GarageRPCSecret: adm.GarageRPCSecret,
GarageGlobalBucketS3APICredentials: adm.GarageGlobalBucketS3APICredentials,
}
return newBootstrap.WriteTo(os.Stdout)
},
}
var subCmdAdmin = subCmd{
name: "admin",
descr: "Sub-commands which only admins can run",
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdAdminMakeBootstrap,
)
},
}

View File

@ -0,0 +1,371 @@
package entrypoint
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strconv"
"sync"
"time"
crypticnet "cryptic-net"
"cryptic-net/bootstrap"
"cryptic-net/garage"
"github.com/cryptic-io/pmux/pmuxlib"
)
// The daemon sub-command deals with starting an actual cryptic-net daemon
// process, which is required to be running for most other cryptic-net
// functionality. The sub-command does the following:
//
// * Creates and locks the runtime directory.
//
// * Creates the data directory and copies the appdir bootstrap file into there,
// if it's not already there.
//
// * Merges the user-provided daemon.yml file with the default, and writes the
// result to the runtime dir.
//
// * Merges daemon.yml configuration into the bootstrap configuration, and
// rewrites the bootstrap file.
//
// * Sets up environment variables that all other sub-processes then use, based
// on the runtime dir.
//
// * Dynamically creates the root pmux config and runs pmux.
//
// * (On exit) cleans up the runtime directory.
func copyBootstrapToDataDir(env *crypticnet.Env, r io.Reader) error {
path := env.DataDirBootstrapPath()
dirPath := filepath.Dir(path)
if err := os.MkdirAll(dirPath, 0700); err != nil {
return fmt.Errorf("creating directory %q: %w", dirPath, err)
}
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating file %q: %w", path, err)
}
_, err = io.Copy(f, r)
f.Close()
if err != nil {
return fmt.Errorf("copying bootstrap file to %q: %w", path, err)
}
if err := env.LoadBootstrap(path); err != nil {
return fmt.Errorf("loading bootstrap from %q: %w", path, err)
}
return nil
}
// creates a new bootstrap file using available information from the network. If
// the new bootstrap file is different than the existing one, the existing one
// is overwritten, ReloadBootstrap is called on env, true is returned.
func reloadBootstrap(env *crypticnet.Env, s3Client garage.S3APIClient) (bool, error) {
newHosts, err := bootstrap.GetGarageBootstrapHosts(env.Context, s3Client)
if err != nil {
return false, fmt.Errorf("getting hosts from garage: %w", err)
}
newHostsHash, err := bootstrap.HostsHash(newHosts)
if err != nil {
return false, fmt.Errorf("calculating hash of new hosts: %w", err)
}
currHostsHash, err := bootstrap.HostsHash(env.Bootstrap.Hosts)
if err != nil {
return false, fmt.Errorf("calculating hash of current hosts: %w", err)
}
if bytes.Equal(newHostsHash, currHostsHash) {
return false, nil
}
buf := new(bytes.Buffer)
if err := env.Bootstrap.WithHosts(newHosts).WriteTo(buf); err != nil {
return false, fmt.Errorf("writing new bootstrap file to buffer: %w", err)
}
if err := copyBootstrapToDataDir(env, buf); err != nil {
return false, fmt.Errorf("copying new bootstrap file to data dir: %w", err)
}
return true, nil
}
// runs a single pmux process ofor daemon, returning only once the env.Context
// has been canceled or bootstrap info has been changed. This will always block
// until the spawned pmux has returned.
func runDaemonPmuxOnce(env *crypticnet.Env, s3Client garage.S3APIClient) error {
thisHost := env.Bootstrap.ThisHost()
thisDaemon := env.ThisDaemon()
fmt.Fprintf(os.Stderr, "host name is %q, ip is %q\n", thisHost.Name, thisHost.Nebula.IP)
pmuxProcConfigs := []pmuxlib.ProcessConfig{
{
Name: "nebula",
Cmd: "cryptic-net-main",
Args: []string{
"nebula-entrypoint",
},
},
{
Name: "dnsmasq",
Cmd: "bash",
Args: []string{
"wait-for-ip",
thisHost.Nebula.IP,
"dnsmasq-entrypoint",
},
},
}
{
var args []string
if allocs := thisDaemon.Storage.Allocations; len(allocs) > 0 {
for _, alloc := range allocs {
args = append(
args,
"wait-for",
net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
"--",
)
}
} else {
args = []string{
"wait-for-ip",
thisHost.Nebula.IP,
}
}
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
Name: "update-global-bucket",
Cmd: "bash",
Args: append(args, "update-global-bucket"),
NoRestartOn: []int{0},
})
}
if len(thisDaemon.Storage.Allocations) > 0 {
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
Name: "garage",
Cmd: "bash",
Args: []string{
"wait-for-ip",
thisHost.Nebula.IP,
"cryptic-net-main", "garage-entrypoint",
},
// garage can take a while to clean up
SigKillWait: (1 * time.Minute) + (10 * time.Second),
})
}
pmuxConfig := pmuxlib.Config{Processes: pmuxProcConfigs}
doneCh := env.Context.Done()
var wg sync.WaitGroup
defer wg.Wait()
ctx, cancel := context.WithCancel(env.Context)
defer cancel()
wg.Add(1)
go func() {
defer wg.Done()
pmuxlib.Run(ctx, pmuxConfig)
}()
ticker := time.NewTicker(3 * time.Minute)
defer ticker.Stop()
for {
select {
case <-doneCh:
return env.Context.Err()
case <-ticker.C:
fmt.Fprintln(os.Stderr, "checking for changes to bootstrap")
if changed, err := reloadBootstrap(env, s3Client); err != nil {
return fmt.Errorf("reloading bootstrap: %w", err)
} else if changed {
fmt.Fprintln(os.Stderr, "bootstrap info has changed, restarting all processes")
return nil
}
}
}
}
var subCmdDaemon = subCmd{
name: "daemon",
descr: "Runs the cryptic-net daemon (Default if no sub-command given)",
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
daemonYmlPath := flags.StringP(
"config-path", "c", "",
"Optional path to a daemon.yml file to load configuration from.",
)
dumpConfig := flags.Bool(
"dump-config", false,
"Write the default configuration file to stdout and exit.",
)
bootstrapPath := flags.StringP(
"bootstrap-path", "b", "",
`Path to a bootstrap.tgz file. This only needs to be provided the first time the daemon is started, after that it is ignored. If the cryptic-net binary has a bootstrap built into it then this argument is always optional.`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
env := subCmdCtx.env
if *dumpConfig {
return writeBuiltinDaemonYml(env, os.Stdout)
}
runtimeDirPath := env.RuntimeDirPath
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath)
if err := os.MkdirAll(runtimeDirPath, 0700); err != nil {
return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err)
} else if err := crypticnet.NewProcLock(runtimeDirPath).WriteLock(); err != nil {
return err
}
// do not defer the cleaning of the runtime directory until the lock has
// been obtained, otherwise we might delete the directory out from under
// the feet of an already running daemon
defer func() {
fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", runtimeDirPath)
if err := os.RemoveAll(runtimeDirPath); err != nil {
fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", runtimeDirPath, err)
}
}()
// If the bootstrap file is not being stored in the data dir, move it
// there and reload the bootstrap info
if env.BootstrapPath != env.DataDirBootstrapPath() {
path := env.BootstrapPath
// If there's no BootstrapPath then no bootstrap file could be
// found. In this case we require the user to provide one on the
// command-line.
if path == "" {
if *bootstrapPath == "" {
return errors.New("No bootstrap.tgz file could be found, and one is not provided with --bootstrap-path")
}
path = *bootstrapPath
}
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("opening file %q: %w", env.BootstrapPath, err)
}
err = copyBootstrapToDataDir(env, f)
f.Close()
if err != nil {
return fmt.Errorf("copying bootstrap file from %q: %w", path, err)
}
}
if err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil {
return fmt.Errorf("merging and writing daemon.yml file: %w", err)
}
{
// we update this Host's data using whatever configuration has been
// provided by daemon.yml. This way the daemon has the most
// up-to-date possible bootstrap. This updated bootstrap will later
// get updated in garage using update-global-bucket, so other hosts
// will see it as well.
// ThisDaemon can only be called after writeDaemonYml.
daemon := env.ThisDaemon()
host := env.Bootstrap.ThisHost()
host.Nebula.PublicAddr = daemon.VPN.PublicAddr
host.Garage = nil
if allocs := daemon.Storage.Allocations; len(allocs) > 0 {
host.Garage = new(bootstrap.GarageHost)
for _, alloc := range allocs {
host.Garage.Instances = append(host.Garage.Instances, bootstrap.GarageHostInstance{
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
WebPort: alloc.WebPort,
})
}
}
env.Bootstrap.Hosts[host.Name] = host
buf := new(bytes.Buffer)
if err := env.Bootstrap.WithHosts(env.Bootstrap.Hosts).WriteTo(buf); err != nil {
return fmt.Errorf("writing new bootstrap file to buffer: %w", err)
}
if err := copyBootstrapToDataDir(env, buf); err != nil {
return fmt.Errorf("copying new bootstrap file to data dir: %w", err)
}
}
for key, val := range env.ToMap() {
if err := os.Setenv(key, val); err != nil {
return fmt.Errorf("failed to set %q to %q: %w", key, val, err)
}
}
for {
// create s3Client anew on every loop, in case the topology has
// changed and we should be connecting to a different garage
// endpoint.
s3Client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
if err := runDaemonPmuxOnce(env, s3Client); errors.Is(err, context.Canceled) {
return nil
} else if err != nil {
return fmt.Errorf("running pmux for daemon: %w", err)
}
}
},
}

View File

@ -0,0 +1,72 @@
package entrypoint
import (
crypticnet "cryptic-net"
"cryptic-net/yamlutil"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/imdario/mergo"
"gopkg.in/yaml.v3"
)
func builtinDaemonYmlPath(env *crypticnet.Env) string {
return filepath.Join(env.AppDirPath, "etc", "daemon.yml")
}
func writeBuiltinDaemonYml(env *crypticnet.Env, w io.Writer) error {
builtinDaemonYmlPath := builtinDaemonYmlPath(env)
builtinDaemonYml, err := os.ReadFile(builtinDaemonYmlPath)
if err != nil {
return fmt.Errorf("reading default daemon.yml at %q: %w", builtinDaemonYmlPath, err)
}
if _, err := w.Write(builtinDaemonYml); err != nil {
return fmt.Errorf("writing default daemon.yml: %w", err)
}
return nil
}
func writeMergedDaemonYml(env *crypticnet.Env, userDaemonYmlPath string) error {
builtinDaemonYmlPath := builtinDaemonYmlPath(env)
var fullDaemonYml map[string]interface{}
if err := yamlutil.LoadYamlFile(&fullDaemonYml, builtinDaemonYmlPath); err != nil {
return fmt.Errorf("parsing builtin daemon.yml file: %w", err)
}
if userDaemonYmlPath != "" {
var daemonYml map[string]interface{}
if err := yamlutil.LoadYamlFile(&daemonYml, userDaemonYmlPath); err != nil {
return fmt.Errorf("parsing %q: %w", userDaemonYmlPath, err)
}
err := mergo.Merge(&fullDaemonYml, daemonYml, mergo.WithOverride)
if err != nil {
return fmt.Errorf("merging contents of file %q: %w", userDaemonYmlPath, err)
}
}
fullDaemonYmlB, err := yaml.Marshal(fullDaemonYml)
if err != nil {
return fmt.Errorf("yaml marshaling daemon config: %w", err)
}
daemonYmlPath := filepath.Join(env.RuntimeDirPath, "daemon.yml")
if err := ioutil.WriteFile(daemonYmlPath, fullDaemonYmlB, 0400); err != nil {
return fmt.Errorf("writing daemon.yml file to %q: %w", daemonYmlPath, err)
}
return nil
}

View File

@ -0,0 +1,118 @@
package entrypoint
import (
"fmt"
"os"
"syscall"
)
var subCmdGarageMC = subCmd{
name: "mc",
descr: "Runs the mc (minio-client) binary. The cryptic-net garage can be accessed under the `garage` alias",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(true)
keyID := flags.StringP(
"key-id", "i", "",
"Optional key ID to use, defaults to that of the shared cryptic-net-global key",
)
keySecret := flags.StringP(
"key-secret", "s", "",
"Optional key secret to use, defaults to that of the shared cryptic-net-global key",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
env := subCmdCtx.env
s3APIAddr := env.Bootstrap.ChooseGaragePeer().S3APIAddr()
if *keyID == "" || *keySecret == "" {
if *keyID == "" {
*keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.ID
}
if *keySecret == "" {
*keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.Secret
}
}
args := flags.Args()
if i := flags.ArgsLenAtDash(); i >= 0 {
args = args[i:]
}
args = append([]string{"mc"}, args...)
var (
binPath = env.BinPath("mc")
cliEnv = append(
os.Environ(),
fmt.Sprintf(
"MC_HOST_garage=http://%s:%s@%s",
*keyID, *keySecret, s3APIAddr,
),
// The garage docs say this is necessary, though nothing bad
// seems to happen if we leave it out *shrug*
"MC_REGION=garage",
)
)
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
return fmt.Errorf(
"calling exec(%q, %#v, %#v): %w",
binPath, args, cliEnv, err,
)
}
return nil
},
}
var subCmdGarageCLI = subCmd{
name: "cli",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running cryptic-net daemon",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
env := subCmdCtx.env
var (
binPath = env.BinPath("garage")
args = append([]string{"garage"}, subCmdCtx.args...)
cliEnv = append(
os.Environ(),
"GARAGE_RPC_HOST="+env.Bootstrap.ChooseGaragePeer().RPCAddr(),
"GARAGE_RPC_SECRET="+env.Bootstrap.GarageRPCSecret,
)
)
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
return fmt.Errorf(
"calling exec(%q, %#v, %#v): %w",
binPath, args, cliEnv, err,
)
}
return nil
},
}
var subCmdGarage = subCmd{
name: "garage",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running cryptic-net daemon",
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdGarageCLI,
subCmdGarageMC,
)
},
}

View File

@ -0,0 +1,151 @@
package entrypoint
import (
"cryptic-net/bootstrap"
"errors"
"fmt"
"net"
"os"
"regexp"
"sort"
"gopkg.in/yaml.v3"
)
var hostNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
func validateHostName(name string) error {
if !hostNameRegexp.MatchString(name) {
return errors.New("a host's name must start with a letter and only contain letters, numbers, and dashes")
}
return nil
}
var subCmdHostsAdd = subCmd{
name: "add",
descr: "Adds a host to the network",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
name := flags.StringP(
"name", "n", "",
"Name of the new host",
)
ip := flags.StringP(
"ip", "i", "",
"IP of the new host",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" || *ip == "" {
return errors.New("--name and --ip are required")
}
if err := validateHostName(*name); err != nil {
return fmt.Errorf("invalid hostname %q: %w", *name, err)
}
if net.ParseIP(*ip) == nil {
return fmt.Errorf("invalid ip %q", *ip)
}
// TODO validate that the IP is in the correct CIDR
env := subCmdCtx.env
client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
host := bootstrap.Host{
Name: *name,
Nebula: bootstrap.NebulaHost{
IP: *ip,
},
}
return bootstrap.PutGarageBoostrapHost(env.Context, client, host)
},
}
var subCmdHostsList = subCmd{
name: "list",
descr: "Lists all hosts in the network, and their IPs",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
env := subCmdCtx.env
client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
hostsMap, err := bootstrap.GetGarageBootstrapHosts(env.Context, client)
if err != nil {
return fmt.Errorf("retrieving hosts from garage: %w", err)
}
hosts := make([]bootstrap.Host, 0, len(hostsMap))
for _, host := range hostsMap {
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
return yaml.NewEncoder(os.Stdout).Encode(hosts)
},
}
var subCmdHostsDelete = subCmd{
name: "delete",
descr: "Deletes a host from the network",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
name := flags.StringP(
"name", "n", "",
"Name of the host to delete",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" {
return errors.New("--name is required")
}
env := subCmdCtx.env
client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
return bootstrap.RemoveGarageBootstrapHost(env.Context, client, *name)
},
}
var subCmdHosts = subCmd{
name: "hosts",
descr: "Sub-commands having to do with configuration of hosts in the network",
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdHostsAdd,
subCmdHostsDelete,
subCmdHostsList,
)
},
}

View File

@ -0,0 +1,37 @@
package entrypoint
import (
"fmt"
"os"
crypticnet "cryptic-net"
)
// The purpose of this binary is to act as the entrypoint of the cryptic-net
// process. It processes the command-line arguments which are passed in, and
// then passes execution along to an appropriate binary housed in AppDir/bin
// (usually a bash script, which is more versatile than a go program).
func Main() {
env, err := crypticnet.NewEnv(true)
if err != nil {
panic(fmt.Sprintf("loading environment: %v", err))
}
err = subCmdCtx{
args: os.Args[1:],
env: env,
}.doSubCmd(
subCmdAdmin,
subCmdDaemon,
subCmdGarage,
subCmdHosts,
subCmdVersion,
)
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,121 @@
package entrypoint
import (
crypticnet "cryptic-net"
"fmt"
"os"
"strings"
"github.com/spf13/pflag"
)
// subCmdCtx contains all information available to a subCmd's do method.
type subCmdCtx struct {
subCmd subCmd // the subCmd itself
args []string // command-line arguments, excluding the subCmd itself.
subCmdNames []string // names of subCmds so far, including this one
env *crypticnet.Env
}
type subCmd struct {
name string
descr string
checkLock bool
do func(subCmdCtx) error
}
func (ctx subCmdCtx) usagePrefix() string {
subCmdNamesStr := strings.Join(ctx.subCmdNames, " ")
if subCmdNamesStr != "" {
subCmdNamesStr += " "
}
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
}
func (ctx subCmdCtx) flagSet(withPassthrough bool) *pflag.FlagSet {
flags := pflag.NewFlagSet(ctx.subCmd.name, pflag.ExitOnError)
flags.Usage = func() {
var passthroughStr string
if withPassthrough {
passthroughStr = " [--] [args...]"
}
fmt.Fprintf(
os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n",
ctx.usagePrefix(), ctx.subCmd.name, passthroughStr,
)
fmt.Fprintf(os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(ctx.subCmd.name))
fmt.Fprintln(os.Stderr, flags.FlagUsages())
os.Stderr.Sync()
os.Exit(2)
}
return flags
}
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
printUsageExit := func(subCmdName string) {
fmt.Fprintf(os.Stderr, "unknown sub-command %q\n", subCmdName)
fmt.Fprintf(
os.Stderr,
"%s<subCmd> [-h|--help] [sub-command flags...]\n",
ctx.usagePrefix(),
)
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
for _, subCmd := range subCmds {
fmt.Fprintf(os.Stderr, " %s\t%s\n", subCmd.name, subCmd.descr)
}
fmt.Fprintf(os.Stderr, "\n")
os.Stderr.Sync()
os.Exit(2)
}
args := ctx.args
if len(args) == 0 {
printUsageExit("")
}
subCmdsMap := map[string]subCmd{}
for _, subCmd := range subCmds {
subCmdsMap[subCmd.name] = subCmd
}
subCmdName, args := args[0], args[1:]
subCmd, ok := subCmdsMap[subCmdName]
if !ok {
printUsageExit(subCmdName)
}
if subCmd.checkLock {
err := crypticnet.NewProcLock(ctx.env.RuntimeDirPath).AssertLock()
if err != nil {
return fmt.Errorf("checking lock file: %w", err)
}
}
err := subCmd.do(subCmdCtx{
subCmd: subCmd,
args: args,
subCmdNames: append(ctx.subCmdNames, subCmdName),
env: ctx.env,
})
if err != nil {
return err
}
return nil
}

View File

@ -1,4 +1,4 @@
package main
package entrypoint
import (
"fmt"
@ -7,12 +7,11 @@ import (
)
var subCmdVersion = subCmd{
name: "version",
descr: "Dumps version and build info to stdout",
noNetwork: true,
do: func(ctx subCmdCtx) error {
name: "version",
descr: "Dumps version and build info to stdout",
do: func(subCmdCtx subCmdCtx) error {
versionPath := filepath.Join(envAppDirPath, "share/version")
versionPath := filepath.Join(subCmdCtx.env.AppDirPath, "share/version")
version, err := os.ReadFile(versionPath)

View File

@ -0,0 +1,129 @@
package garage_entrypoint
import (
"fmt"
"log"
"net"
"os"
"path/filepath"
"strconv"
"time"
crypticnet "cryptic-net"
"cryptic-net/garage"
"github.com/cryptic-io/pmux/pmuxlib"
)
func writeChildConf(
env *crypticnet.Env,
alloc crypticnet.DaemonYmlStorageAllocation,
) (string, error) {
if err := os.MkdirAll(alloc.MetaPath, 0750); err != nil {
return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err)
}
thisHost := env.Bootstrap.ThisHost()
peer := garage.Peer{
IP: thisHost.Nebula.IP,
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
}
pubKey, privKey := peer.RPCPeerKey()
nodeKeyPath := filepath.Join(alloc.MetaPath, "node_key")
nodeKeyPubPath := filepath.Join(alloc.MetaPath, "node_keypub")
if err := os.WriteFile(nodeKeyPath, privKey, 0400); err != nil {
return "", fmt.Errorf("writing private key to %q: %w", nodeKeyPath, err)
} else if err := os.WriteFile(nodeKeyPubPath, pubKey, 0440); err != nil {
return "", fmt.Errorf("writing public key to %q: %w", nodeKeyPubPath, err)
}
garageTomlPath := filepath.Join(
env.RuntimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort),
)
err := garage.WriteGarageTomlFile(garageTomlPath, garage.GarageTomlData{
MetaPath: alloc.MetaPath,
DataPath: alloc.DataPath,
RPCSecret: env.Bootstrap.GarageRPCSecret,
RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)),
WebAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.WebPort)),
BootstrapPeers: env.Bootstrap.GarageRPCPeerAddrs(),
})
if err != nil {
return "", fmt.Errorf("creating garage.toml file at %q: %w", garageTomlPath, err)
}
return garageTomlPath, nil
}
func waitForArgs(env *crypticnet.Env, bin string, binArgs ...string) []string {
thisHost := env.Bootstrap.ThisHost()
var args []string
for _, alloc := range env.ThisDaemon().Storage.Allocations {
args = append(
args,
"wait-for",
net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
"--",
)
}
args = append(args, bin)
args = append(args, binArgs...)
return args
}
func Main() {
env, err := crypticnet.ReadEnv()
if err != nil {
log.Fatalf("reading envvars: %v", err)
}
var pmuxProcConfigs []pmuxlib.ProcessConfig
for _, alloc := range env.ThisDaemon().Storage.Allocations {
childConfPath, err := writeChildConf(env, alloc)
if err != nil {
log.Fatalf("writing child config file for alloc %+v: %v", alloc, err)
}
log.Printf("wrote config file %q", childConfPath)
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
Name: fmt.Sprintf("garage-%d", alloc.RPCPort),
Cmd: "garage",
Args: []string{"-c", childConfPath, "server"},
SigKillWait: 1 * time.Minute,
})
}
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
Name: "garage-apply-layout-diff",
Cmd: "bash",
Args: waitForArgs(env, "bash", "garage-apply-layout-diff"),
NoRestartOn: []int{0},
})
pmuxlib.Run(env.Context, pmuxlib.Config{Processes: pmuxProcConfigs})
}

View File

@ -0,0 +1,256 @@
package garage_layout_diff
// This binary accepts the output of `garage layout show` into its stdout, and
// it outputs a newline-delimited set of `garage layout $cmd` strings on
// stdout. The layout commands which are output will, if run, bring the current
// node's layout on the cluster up-to-date with what's in daemon.yml.
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
crypticnet "cryptic-net"
"cryptic-net/garage"
)
type clusterNode struct {
ID string
Zone string
Capacity int
}
type clusterNodes []clusterNode
func (n clusterNodes) get(id string) (clusterNode, bool) {
var ok bool
for _, node := range n {
if len(node.ID) > len(id) {
ok = strings.HasPrefix(node.ID, id)
} else {
ok = strings.HasPrefix(id, node.ID)
}
if ok {
return node, true
}
}
return clusterNode{}, false
}
var currClusterLayoutVersionB = []byte("Current cluster layout version:")
func readCurrNodes(r io.Reader) (clusterNodes, int, error) {
input, err := io.ReadAll(r)
if err != nil {
return nil, 0, fmt.Errorf("reading stdin: %w", err)
}
// NOTE I'm not sure if this check should be turned on or not. It simplifies
// things to turn it off and just say that no one should ever be manually
// messing with the layout, but on the other hand maybe someone might?
//
//if i := bytes.Index(input, []byte("==== STAGED ROLE CHANGES ====")); i >= 0 {
// return nil, 0, errors.New("cluster layout has staged changes already, won't modify")
//}
/* The first section of input will always be something like this:
```
==== CURRENT CLUSTER LAYOUT ====
ID Tags Zone Capacity
AAA ZZZ 1
BBB ZZZ 1
CCC ZZZ 1
Current cluster layout version: N
```
There may be more, depending on if the cluster already has changes staged,
but this will definitely be first. */
i := bytes.Index(input, currClusterLayoutVersionB)
if i < 0 {
return nil, 0, errors.New("no current cluster layout found in input")
}
input, tail := input[:i], input[i:]
var currNodes clusterNodes
for inputBuf := bufio.NewReader(bytes.NewBuffer(input)); ; {
line, err := inputBuf.ReadString('\n')
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, 0, fmt.Errorf("reading input line by line from buffer: %w", err)
}
fields := strings.Fields(line)
if len(fields) < 3 {
continue
}
id := fields[0]
// The ID will always be given ending in this fucked up ellipses
if trimmedID := strings.TrimSuffix(id, "…"); id == trimmedID {
continue
} else {
id = trimmedID
}
zone := fields[1]
capacity, err := strconv.Atoi(fields[2])
if err != nil {
return nil, 0, fmt.Errorf("parsing capacity %q: %w", fields[2], err)
}
currNodes = append(currNodes, clusterNode{
ID: id,
Zone: zone,
Capacity: capacity,
})
}
// parse current cluster version from tail
tail = bytes.TrimPrefix(tail, currClusterLayoutVersionB)
if i := bytes.Index(tail, []byte("\n")); i > 0 {
tail = tail[:i]
}
tail = bytes.TrimSpace(tail)
version, err := strconv.Atoi(string(tail))
if err != nil {
return nil, 0, fmt.Errorf("parsing version string from %q: %w", tail, err)
}
return currNodes, version, nil
}
func readExpNodes(env *crypticnet.Env) clusterNodes {
thisHost := env.Bootstrap.ThisHost()
var expNodes clusterNodes
for _, alloc := range env.ThisDaemon().Storage.Allocations {
peer := garage.Peer{
IP: thisHost.Nebula.IP,
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
}
id := peer.RPCPeerID()
expNodes = append(expNodes, clusterNode{
ID: id,
Zone: env.Bootstrap.HostName,
Capacity: alloc.Capacity / 100,
})
}
return expNodes
}
// NOTE: The id formatting for currNodes and expNodes is different; expNodes has
// fully expanded ids, currNodes are abbreviated.
func diff(currNodes, expNodes clusterNodes) []string {
var lines []string
for _, node := range currNodes {
if _, ok := expNodes.get(node.ID); !ok {
lines = append(
lines,
fmt.Sprintf("garage layout remove %s", node.ID),
)
}
}
for _, expNode := range expNodes {
currNode, ok := currNodes.get(expNode.ID)
currNode.ID = expNode.ID // so that equality checking works
if ok && currNode == expNode {
continue
}
lines = append(
lines,
fmt.Sprintf(
"garage layout assign %s -z %s -c %d",
expNode.ID,
expNode.Zone,
expNode.Capacity,
),
)
}
return lines
}
func Main() {
env, err := crypticnet.ReadEnv()
if err != nil {
panic(fmt.Errorf("reading environment: %w", err))
}
currNodes, currVersion, err := readCurrNodes(os.Stdin)
if err != nil {
panic(fmt.Errorf("reading current layout from stdin: %w", err))
}
thisCurrNodes := make(clusterNodes, 0, len(currNodes))
for _, node := range currNodes {
if env.Bootstrap.HostName != node.Zone {
continue
}
thisCurrNodes = append(thisCurrNodes, node)
}
expNodes := readExpNodes(env)
lines := diff(thisCurrNodes, expNodes)
if len(lines) == 0 {
return
}
for _, line := range lines {
fmt.Println(line)
}
fmt.Printf("garage layout apply --version %d\n", currVersion+1)
}

View File

@ -0,0 +1,137 @@
package garage_layout_diff
import (
"bytes"
"reflect"
"strconv"
"testing"
)
func TestReadCurrNodes(t *testing.T) {
expNodes := clusterNodes{
{
ID: "AAA",
Zone: "XXX",
Capacity: 1,
},
{
ID: "BBB",
Zone: "YYY",
Capacity: 2,
},
{
ID: "CCC",
Zone: "ZZZ",
Capacity: 3,
},
}
expVersion := 666
tests := []struct {
input string
expNodes clusterNodes
expVersion int
}{
{
input: `
==== CURRENT CLUSTER LAYOUT ====
ID Tags Zone Capacity
AAA XXX 1
BBB YYY 2
CCC ZZZ 3
Current cluster layout version: 666
`,
expNodes: expNodes,
expVersion: expVersion,
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
gotNodes, gotVersion, err := readCurrNodes(
bytes.NewBufferString(test.input),
)
if err != nil {
t.Fatal(err)
}
if gotVersion != test.expVersion {
t.Fatalf(
"expected version %d, got %d",
test.expVersion,
gotVersion,
)
}
if !reflect.DeepEqual(gotNodes, test.expNodes) {
t.Fatalf(
"expected nodes: %#v,\ngot nodes: %#v",
gotNodes,
test.expNodes,
)
}
})
}
}
func TestDiff(t *testing.T) {
currNodes := clusterNodes{
{
ID: "1",
Zone: "zone",
Capacity: 1,
},
{
ID: "2",
Zone: "zone",
Capacity: 1,
},
{
ID: "3",
Zone: "zone",
Capacity: 1,
},
{
ID: "4",
Zone: "zone",
Capacity: 1,
},
}
expNodes := clusterNodes{
{
ID: "111",
Zone: "zone",
Capacity: 1,
},
{
ID: "222",
Zone: "zone2",
Capacity: 1,
},
{
ID: "333",
Zone: "zone",
Capacity: 10,
},
}
expLines := []string{
`garage layout remove 4`,
`garage layout assign 222 -z zone2 -c 1`,
`garage layout assign 333 -z zone -c 10`,
}
gotLines := diff(currNodes, expNodes)
if !reflect.DeepEqual(gotLines, expLines) {
t.Fatalf("expected lines: %#v,\ngot lines: %#v", expLines, gotLines)
}
}

View File

@ -0,0 +1,65 @@
package garage_peer_keygen
/*
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! !!
!! DANGER !!
!! !!
!! This script will deterministically produce public/private keys given some !!
!! arbitrary input. This is NEVER what you want. It's only being used in !!
!! cryptic-net for a very specific purpose for which I think it's ok and is !!
!! very necessary, and people are probably _still_ going to yell at me. !!
!! !!
!! DONT USE THIS. !!
!! !!
!! - Brian !!
!! !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*/
import (
"encoding/hex"
"flag"
"fmt"
"io/ioutil"
"os"
"cryptic-net/garage"
)
func Main() {
ip := flag.String("ip", "", "Internal IP address of the node to generate a key for")
port := flag.Int("port", 0, "RPC port number for the garage instance to generate a key for")
outPriv := flag.String("out-priv", "", "The path to the private key which should be created, if given.")
outPub := flag.String("out-pub", "", "The path to the public key which should be created, if given.")
danger := flag.Bool("danger", false, "Set this flag to indicate you understand WHY this binary should NEVER be used (see source code).")
flag.Parse()
if len(*ip) == 0 || *port == 0 || !*danger {
panic("The arguments -ip, -port, and -danger are required")
}
peer := garage.Peer{
IP: *ip,
RPCPort: *port,
}
pubKey, privKey := peer.RPCPeerKey()
fmt.Fprintln(os.Stdout, hex.EncodeToString(pubKey))
if *outPub != "" {
if err := ioutil.WriteFile(*outPub, pubKey, 0444); err != nil {
panic(fmt.Errorf("writing public key to %q: %w", *outPub, err))
}
}
if *outPriv != "" {
if err := ioutil.WriteFile(*outPriv, privKey, 0400); err != nil {
panic(fmt.Errorf("writing private key to %q: %w", *outPriv, err))
}
}
}

View File

@ -0,0 +1,134 @@
package nebula_entrypoint
import (
"cryptic-net/yamlutil"
"log"
"net"
"os"
"path/filepath"
"strconv"
"syscall"
crypticnet "cryptic-net"
)
func Main() {
env, err := crypticnet.ReadEnv()
if err != nil {
log.Fatalf("reading envvars: %v", err)
}
var (
lighthouseHostIPs []string
staticHostMap = map[string][]string{}
)
for _, host := range env.Bootstrap.Hosts {
if host.Nebula.PublicAddr == "" {
continue
}
lighthouseHostIPs = append(lighthouseHostIPs, host.Nebula.IP)
staticHostMap[host.Nebula.IP] = []string{host.Nebula.PublicAddr}
}
config := map[string]interface{}{
"pki": map[string]string{
"ca": env.Bootstrap.NebulaHostCert.CACert,
"cert": env.Bootstrap.NebulaHostCert.HostCert,
"key": env.Bootstrap.NebulaHostCert.HostKey,
},
"static_host_map": staticHostMap,
"punchy": map[string]bool{
"punch": true,
"respond": true,
},
"tun": map[string]interface{}{
"dev": "cryptic-nebula1",
},
}
if err != nil {
log.Fatal(err)
}
if publicAddr := env.ThisDaemon().VPN.PublicAddr; publicAddr == "" {
config["listen"] = map[string]string{
"host": "0.0.0.0",
"port": "0",
}
config["lighthouse"] = map[string]interface{}{
"hosts": lighthouseHostIPs,
}
} else {
_, port, err := net.SplitHostPort(publicAddr)
if err != nil {
log.Fatalf("parsing public address %q: %v", publicAddr, err)
}
config["listen"] = map[string]string{
"host": "0.0.0.0",
"port": port,
}
config["lighthouse"] = map[string]interface{}{
"hosts": []string{},
"am_lighthouse": true,
}
}
thisDaemon := env.ThisDaemon()
var firewallInbound []crypticnet.ConfigFirewallRule
for _, alloc := range thisDaemon.Storage.Allocations {
firewallInbound = append(
firewallInbound,
crypticnet.ConfigFirewallRule{
Port: strconv.Itoa(alloc.S3APIPort),
Proto: "tcp",
Host: "any",
},
crypticnet.ConfigFirewallRule{
Port: strconv.Itoa(alloc.RPCPort),
Proto: "tcp",
Host: "any",
},
crypticnet.ConfigFirewallRule{
Port: strconv.Itoa(alloc.WebPort),
Proto: "tcp",
Host: "any",
},
)
}
firewall := thisDaemon.VPN.Firewall
firewall.Inbound = append(firewallInbound, firewall.Inbound...)
config["firewall"] = firewall
nebulaYmlPath := filepath.Join(env.RuntimeDirPath, "nebula.yml")
if err := yamlutil.WriteYamlFile(config, nebulaYmlPath); err != nil {
log.Fatalf("writing nebula.yml to %q: %v", nebulaYmlPath, err)
}
var (
binPath = env.BinPath("nebula")
args = []string{"nebula", "-config", nebulaYmlPath}
cliEnv = os.Environ()
)
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
log.Fatalf("calling exec(%q, %#v, %#v)", binPath, args, cliEnv)
}
}

View File

@ -0,0 +1,30 @@
package update_global_bucket
import (
crypticnet "cryptic-net"
"cryptic-net/bootstrap"
"log"
)
func Main() {
env, err := crypticnet.ReadEnv()
if err != nil {
log.Fatalf("reading envvars: %v", err)
}
client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
log.Fatalf("creating client for global bucket: %v", err)
}
err = bootstrap.PutGarageBoostrapHost(
env.Context,
client,
env.Bootstrap.ThisHost(),
)
if err != nil {
log.Fatal(err)
}
}

View File

@ -0,0 +1,51 @@
package crypticnet
type ConfigFirewall struct {
Conntrack ConfigConntrack `yaml:"conntrack"`
Outbound []ConfigFirewallRule `yaml:"outbound"`
Inbound []ConfigFirewallRule `yaml:"inbound"`
}
type ConfigConntrack struct {
TCPTimeout string `yaml:"tcp_timeout"`
UDPTimeout string `yaml:"udp_timeout"`
DefaultTimeout string `yaml:"default_timeout"`
MaxConnections int `yaml:"max_connections"`
}
type ConfigFirewallRule struct {
Port string `yaml:"port,omitempty"`
Code string `yaml:"code,omitempty"`
Proto string `yaml:"proto,omitempty"`
Host string `yaml:"host,omitempty"`
Group string `yaml:"group,omitempty"`
Groups []string `yaml:"groups,omitempty"`
CIDR string `yaml:"cidr,omitempty"`
CASha string `yaml:"ca_sha,omitempty"`
CAName string `yaml:"ca_name,omitempty"`
}
// DaemonYmlStorageAllocation describes the structure of each storage allocation
// within the daemon.yml file.
type DaemonYmlStorageAllocation struct {
DataPath string `yaml:"data_path"`
MetaPath string `yaml:"meta_path"`
Capacity int `yaml:"capacity"`
S3APIPort int `yaml:"api_port"` // TODO fix field name here
RPCPort int `yaml:"rpc_port"`
WebPort int `yaml:"web_port"`
}
// DaemonYml describes the structure of the daemon.yml file.
type DaemonYml struct {
DNS struct {
Resolvers []string `yaml:"resolvers"`
} `yaml:"dns"`
VPN struct {
PublicAddr string `yaml:"public_addr"`
Firewall ConfigFirewall `yaml:"firewall"`
} `yaml:"vpn"`
Storage struct {
Allocations []DaemonYmlStorageAllocation
} `yaml:"storage"`
}

3
go-workspace/src/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package globals defines global constants and variables which are valid
// across all cryptic-net processes and sub-processes.
package crypticnet

226
go-workspace/src/env.go Normal file
View File

@ -0,0 +1,226 @@
package crypticnet
import (
"context"
"cryptic-net/bootstrap"
"cryptic-net/yamlutil"
"errors"
"fmt"
"io/fs"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
"github.com/adrg/xdg"
)
// Names of various environment variables which get set by the entrypoint.
const (
DaemonYmlPathEnvVar = "_DAEMON_YML_PATH"
BootstrapPathEnvVar = "_BOOTSTRAP_PATH"
RuntimeDirPathEnvVar = "_RUNTIME_DIR_PATH"
DataDirPathEnvVar = "_DATA_DIR_PATH"
)
// Env contains the values of environment variables, as well as other entities
// which are useful across all processes.
type Env struct {
Context context.Context
AppDirPath string
DaemonYmlPath string
RuntimeDirPath string
DataDirPath string
// If NewEnv is called with bootstrapOptional, and a bootstrap file is not
// found, then these fields will not be set.
BootstrapPath string
Bootstrap bootstrap.Bootstrap
thisDaemon DaemonYml
thisDaemonOnce sync.Once
}
func getAppDirPath() string {
appDirPath := os.Getenv("APPDIR")
if appDirPath == "" {
appDirPath = "."
}
return appDirPath
}
// NewEnv calculates an Env instance based on the APPDIR and XDG envvars.
//
// If bootstrapOptional is true then NewEnv will first check if a bootstrap file
// can be found in the expected places, and if not then it will not populate
// BootstrapFS or any other fields based on it.
func NewEnv(bootstrapOptional bool) (*Env, error) {
runtimeDirPath := filepath.Join(xdg.RuntimeDir, "cryptic-net")
appDirPath := getAppDirPath()
env := &Env{
AppDirPath: appDirPath,
DaemonYmlPath: filepath.Join(runtimeDirPath, "daemon.yml"),
RuntimeDirPath: runtimeDirPath,
DataDirPath: filepath.Join(xdg.DataHome, "cryptic-net"),
}
return env, env.init(bootstrapOptional)
}
// ReadEnv reads an Env from the process's environment variables, rather than
// calculating like NewEnv does.
func ReadEnv() (*Env, error) {
var err error
readEnv := func(key string) string {
if err != nil {
return ""
}
val := os.Getenv(key)
if val == "" {
err = fmt.Errorf("envvar %q not set", key)
}
return val
}
env := &Env{
AppDirPath: getAppDirPath(),
DaemonYmlPath: readEnv(DaemonYmlPathEnvVar),
RuntimeDirPath: readEnv(RuntimeDirPathEnvVar),
DataDirPath: readEnv(DataDirPathEnvVar),
}
if err != nil {
return nil, err
}
return env, env.init(false)
}
// DataDirBootstrapPath returns the path to the bootstrap file within the user's
// data dir. If the file does not exist there it will be found in the AppDirPath
// by ReloadBootstrap.
func (e *Env) DataDirBootstrapPath() string {
return filepath.Join(e.DataDirPath, "bootstrap.tgz")
}
// LoadBootstrap sets BootstrapPath to the given value, and loads BootstrapFS
// and all derived fields based on that.
func (e *Env) LoadBootstrap(path string) error {
var err error
if e.Bootstrap, err = bootstrap.FromFile(path); err != nil {
return fmt.Errorf("parsing bootstrap.tgz at %q: %w", path, err)
}
e.BootstrapPath = path
return nil
}
func (e *Env) initBootstrap(bootstrapOptional bool) error {
exists := func(path string) (bool, error) {
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat'ing %q: %w", path, err)
}
return true, nil
}
// start by checking if a bootstrap can be found in the user's data
// directory. This will only not be the case if daemon has never been
// successfully started.
{
bootstrapPath := e.DataDirBootstrapPath()
if exists, err := exists(bootstrapPath); err != nil {
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
} else if exists {
return e.LoadBootstrap(bootstrapPath)
}
}
// fallback to checking within the AppDir for a bootstrap which has been
// embedded into the binary.
{
bootstrapPath := filepath.Join(e.AppDirPath, "share/bootstrap.tgz")
if exists, err := exists(bootstrapPath); err != nil {
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
} else if !exists && !bootstrapOptional {
return fmt.Errorf("boostrap file not found at %q", bootstrapPath)
} else if exists {
return e.LoadBootstrap(bootstrapPath)
}
}
return nil
}
func (e *Env) init(bootstrapOptional bool) error {
var cancel context.CancelFunc
e.Context, cancel = context.WithCancel(context.Background())
signalCh := make(chan os.Signal, 2)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-signalCh
cancel()
fmt.Fprintf(os.Stderr, "got signal %v, will exit gracefully\n", sig)
sig = <-signalCh
fmt.Fprintf(os.Stderr, "second interrupt signal %v received, force quitting, there may be zombie children left behind, good luck!\n", sig)
os.Stderr.Sync()
os.Exit(1)
}()
if err := e.initBootstrap(bootstrapOptional); err != nil {
return fmt.Errorf("initializing bootstrap data: %w", err)
}
return nil
}
// ToMap returns the Env as a map of key/value strings. If this map is set into
// a process's environment, then that process can read it back using ReadEnv.
func (e *Env) ToMap() map[string]string {
return map[string]string{
DaemonYmlPathEnvVar: e.DaemonYmlPath,
BootstrapPathEnvVar: e.BootstrapPath,
RuntimeDirPathEnvVar: e.RuntimeDirPath,
DataDirPathEnvVar: e.DataDirPath,
}
}
// ThisDaemon returns the DaemonYml (loaded from DaemonYmlPath) for the
// currently running process.
func (e *Env) ThisDaemon() DaemonYml {
e.thisDaemonOnce.Do(func() {
if err := yamlutil.LoadYamlFile(&e.thisDaemon, e.DaemonYmlPath); err != nil {
panic(err)
}
})
return e.thisDaemon
}
// BinPath returns the absolute path to a binary in the AppDir.
func (e *Env) BinPath(name string) string {
return filepath.Join(e.AppDirPath, "bin", name)
}

View File

@ -0,0 +1,34 @@
package garage
import (
"errors"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
// IsKeyNotFound returns true if the given error is the result of a key not
// being found in a bucket.
func IsKeyNotFound(err error) bool {
var mErr minio.ErrorResponse
return errors.As(err, &mErr) && mErr.Code == "NoSuchKey"
}
// S3APIClient is a client used to interact with garage's S3 API.
type S3APIClient = *minio.Client
// S3APICredentials describe data fields necessary for authenticating with a
// garage S3 API endpoint.
type S3APICredentials struct {
ID string `yaml:"id"`
Secret string `yaml:"secret"`
}
// NewS3APIClient returns a minio client configured to use the given garage S3 API
// endpoint.
func NewS3APIClient(addr string, creds S3APICredentials) (S3APIClient, error) {
return minio.New(addr, &minio.Options{
Creds: credentials.NewStaticV4(creds.ID, creds.Secret, ""),
Region: Region,
})
}

View File

@ -0,0 +1,13 @@
// Package garage contains helper functions and types which are useful for
// setting up garage configs, processes, and deployments.
package garage
const (
// Region is the region which garage is configured with.
Region = "garage"
// GlobalBucket is the name of the global garage bucket which is
// accessible to all hosts in the network.
GlobalBucket = "cryptic-net-global"
)

View File

@ -0,0 +1,41 @@
package garage
import "io"
type infiniteReader struct {
b []byte
i int
}
// NewInfiniteReader returns a reader which will produce the given bytes in
// repetition. len(b) must be greater than 0.
func NewInfiniteReader(b []byte) io.Reader {
if len(b) == 0 {
panic("len(b) must be greater than 0")
}
return &infiniteReader{b: b}
}
func (r *infiniteReader) Read(b []byte) (int, error) {
// here, have a puzzle
var n int
for {
n += copy(b[n:], r.b[r.i:])
if r.i > 0 {
n += copy(b[n:], r.b[:r.i])
}
r.i = (r.i + n) % len(r.b)
if n >= len(b) {
return n, nil
}
}
}

View File

@ -0,0 +1,101 @@
package garage
import (
"bytes"
"strconv"
"testing"
)
func TestInfiniteReader(t *testing.T) {
tests := []struct {
in []byte
size int
exp []string
}{
{
in: []byte("a"),
size: 1,
exp: []string{"a"},
},
{
in: []byte("ab"),
size: 1,
exp: []string{"a", "b"},
},
{
in: []byte("ab"),
size: 2,
exp: []string{"ab"},
},
{
in: []byte("ab"),
size: 3,
exp: []string{"aba", "bab"},
},
{
in: []byte("ab"),
size: 4,
exp: []string{"abab"},
},
{
in: []byte("ab"),
size: 5,
exp: []string{"ababa", "babab"},
},
{
in: []byte("abc"),
size: 1,
exp: []string{"a", "b", "c"},
},
{
in: []byte("abc"),
size: 2,
exp: []string{"ab", "ca", "bc"},
},
{
in: []byte("abc"),
size: 3,
exp: []string{"abc"},
},
{
in: []byte("abc"),
size: 4,
exp: []string{"abca", "bcab", "cabc"},
},
{
in: []byte("abc"),
size: 5,
exp: []string{"abcab", "cabca", "bcabc"},
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
r := NewInfiniteReader(test.in)
buf := make([]byte, test.size)
assertRead := func(expBuf []byte) {
n, err := r.Read(buf)
if !bytes.Equal(buf, expBuf) {
t.Fatalf("expected bytes %q, got %q", expBuf, buf)
} else if n != len(buf) {
t.Fatalf("expected n %d, got %d", len(buf), n)
} else if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
for i := 0; i < 3; i++ {
for _, expStr := range test.exp {
assertRead([]byte(expStr))
}
}
})
}
}

View File

@ -0,0 +1,66 @@
package garage
import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"net"
"strconv"
)
// Peer describes all information necessary to connect to a given garage node.
type Peer struct {
IP string
RPCPort int
S3APIPort int
}
// RPCPeerKey deterministically generates a public/private keys which can
// be used as a garage node key.
//
// DANGER: This function will deterministically produce public/private keys
// given some arbitrary input. This is NEVER what you want. It's only being used
// in cryptic-net for a very specific purpose for which I think it's ok and is
// very necessary, and people are probably _still_ going to yell at me.
//
func (p Peer) RPCPeerKey() (pubKey, privKey []byte) {
input := []byte(net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort)))
// Append the length of the input to the input, so that the input "foo"
// doesn't generate the same key as the input "foofoo".
input = strconv.AppendInt(input, int64(len(input)), 10)
pubKey, privKey, err := ed25519.GenerateKey(NewInfiniteReader(input))
if err != nil {
panic(err)
}
return pubKey, privKey
}
// RPCPeerID returns the peer ID of the garage node for use in communicating
// over RPC.
//
// DANGER: See warning on RPCPeerKey.
func (p Peer) RPCPeerID() string {
pubKey, _ := p.RPCPeerKey()
return hex.EncodeToString(pubKey)
}
// RPCAddr returns the address of the peer's RPC port.
func (p Peer) RPCAddr() string {
return net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort))
}
// RPCPeerAddr returns the full peer address (e.g. "id@ip:port") of the garage
// node for use in communicating over RPC.
//
// DANGER: See warning on RPCPeerKey.
func (p Peer) RPCPeerAddr() string {
return fmt.Sprintf("%s@%s", p.RPCPeerID(), p.RPCAddr())
}
// S3APIAddr returns the address of the peer's S3 API port.
func (p Peer) S3APIAddr() string {
return net.JoinHostPort(p.IP, strconv.Itoa(p.S3APIPort))
}

View File

@ -0,0 +1,76 @@
package garage
import (
"fmt"
"io"
"os"
"text/template"
)
// GarageTomlData describes all fields needed for rendering a garage.toml
// file via this package's template.
type GarageTomlData struct {
MetaPath string
DataPath string
RPCSecret string
RPCAddr string
APIAddr string
WebAddr string
BootstrapPeers []string
}
var garageTomlTpl = template.Must(template.New("").Parse(`
metadata_dir = "{{ .MetaPath }}"
data_dir = "{{ .DataPath }}"
replication_mode = "3"
rpc_secret = "{{ .RPCSecret }}"
rpc_bind_addr = "{{ .RPCAddr }}"
rpc_public_addr = "{{ .RPCAddr }}"
bootstrap_peers = [{{- range .BootstrapPeers }}
"{{ . }}",
{{ end -}}]
[s3_api]
api_bind_addr = "{{ .APIAddr }}"
s3_region = "garage"
[s3_web]
bind_addr = "{{ .WebAddr }}"
root_domain = ".example.com"
`))
// RenderGarageToml renders a garage.toml using the given data into the writer.
func RenderGarageToml(into io.Writer, data GarageTomlData) error {
return garageTomlTpl.Execute(into, data)
}
// WriteGarageTomlFile renders a garage.toml using the given data to a new file
// at the given path.
func WriteGarageTomlFile(path string, data GarageTomlData) error {
file, err := os.OpenFile(
path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640,
)
if err != nil {
return fmt.Errorf("creating file: %w", err)
}
defer file.Close()
err = RenderGarageToml(file, data)
if err != nil {
return fmt.Errorf("rendering template to file: %w", err)
}
return nil
}

View File

@ -1,25 +1,24 @@
module isle
module cryptic-net
go 1.22
go 1.17
require (
code.betamike.com/micropelago/pmux v0.0.0-20241029124408-55bc7097f7b8
dev.mediocregopher.com/mediocre-go-lib.git v0.0.0-20241023182613-55984cdf5233
github.com/adrg/xdg v0.4.0
github.com/jxskiss/base62 v1.1.0
github.com/cryptic-io/pmux v0.0.0-20220630194257-a451ee620c83
github.com/imdario/mergo v0.3.12
github.com/minio/minio-go/v7 v7.0.28
github.com/sergi/go-diff v1.3.1
github.com/nlepage/go-tarfs v1.1.0
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/slackhq/nebula v1.6.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
gopkg.in/yaml.v3 v3.0.1
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/uuid v1.1.1 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@ -31,12 +30,12 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v1.2.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/smartystreets/assertions v1.13.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
golang.org/x/sys v0.0.0-20220406155245-289d7a0edf71 // indirect
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect

View File

@ -1,35 +1,34 @@
code.betamike.com/micropelago/pmux v0.0.0-20241029124408-55bc7097f7b8 h1:DTAMr60/y9ZtpMORg50LYGpxyvA9+3UFKVW4mb4CwAE=
code.betamike.com/micropelago/pmux v0.0.0-20241029124408-55bc7097f7b8/go.mod h1:WlEWacLREVfIQl1IlBjKzuDgL56DFRvyl7YiL5gGP4w=
dev.mediocregopher.com/mediocre-go-lib.git v0.0.0-20241023182613-55984cdf5233 h1:Ea4HixNfDNDPh7zMngPpEeDf8gpociSPEROBFBedqIY=
dev.mediocregopher.com/mediocre-go-lib.git v0.0.0-20241023182613-55984cdf5233/go.mod h1:nP+AtQWrc3k5qq5y3ABiBLkOfUPlk/FO9fpTFpF+jgs=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/cryptic-io/pmux v0.0.0-20220630194257-a451ee620c83 h1:0W4j4Rg0hMPyUpuvZuj1u9Gmgah9SSsfAUeNvTxo2BA=
github.com/cryptic-io/pmux v0.0.0-20220630194257-a451ee620c83/go.mod h1:fg/CCfMpcbqO7/iqLjoYskopsVO3JqdD3Oij83+YXSI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4=
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -48,12 +47,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nlepage/go-tarfs v1.1.0 h1:bsACOiZMB/zFjYG/sE01070i9Fl26MnRpw0L6WuyfVs=
github.com/nlepage/go-tarfs v1.1.0/go.mod h1:IhxRcLhLkawBetnwu/JNuoPkq/6cclAllhgEa6SmzS8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/slackhq/nebula v1.6.1 h1:/OCTR3abj0Sbf2nGoLUrdDXImrCv0ZVFpVPP5qa0DsM=
@ -61,44 +62,45 @@ github.com/slackhq/nebula v1.6.1/go.mod h1:UmkqnXe4O53QwToSl/gG7sM4BroQwAB7dd4hU
github.com/smartystreets/assertions v1.13.0 h1:Dx1kYM01xsSqKPno3aqLnrwac2LetPvN23diwyr69Qs=
github.com/smartystreets/assertions v1.13.0/go.mod h1:wDmR7qL282YbGsPy6H/yAsesrxfxaaSlJazyFLYVFx8=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c h1:u6SKchux2yDvFQnDHS3lPnIRmfVJ5Sxy3ao2SIdysLQ=
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=
github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0=
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220406155245-289d7a0edf71 h1:PRD0hj6tTuUnCFD08vkvjkYFbQg/9lV8KIxe1y4/cvU=
golang.org/x/sys v0.0.0-20220406155245-289d7a0edf71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 h1:GLw7MR8AfAG2GmGcmVgObFOHXYypgGjnGno25RDwn3Y=
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,152 @@
// Package nebula contains helper functions and types which are useful for
// setting up nebula configs, processes, and deployments.
package nebula
import (
"crypto/ed25519"
"crypto/rand"
"fmt"
"io"
"net"
"time"
"github.com/slackhq/nebula/cert"
"golang.org/x/crypto/curve25519"
)
// HostCert contains the certificate and private key files which will need to
// be present on a particular host. Each file is PEM encoded.
type HostCert struct {
CACert string
HostKey string
HostCert string
}
// CACert contains the certificate and private files which can be used to create
// HostCerts. Each file is PEM encoded.
type CACert struct {
CACert string
CAKey string
}
// NewHostCert generates a new key/cert for a nebula host using the CA key
// which will be found in the adminFS.
func NewHostCert(
caCert CACert, hostName, hostIP string,
) (
HostCert, error,
) {
// The logic here is largely based on
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCert.CAKey))
if err != nil {
return HostCert{}, fmt.Errorf("unmarshaling ca.key: %w", err)
}
caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCert.CACert))
if err != nil {
return HostCert{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
}
issuer, err := caCrt.Sha256Sum()
if err != nil {
return HostCert{}, fmt.Errorf("getting ca.crt issuer: %w", err)
}
expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second)
ip := net.ParseIP(hostIP)
if ip == nil {
return HostCert{}, fmt.Errorf("invalid host ip %q", hostIP)
}
ipNet := &net.IPNet{
IP: ip,
Mask: ipCIDRMask,
}
var hostPub, hostKey []byte
{
var pubkey, privkey [32]byte
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
return HostCert{}, fmt.Errorf("reading random bytes to form private key: %w", err)
}
curve25519.ScalarBaseMult(&pubkey, &privkey)
hostPub, hostKey = pubkey[:], privkey[:]
}
hostCrt := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: hostName,
Ips: []*net.IPNet{ipNet},
NotBefore: time.Now(),
NotAfter: expireAt,
PublicKey: hostPub,
IsCA: false,
Issuer: issuer,
},
}
if err := hostCrt.CheckRootConstrains(caCrt); err != nil {
return HostCert{}, fmt.Errorf("validating certificate constraints: %w", err)
}
if err := hostCrt.Sign(caKey); err != nil {
return HostCert{}, fmt.Errorf("signing host cert with ca.key: %w", err)
}
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
hostCrtPEM, err := hostCrt.MarshalToPEM()
if err != nil {
return HostCert{}, fmt.Errorf("marshalling host.crt: %w", err)
}
return HostCert{
CACert: caCert.CACert,
HostKey: string(hostKeyPEM),
HostCert: string(hostCrtPEM),
}, nil
}
// NewCACert generates a CACert. The domain should be the network's root domain,
// and is included in the signing certificate's Name field.
func NewCACert(domain string, ipCIDRS []*net.IPNet) (CACert, error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(fmt.Errorf("generating ed25519 key: %w", err))
}
now := time.Now()
expireAt := now.Add(2 * 365 * 24 * time.Hour)
caCrt := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: fmt.Sprintf("%s cryptic-net root cert", domain),
Ips: ipCIDRS,
NotBefore: now,
NotAfter: expireAt,
PublicKey: pubKey,
IsCA: true,
},
}
if err := caCrt.Sign(privKey); err != nil {
return CACert{}, fmt.Errorf("signing caCrt: %w", err)
}
caKeyPEM := cert.MarshalEd25519PrivateKey(privKey)
caCrtPem, err := caCrt.MarshalToPEM()
if err != nil {
return CACert{}, fmt.Errorf("marshaling caCrt: %w", err)
}
return CACert{
CACert: string(caCrtPem),
CAKey: string(caKeyPEM),
}, nil
}

View File

@ -0,0 +1,99 @@
package crypticnet
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/shirou/gopsutil/process"
)
var errDaemonNotRunning = errors.New("no cryptic-net daemon process running")
// ProcLock is used to lock a process.
type ProcLock interface {
// WriteLock creates a new lock, or errors if the lock is alread held.
WriteLock() error
// AssertLock returns an error if the lock already exists.
AssertLock() error
}
type procLock struct {
dir string
}
// NewProcLock returns a ProcLock which will use a file in the given directory
// to lock the process.
func NewProcLock(dir string) ProcLock {
return &procLock{dir: dir}
}
func (pl *procLock) path() string {
return filepath.Join(pl.dir, "lock")
}
func (pl *procLock) WriteLock() error {
lockFilePath := pl.path()
lockFile, err := os.OpenFile(
lockFilePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400,
)
if errors.Is(err, os.ErrExist) {
return fmt.Errorf(
"lock file %q already exists, if the cryptic-net daemon is not already running you can safely delete this file",
lockFilePath,
)
} else if err != nil {
return fmt.Errorf("opening lockfile %q: %w", lockFilePath, err)
}
defer lockFile.Close()
if _, err := fmt.Fprintf(lockFile, "%d\n", os.Getpid()); err != nil {
return fmt.Errorf("writing pid to %q: %w", lockFilePath, err)
}
return nil
}
// checks that the lock file exists and that the process which created it also
// still exists.
func (pl *procLock) AssertLock() error {
lockFilePath := pl.path()
lockFile, err := os.Open(lockFilePath)
if errors.Is(err, fs.ErrNotExist) {
return errDaemonNotRunning
} else if err != nil {
return fmt.Errorf("checking lock file %q: %w", lockFilePath, err)
}
defer lockFile.Close()
var pid int32
if _, err := fmt.Fscan(lockFile, &pid); err != nil {
return fmt.Errorf("scanning pid from lock file %q: %w", lockFilePath, err)
}
procExists, err := process.PidExists(pid)
if err != nil {
return fmt.Errorf("checking if process %d exists: %w", pid, err)
} else if !procExists {
return errDaemonNotRunning
}
return nil
}

View File

@ -0,0 +1,24 @@
// Package tarutil implements utilities which are useful for interacting with
// tar and tgz files.
package tarutil
import (
"compress/gzip"
"fmt"
"io"
"io/fs"
"github.com/nlepage/go-tarfs"
)
// FSFromReader returns a FS instance which will read the contents of a tgz
// file from the given Reader.
func FSFromReader(r io.Reader) (fs.FS, error) {
gf, err := gzip.NewReader(r)
if err != nil {
return nil, fmt.Errorf("un-gziping: %w", err)
}
defer gf.Close()
return tarfs.New(gf)
}

View File

@ -0,0 +1,112 @@
package tarutil
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"path/filepath"
"strings"
)
// TGZWriter is a utility for writing tgz files. If an internal error is
// encountered by any method then all subsequent methods will be no-ops, and
// Close() will return that error (after closing out resources).
type TGZWriter struct {
gzipW *gzip.Writer
tarW *tar.Writer
err error
dirsWritten map[string]bool
}
// NewTGZWriter initializes and returns a new instance of TGZWriter which will
// write all data to the given io.Writer.
func NewTGZWriter(w io.Writer) *TGZWriter {
gzipW := gzip.NewWriter(w)
tarW := tar.NewWriter(gzipW)
return &TGZWriter{
gzipW: gzipW,
tarW: tarW,
dirsWritten: map[string]bool{},
}
}
// Close cleans up all open resources being held by TGZWriter, and returns the
// first internal error which was encountered during its operation (if any).
func (w *TGZWriter) Close() error {
w.tarW.Close()
w.gzipW.Close()
return w.err
}
func (w *TGZWriter) writeDir(path string) {
if w.err != nil {
return
} else if path != "." {
w.writeDir(filepath.Dir(path))
}
if path == "." {
path = "./"
} else {
path = "./" + strings.TrimPrefix(path, "./")
path = path + "/"
}
if w.dirsWritten[path] {
return
}
err := w.tarW.WriteHeader(&tar.Header{
Name: path,
Mode: 0700,
})
if err != nil {
w.err = fmt.Errorf("writing header for directory %q: %w", path, err)
return
}
w.dirsWritten[path] = true
}
// WriteFile writes a file to the tgz archive. The file will automatically be
// rooted to the "." directory, and any sub-directories the file exists in
// should have already been created.
func (w *TGZWriter) WriteFile(path string, size int64, body io.Reader) {
if w.err != nil {
return
}
path = "./" + strings.TrimPrefix(path, "./")
w.writeDir(filepath.Dir(path))
err := w.tarW.WriteHeader(&tar.Header{
Name: path,
Size: size,
Mode: 0400,
})
if err != nil {
w.err = fmt.Errorf("writing header for file %q: %w", path, err)
return
}
if _, err := io.Copy(w.tarW, body); err != nil {
w.err = fmt.Errorf("writing file body of file %q: %w", path, err)
return
}
}
// WriteFileBytes is a shortcut for calling WriteFile with the given byte slice
// being used as the file body.
func (w *TGZWriter) WriteFileBytes(path string, body []byte) {
bodyR := bytes.NewReader(body)
w.WriteFile(path, bodyR.Size(), bodyR)
}

View File

@ -0,0 +1,67 @@
package yamlutil
import (
"fmt"
"io/fs"
"os"
"gopkg.in/yaml.v3"
)
// LoadYamlFile reads the file at the given path and unmarshals it into the
// given pointer.
func LoadYamlFile(into interface{}, path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("opening file: %w", err)
}
defer file.Close()
if err = yaml.NewDecoder(file).Decode(into); err != nil {
return fmt.Errorf("decoding yaml: %w", err)
}
return nil
}
// WriteYamlFile encodes the given data as a yaml document, and writes it to the
// given file path, overwriting any previous data.
func WriteYamlFile(data interface{}, path string) error {
file, err := os.OpenFile(
path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640,
)
if err != nil {
return fmt.Errorf("opening file: %w", err)
}
err = yaml.NewEncoder(file).Encode(data)
file.Close()
if err != nil {
return fmt.Errorf("writing/encoding file: %w", err)
}
return nil
}
// LoadYamlFSFile is like LoadYamlFile, but it will read the file from the given
// fs.FS instance.
func LoadYamlFSFile(into interface{}, f fs.FS, path string) error {
body, err := fs.ReadFile(f, path)
if err != nil {
return fmt.Errorf("reading file from FS: %w", err)
}
if err := yaml.Unmarshal(body, into); err != nil {
return fmt.Errorf("yaml unmarshaling: %w", err)
}
return nil
}

View File

@ -1,4 +0,0 @@
# https://github.com/golangci/golangci-lint/issues/4733
linters-settings:
errcheck:
ignore : ""

View File

@ -1,195 +0,0 @@
// Package bootstrap deals with the parsing and creation of bootstrap.json
// files. It also contains some helpers which rely on bootstrap data.
package bootstrap
import (
"crypto/sha512"
"encoding/json"
"fmt"
"isle/nebula"
"isle/toolkit"
"maps"
"net/netip"
"path/filepath"
"sort"
"strings"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
)
// StateDirPath returns the path within the user's state directory where the
// bootstrap file is stored.
func StateDirPath(dataDirPath string) string {
return filepath.Join(dataDirPath, "bootstrap.json")
}
// CreationParams are general parameters used when creating a new network. These
// are available to all hosts within the network via their bootstrap files.
type CreationParams struct {
ID string
Name string
Domain string
}
// NewCreationParams instantiates and returns a CreationParams.
func NewCreationParams(name, domain string) CreationParams {
return CreationParams{
ID: toolkit.RandStr(32),
Name: name,
Domain: domain,
}
}
// Annotate implements the mctx.Annotator interface.
func (p CreationParams) Annotate(aa mctx.Annotations) {
aa["networkID"] = p.ID
aa["networkName"] = p.Name
aa["networkDomain"] = p.Domain
}
// Matches returns true if the given string matches some aspect of the
// CreationParams.
func (p CreationParams) Matches(str string) bool {
if strings.HasPrefix(p.ID, str) {
return true
}
if strings.EqualFold(p.Name, str) {
return true
}
if strings.EqualFold(p.Domain, str) {
return true
}
return false
}
// Conflicts returns true if either CreationParams has some parameter which
// overlaps with that of the other.
func (p CreationParams) Conflicts(p2 CreationParams) bool {
if p.ID == p2.ID {
return true
}
if strings.EqualFold(p.Name, p2.Name) {
return true
}
if strings.EqualFold(p.Domain, p2.Domain) {
return true
}
return false
}
// Bootstrap contains all information which is needed by a host daemon to join a
// network on boot.
type Bootstrap struct {
NetworkCreationParams CreationParams
CAPublicCredentials nebula.CAPublicCredentials
PrivateCredentials nebula.HostPrivateCredentials
HostAssigned `json:"-"`
SignedHostAssigned nebula.Signed[HostAssigned] // signed by CA
Hosts map[nebula.HostName]Host
}
// New initializes and returns a new Bootstrap file for a new host.
//
// TODO in the resulting bootstrap only include this host and hosts which are
// necessary for connecting to nebula/garage. Remember to immediately re-poll
// garage for the full hosts list during network joining.
func New(
caCreds nebula.CACredentials,
adminCreationParams CreationParams,
existingHosts map[nebula.HostName]Host,
name nebula.HostName,
ip netip.Addr,
) (
Bootstrap, error,
) {
hostPubCreds, hostPrivCreds, err := nebula.NewHostCredentials(
caCreds, name, ip,
)
if err != nil {
return Bootstrap{}, fmt.Errorf("generating host credentials: %w", err)
}
assigned := HostAssigned{
Name: name,
PublicCredentials: hostPubCreds,
}
signedAssigned, err := nebula.Sign(assigned, caCreds.SigningPrivateKey)
if err != nil {
return Bootstrap{}, fmt.Errorf("signing assigned fields: %w", err)
}
existingHosts = maps.Clone(existingHosts)
existingHosts[name] = Host{
HostAssigned: assigned,
}
return Bootstrap{
NetworkCreationParams: adminCreationParams,
CAPublicCredentials: caCreds.Public,
PrivateCredentials: hostPrivCreds,
HostAssigned: assigned,
SignedHostAssigned: signedAssigned,
Hosts: existingHosts,
}, nil
}
// UnmarshalJSON implements the json.Unmarshaler interface. It will
// automatically populate the HostAssigned field by unwrapping the
// SignedHostAssigned field.
func (b *Bootstrap) UnmarshalJSON(data []byte) error {
type inner Bootstrap
err := json.Unmarshal(data, (*inner)(b))
if err != nil {
return err
}
b.HostAssigned, err = b.SignedHostAssigned.Unwrap(
b.CAPublicCredentials.SigningKey,
)
if err != nil {
return fmt.Errorf("unwrapping HostAssigned: %w", err)
}
return nil
}
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
// HostName isn't found in the Hosts map.
func (b Bootstrap) ThisHost() Host {
host, ok := b.Hosts[b.Name]
if !ok {
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.Name))
}
return host
}
// Hash returns a deterministic hash of the given hosts map.
func HostsHash(hostsMap map[nebula.HostName]Host) ([]byte, error) {
hosts := make([]Host, 0, len(hostsMap))
for _, host := range hostsMap {
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool {
return hosts[i].Name < hosts[j].Name
})
h := sha512.New()
if err := json.NewEncoder(h).Encode(hosts); err != nil {
return nil, err
}
return h.Sum(nil), nil
}

View File

@ -1,30 +0,0 @@
package bootstrap
import (
"isle/garage"
)
// GarageNodes returns a Node for each known garage instance in the network.
func (b Bootstrap) GarageNodes() []garage.RemoteNode {
var nodes []garage.RemoteNode
for _, host := range b.Hosts {
nodes = append(nodes, host.GarageNodes()...)
}
return nodes
}
// ChooseGarageNode returns a RemoteNode for a garage instance from the network.
// It will prefer a garage instance on this particular host, if there is one,
// but will otherwise return a random endpoint.
func (b Bootstrap) ChooseGarageNode() garage.RemoteNode {
thisHost := b.ThisHost()
if len(thisHost.Garage.Instances) > 0 {
return thisHost.GarageNodes()[0]
}
for _, node := range b.GarageNodes() {
return node
}
panic("no garage instances configured")
}

View File

@ -1,109 +0,0 @@
package bootstrap
import (
"fmt"
"isle/garage"
"isle/nebula"
"net/netip"
)
// NebulaHost describes the nebula configuration of a Host which is relevant for
// other hosts to know.
type NebulaHost struct {
PublicAddr string
}
// GarageHost describes a single garage instance in the GarageHost.
type GarageHostInstance struct {
ID string
RPCPort int
S3APIPort int
}
// GarageHost describes the garage configuration of a Host which is relevant for
// other hosts to know.
type GarageHost struct {
Instances []GarageHostInstance
}
// HostAssigned are all fields related to a host which were assigned to it by an
// admin.
type HostAssigned struct {
Name nebula.HostName
PublicCredentials nebula.HostPublicCredentials
}
// HostConfigured are all the fields a host can configure for itself.
type HostConfigured struct {
Nebula NebulaHost `json:",omitempty"`
Garage GarageHost `json:",omitempty"`
}
// AuthenticatedHost wraps all the data about a host which other hosts may know
// about it, such that those hosts can authenticate that the data is valid and
// approved by an admin.
type AuthenticatedHost struct {
Assigned nebula.Signed[HostAssigned] // signed by CA
Configured nebula.Signed[HostConfigured] // signed by host
}
// Unwrap attempts to authenticate and unwrap the Host embedded in this
// instance. nebula.ErrInvalidSignature is returned if any signatures are
// invalid.
func (ah AuthenticatedHost) Unwrap(caCreds nebula.CAPublicCredentials) (Host, error) {
assigned, err := ah.Assigned.Unwrap(caCreds.SigningKey)
if err != nil {
return Host{}, fmt.Errorf("unwrapping assigned fields using CA public key: %w", err)
}
configured, err := ah.Configured.Unwrap(assigned.PublicCredentials.SigningKey)
if err != nil {
return Host{}, fmt.Errorf("unwrapping configured fields using host public key: %w", err)
}
return Host{assigned, configured}, nil
}
// Host contains all data bout a host which other hosts may know about it.
//
// A Host should only be obtained over the network as an AuthenticatedHost, and
// subsequently Unwrapped.
type Host struct {
HostAssigned
HostConfigured
}
// IP returns the IP address encoded in the Host's nebula certificate, or panics
// if there is an error.
//
// This assumes that the Host and its data has already been verified against the
// CA signing key.
func (h Host) IP() netip.Addr {
cert := h.PublicCredentials.Cert.Unwrap()
if len(cert.Details.Ips) == 0 {
panic(fmt.Sprintf("host %q not configured with any ips: %+v", h.Name, h))
}
ip := cert.Details.Ips[0].IP
addr, ok := netip.AddrFromSlice(ip)
if !ok {
panic(fmt.Sprintf("ip %q (%#v) is not valid, somehow", ip, ip))
}
return addr
}
// GarageNodes returns a RemoteNode for each garage instance advertised by this
// Host.
func (h Host) GarageNodes() []garage.RemoteNode {
var nodes []garage.RemoteNode
for _, instance := range h.Garage.Instances {
nodes = append(nodes, garage.RemoteNode{
ID: instance.ID,
IP: h.IP().String(),
RPCPort: instance.RPCPort,
S3APIPort: instance.S3APIPort,
})
}
return nodes
}

View File

@ -1,9 +0,0 @@
package main
import (
"isle/bootstrap"
)
func (ctx subCmdCtx) getHosts() ([]bootstrap.Host, error) {
return ctx.getDaemonRPC().GetHosts(ctx)
}

View File

@ -1,95 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"isle/daemon"
"isle/daemon/daecommon"
"isle/daemon/network"
)
// TODO it would be good to have an `isle daemon config-check` kind of command,
// which could be run prior to a systemd service restart to make sure we don't
// restart the service into a configuration that will definitely fail.
var subCmdDaemon = subCmd{
name: "daemon",
descr: "Runs the isle daemon (Default if no sub-command given)",
noNetwork: true,
do: func(ctx subCmdCtx) error {
daemonConfigPath := ctx.flags.StringP(
"config-path", "c", "",
"Optional path to a daemon.yml file to load configuration from.",
)
dumpConfig := ctx.flags.Bool(
"dump-config", false,
"Write the default configuration file to stdout and exit.",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *dumpConfig {
return daecommon.CopyDefaultConfig(os.Stdout)
}
// TODO check that daemon is either running as root, or that the
// required linux capabilities are set.
// TODO check that the tun module is loaded (for nebula).
daemonConfig, err := daecommon.LoadConfig(*daemonConfigPath)
if err != nil {
return fmt.Errorf("loading daemon config: %w", err)
}
networkLoader, err := network.NewLoader(
ctx,
ctx.logger.WithNamespace("loader"),
envBinDirPath,
nil,
)
if err != nil {
return fmt.Errorf("instantiating network loader: %w", err)
}
daemonInst, err := daemon.New(
ctx, ctx.logger, networkLoader, daemonConfig,
)
if err != nil {
return fmt.Errorf("starting daemon: %w", err)
}
defer func() {
ctx.logger.Info(ctx, "Stopping child processes")
if err := daemonInst.Shutdown(); err != nil {
ctx.logger.Error(ctx, "Shutting down daemon cleanly failed, there may be orphaned child processes", err)
}
ctx.logger.Info(ctx, "Child processes successfully stopped")
}()
{
logger := ctx.logger.WithNamespace("http")
httpSrv, err := newHTTPServer(
ctx, logger, daemonInst,
)
if err != nil {
return fmt.Errorf("starting HTTP server: %w", err)
}
defer func() {
// see comment in daemonInst shutdown logic regarding background
// context.
logger.Info(ctx, "Shutting down HTTP socket")
if err := httpSrv.Shutdown(context.Background()); err != nil {
logger.Error(ctx, "Failed to cleanly shutdown http server", err)
}
}()
}
<-ctx.Done()
return nil
},
}

View File

@ -1,65 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"io/fs"
"isle/daemon"
"net"
"net/http"
"os"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
const daemonHTTPRPCPath = "/rpc/v0.json"
func newHTTPServer(
ctx context.Context, logger *mlog.Logger, daemonInst *daemon.Daemon,
) (
*http.Server, error,
) {
socketPath := daemon.HTTPSocketPath()
ctx = mctx.Annotate(ctx, "socketPath", socketPath)
if err := os.Remove(socketPath); errors.Is(err, fs.ErrNotExist) {
// No problem
} else if err != nil {
return nil, fmt.Errorf(
"removing %q prior to listening: %w", socketPath, err,
)
} else {
logger.WarnString(
ctx, "Deleted existing socket file prior to listening, it's possible a previous daemon failed to shutdown gracefully",
)
}
l, err := net.Listen("unix", socketPath)
if err != nil {
return nil, fmt.Errorf(
"listening on socket %q: %w", socketPath, err,
)
}
if err := os.Chmod(socketPath, 0660); err != nil {
return nil, fmt.Errorf(
"setting permissions of %q to 0660: %w", socketPath, err,
)
}
logger.Info(ctx, "HTTP server socket created")
httpMux := http.NewServeMux()
httpMux.Handle(daemonHTTPRPCPath, daemonInst.HTTPRPCHandler())
srv := &http.Server{Handler: httpMux}
go func() {
if err := srv.Serve(l); !errors.Is(err, http.ErrServerClosed) {
logger.Fatal(ctx, "HTTP server unexpectedly shut down", err)
}
}()
return srv, nil
}

View File

@ -1,64 +0,0 @@
package main
import (
"encoding"
"errors"
"fmt"
"isle/nebula"
"isle/toolkit"
"net/netip"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
type textUnmarshaler[T any] interface {
encoding.TextUnmarshaler
*T
}
type textUnmarshalerFlag[T encoding.TextMarshaler, P textUnmarshaler[T]] struct {
V T
}
func (f *textUnmarshalerFlag[T, P]) Set(v string) error {
return P(&(f.V)).UnmarshalText([]byte(v))
}
func (f *textUnmarshalerFlag[T, P]) String() string {
b, err := f.V.MarshalText()
if err != nil {
panic(fmt.Sprintf("calling MarshalText on %#v: %v", f.V, err))
}
return string(b)
}
func (f *textUnmarshalerFlag[T, P]) Type() string { return "string" }
////////////////////////////////////////////////////////////////////////////////
type (
hostNameFlag = textUnmarshalerFlag[nebula.HostName, *nebula.HostName]
ipNetFlag = textUnmarshalerFlag[nebula.IPNet, *nebula.IPNet]
ipFlag = textUnmarshalerFlag[netip.Addr, *netip.Addr]
)
type logLevelFlag struct {
mlog.Level
}
func (f *logLevelFlag) Set(v string) error {
f.Level = toolkit.LogLevelFromString(v)
if f.Level == nil {
return errors.New("not a valid log level")
}
return nil
}
func (f *logLevelFlag) String() string {
if f.Level == nil {
return "UNKNOWN"
}
return f.Level.String()
}
func (f *logLevelFlag) Type() string { return "string" }

View File

@ -1,164 +0,0 @@
package main
import (
"errors"
"fmt"
"isle/daemon/daecommon"
"os"
"path/filepath"
"syscall"
)
// minio-client keeps a configuration directory which contains various pieces of
// information which may or may not be useful. Unfortunately when it initializes
// this directory it likes to print some annoying logs, so we pre-initialize in
// order to prevent it from doing so.
func initMCConfigDir(envVars daecommon.EnvVars) (string, error) {
var (
path = filepath.Join(envVars.StateDir.Path, "mc")
sharePath = filepath.Join(path, "share")
configJSONPath = filepath.Join(path, "config.json")
)
if err := os.MkdirAll(sharePath, 0700); err != nil {
return "", fmt.Errorf("creating %q: %w", sharePath, err)
}
if err := os.WriteFile(configJSONPath, []byte(`{}`), 0600); err != nil {
return "", fmt.Errorf("writing %q: %w", configJSONPath, err)
}
return path, nil
}
var subCmdGarageMC = subCmd{
name: "mc",
descr: "Runs the mc (minio-client) binary. The isle garage can be accessed under the `garage` alias",
passthroughArgs: true,
do: func(ctx subCmdCtx) error {
keyID := ctx.flags.StringP(
"key-id", "i", "",
"Optional key ID to use, defaults to that of the shared global key",
)
keySecret := ctx.flags.StringP(
"key-secret", "s", "",
"Optional key secret to use, defaults to that of the shared global key",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
clientParams, err := ctx.getDaemonRPC().GetGarageClientParams(ctx)
if err != nil {
return fmt.Errorf("calling GetGarageClientParams: %w", err)
}
s3APIAddr := clientParams.Node.S3APIAddr()
if *keyID == "" {
*keyID = clientParams.GlobalBucketS3APICredentials.ID
}
if *keySecret == "" {
*keySecret = clientParams.GlobalBucketS3APICredentials.Secret
}
args := ctx.flags.Args()
if i := ctx.flags.ArgsLenAtDash(); i >= 0 {
args = args[i:]
}
envVars := daecommon.GetEnvVars()
configDir, err := initMCConfigDir(envVars)
if err != nil {
return fmt.Errorf("initializing minio-client config directory: %w", err)
}
args = append([]string{
binPath("mc"),
"--config-dir", configDir,
}, args...)
var (
mcHostVar = fmt.Sprintf(
"MC_HOST_garage=http://%s:%s@%s",
*keyID, *keySecret, s3APIAddr,
)
binPath = binPath("mc")
cliEnv = append(
os.Environ(),
mcHostVar,
// The garage docs say this is necessary, though nothing bad
// seems to happen if we leave it out *shrug*
"MC_REGION=garage",
)
)
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
return fmt.Errorf(
"calling exec(%q, %#v, %#v): %w",
binPath, args, cliEnv, err,
)
}
return nil
},
}
var subCmdGarageCLI = subCmd{
name: "cli",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
passthroughArgs: true,
do: func(ctx subCmdCtx) error {
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
clientParams, err := ctx.getDaemonRPC().GetGarageClientParams(ctx)
if err != nil {
return fmt.Errorf("calling GetGarageClientParams: %w", err)
}
if clientParams.RPCSecret == "" {
return errors.New("this host does not have the garage RPC secret")
}
var (
binPath = binPath("garage")
args = append([]string{"garage"}, ctx.opts.args...)
cliEnv = append(
os.Environ(),
"GARAGE_RPC_HOST="+clientParams.Node.RPCNodeAddr(),
"GARAGE_RPC_SECRET="+clientParams.RPCSecret,
)
)
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
return fmt.Errorf(
"calling exec(%q, %#v, %#v): %w",
binPath, args, cliEnv, err,
)
}
return nil
},
}
var subCmdGarage = subCmd{
name: "garage",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdGarageCLI,
subCmdGarageMC,
)
},
}

View File

@ -1,142 +0,0 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"isle/bootstrap"
"isle/daemon/network"
"os"
"sort"
)
var subCmdHostCreate = subCmd{
name: "create",
descr: "Creates a new host in the network, writing its new bootstrap.json to stdout",
do: func(ctx subCmdCtx) error {
var (
hostName hostNameFlag
ip ipFlag
)
hostNameF := ctx.flags.VarPF(
&hostName,
"hostname", "n",
"Name of the host to generate bootstrap.json for",
)
ctx.flags.VarP(&ip, "ip", "i", "IP of the new host. An available IP will be chosen if none is given.")
canCreateHosts := ctx.flags.Bool(
"can-create-hosts",
false,
"The new host should have the ability to create hosts too",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed {
return errors.New("--hostname is required")
}
res, err := ctx.getDaemonRPC().CreateHost(
ctx, hostName.V, network.CreateHostOpts{
IP: ip.V,
CanCreateHosts: *canCreateHosts,
},
)
if err != nil {
return fmt.Errorf("calling CreateHost: %w", err)
}
return json.NewEncoder(os.Stdout).Encode(res)
},
}
var subCmdHostList = subCmd{
name: "list",
descr: "Lists all hosts in the network, and their IPs",
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
ctx, err := ctx.withParsedFlags()
if err != nil {
return nil, fmt.Errorf("parsing flags: %w", err)
}
hostsRes, err := ctx.getHosts()
if err != nil {
return nil, fmt.Errorf("calling GetHosts: %w", err)
}
type host struct {
Name string
VPN struct {
IP string
}
Storage bootstrap.GarageHost `json:",omitempty"`
}
hosts := make([]host, 0, len(hostsRes))
for _, h := range hostsRes {
host := host{
Name: string(h.Name),
Storage: h.Garage,
}
host.VPN.IP = h.IP().String()
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
return hosts, nil
}),
}
var subCmdHostRemove = subCmd{
name: "remove",
descr: "Removes a host from the network",
do: func(ctx subCmdCtx) error {
var (
hostName hostNameFlag
)
hostNameF := ctx.flags.VarPF(
&hostName,
"hostname", "n",
"Name of the host to remove",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed {
return errors.New("--hostname is required")
}
if err := ctx.getDaemonRPC().RemoveHost(ctx, hostName.V); err != nil {
return fmt.Errorf("calling RemoveHost: %w", err)
}
return nil
},
}
var subCmdHost = subCmd{
name: "host",
plural: "s",
descr: "Sub-commands having to do with configuration of hosts in the network",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdHostCreate,
subCmdHostRemove,
subCmdHostList,
)
},
}

View File

@ -1,83 +0,0 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
func getAppDirPath() string {
appDirPath := os.Getenv("APPDIR")
if appDirPath == "" {
appDirPath = "."
}
return appDirPath
}
var (
envAppDirPath = getAppDirPath()
envBinDirPath = filepath.Join(envAppDirPath, "bin")
)
func binPath(name string) string {
return filepath.Join(envBinDirPath, name)
}
var rootCmd = subCmd{
name: "isle",
descr: "All Isle sub-commands",
noNetwork: true,
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdDaemon,
subCmdGarage,
subCmdHost,
subCmdNebula,
subCmdNetwork,
subCmdStorage,
subCmdVersion,
)
},
}
func doRootCmd(
ctx context.Context,
logger *mlog.Logger,
opts *subCmdCtxOpts,
) error {
subCmdCtx := newSubCmdCtx(ctx, logger, rootCmd, opts)
return subCmdCtx.subCmd.do(subCmdCtx)
}
func main() {
logger := mlog.NewLogger(nil)
defer logger.Close()
ctx, cancel := context.WithCancel(context.Background())
signalCh := make(chan os.Signal, 2)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-signalCh
cancel()
ctx := mctx.Annotate(ctx, "signal", sig.String())
logger.Info(ctx, "got signal, exiting gracefully")
sig = <-signalCh
ctx = mctx.Annotate(ctx, "signal", sig.String())
logger.FatalString(ctx, "second signal received, force quitting, there may be zombie children left behind, good luck!")
}()
if err := doRootCmd(ctx, logger, nil); err != nil {
fmt.Fprintln(os.Stderr, err)
}
}

View File

@ -1,58 +0,0 @@
package main
import (
"bytes"
"context"
"isle/daemon"
"isle/toolkit"
"reflect"
"testing"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
type runHarness struct {
ctx context.Context
logger *mlog.Logger
daemonRPC *daemon.MockRPC
stdout *bytes.Buffer
}
func newRunHarness(t *testing.T) *runHarness {
t.Parallel()
var (
ctx = context.Background()
logger = toolkit.NewTestLogger(t)
daemonRPC = daemon.NewMockRPC(t)
stdout = new(bytes.Buffer)
)
return &runHarness{ctx, logger, daemonRPC, stdout}
}
func (h *runHarness) run(_ *testing.T, args ...string) error {
return doRootCmd(h.ctx, h.logger, &subCmdCtxOpts{
args: args,
daemonRPC: h.daemonRPC,
stdout: h.stdout,
})
}
func (h *runHarness) runAssertStdout(
t *testing.T,
want any,
args ...string,
) {
var (
gotType = reflect.ValueOf(want)
got = reflect.New(gotType.Type())
)
h.stdout.Reset()
assert.NoError(t, h.run(t, args...))
assert.NoError(t, yaml.Unmarshal(h.stdout.Bytes(), got.Interface()))
assert.Equal(t, want, got.Elem().Interface())
}

View File

@ -1,135 +0,0 @@
package main
import (
"errors"
"fmt"
"isle/nebula"
"os"
)
var subCmdNebulaCreateCert = subCmd{
name: "create-cert",
descr: "Creates a signed nebula certificate file for an existing host and writes it to stdout",
do: func(ctx subCmdCtx) error {
var hostName hostNameFlag
hostNameF := ctx.flags.VarPF(
&hostName,
"hostname", "n",
"Name of the host to generate a certificate for",
)
pubKeyPath := ctx.flags.StringP(
"public-key-path", "p", "",
`Path to PEM file containing public key which will be embedded in the cert.`,
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed || *pubKeyPath == "" {
return errors.New("--hostname and --pub-key-path are required")
}
hostPubPEM, err := os.ReadFile(*pubKeyPath)
if err != nil {
return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err)
}
var hostPub nebula.EncryptingPublicKey
if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil {
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
}
res, err := ctx.getDaemonRPC().CreateNebulaCertificate(
ctx, hostName.V, hostPub,
)
if err != nil {
return fmt.Errorf("calling CreateNebulaCertificate: %w", err)
}
nebulaHostCertPEM, err := res.Unwrap().MarshalToPEM()
if err != nil {
return fmt.Errorf("marshaling cert to PEM: %w", err)
}
if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil {
return fmt.Errorf("writing to stdout: %w", err)
}
return nil
},
}
var subCmdNebulaShow = subCmd{
name: "show",
descr: "Writes nebula network information to stdout in JSON format",
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
ctx, err := ctx.withParsedFlags()
if err != nil {
return nil, fmt.Errorf("parsing flags: %w", err)
}
hosts, err := ctx.getHosts()
if err != nil {
return nil, fmt.Errorf("getting hosts: %w", err)
}
caPublicCreds, err := ctx.getDaemonRPC().GetNebulaCAPublicCredentials(ctx)
if err != nil {
return nil, fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err)
}
caCert := caPublicCreds.Cert
caCertDetails := caCert.Unwrap().Details
if len(caCertDetails.Subnets) != 1 {
return nil, fmt.Errorf(
"malformed ca.crt, contains unexpected subnets %#v",
caCertDetails.Subnets,
)
}
subnet := caCertDetails.Subnets[0]
type outLighthouse struct {
PublicAddr string
IP string
}
out := struct {
CACert nebula.Certificate
SubnetCIDR string
Lighthouses []outLighthouse
}{
CACert: caCert,
SubnetCIDR: subnet.String(),
}
for _, h := range hosts {
if h.Nebula.PublicAddr == "" {
continue
}
out.Lighthouses = append(out.Lighthouses, outLighthouse{
PublicAddr: h.Nebula.PublicAddr,
IP: h.IP().String(),
})
}
return out, nil
}),
}
var subCmdNebula = subCmd{
name: "nebula",
descr: "Sub-commands related to the nebula VPN",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdNebulaCreateCert,
subCmdNebulaShow,
)
},
}

View File

@ -1,134 +0,0 @@
package main
import (
"errors"
"fmt"
"isle/daemon/network"
"isle/jsonutil"
)
var subCmdNetworkCreate = subCmd{
name: "create",
descr: "Create's a new network, with this host being the first host in that network.",
noNetwork: true,
do: func(ctx subCmdCtx) error {
var (
ipNet ipNetFlag
hostName hostNameFlag
)
name := ctx.flags.StringP(
"name", "N", "",
"Human-readable name to identify the network as.",
)
domain := ctx.flags.StringP(
"domain", "d", "",
"Domain name that should be used as the root domain in the network.",
)
ipNetF := ctx.flags.VarPF(
&ipNet, "ip-net", "i",
`An IP subnet, in CIDR form, which will be the overall range of`+
` possible IPs in the network. The first IP in this network`+
` range will become this first host's IP.`,
)
hostNameF := ctx.flags.VarPF(
&hostName,
"hostname", "n",
"Name of this host, which will be the first host in the network",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" ||
*domain == "" ||
!ipNetF.Changed ||
!hostNameF.Changed {
return errors.New("--name, --domain, --ip-net, and --hostname are required")
}
err = ctx.getDaemonRPC().CreateNetwork(
ctx, *name, *domain, ipNet.V, hostName.V,
)
if err != nil {
return fmt.Errorf("creating network: %w", err)
}
return nil
},
}
var subCmdNetworkJoin = subCmd{
name: "join",
descr: "Joins this host to an existing network",
noNetwork: true,
do: func(ctx subCmdCtx) error {
bootstrapPath := ctx.flags.StringP(
"bootstrap-path", "b", "", "Path to a bootstrap.json file.",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *bootstrapPath == "" {
return errors.New("--bootstrap-path is required")
}
var newBootstrap network.JoiningBootstrap
if err := jsonutil.LoadFile(&newBootstrap, *bootstrapPath); err != nil {
return fmt.Errorf(
"loading bootstrap from %q: %w", *bootstrapPath, err,
)
}
return ctx.getDaemonRPC().JoinNetwork(ctx, newBootstrap)
},
}
var subCmdNetworkList = subCmd{
name: "list",
descr: "Lists all networks which have been joined",
noNetwork: true,
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
ctx, err := ctx.withParsedFlags()
if err != nil {
return nil, fmt.Errorf("parsing flags: %w", err)
}
return ctx.getDaemonRPC().GetNetworks(ctx)
}),
}
var subCmdNetworkGetConfig = subCmd{
name: "get-config",
descr: "Displays the currently active configuration for a joined network",
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
ctx, err := ctx.withParsedFlags()
if err != nil {
return nil, fmt.Errorf("parsing flags: %w", err)
}
return ctx.getDaemonRPC().GetConfig(ctx)
}),
}
var subCmdNetwork = subCmd{
name: "network",
descr: "Sub-commands related to network membership",
plural: "s",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdNetworkCreate,
subCmdNetworkJoin,
subCmdNetworkList,
subCmdNetworkGetConfig,
)
},
}

View File

@ -1,208 +0,0 @@
package main
import (
"cmp"
"errors"
"fmt"
"isle/daemon/daecommon"
"slices"
"golang.org/x/exp/maps"
)
type storageAllocation struct {
Index int `yaml:"index"`
daecommon.ConfigStorageAllocation `yaml:",inline"`
}
func indexStorageAllocations(
config daecommon.NetworkConfig,
) []storageAllocation {
slices.SortFunc(
config.Storage.Allocations,
func(i, j daecommon.ConfigStorageAllocation) int {
return cmp.Compare(i.RPCPort, j.RPCPort)
},
)
allocs := make([]storageAllocation, len(config.Storage.Allocations))
for i := range config.Storage.Allocations {
allocs[i] = storageAllocation{i, config.Storage.Allocations[i]}
}
return allocs
}
var subCmdStorageAllocationAdd = subCmd{
name: "add-allocation",
descr: "Adds a new storage allocation to the host",
do: func(ctx subCmdCtx) error {
var alloc daecommon.ConfigStorageAllocation
ctx.flags.StringVar(
&alloc.DataPath,
"data-path",
"",
"Path to the directory data should be stored in",
)
ctx.flags.StringVar(
&alloc.MetaPath,
"meta-path",
"",
"Path to the directory metadata should be stored in. This is a"+
" smaller dataset which benefits from a faster drive, if"+
" possible.",
)
ctx.flags.IntVar(
&alloc.Capacity,
"capacity",
0,
"How many gigabytes to allocate.",
)
ctx.flags.IntVar(
&alloc.S3APIPort,
"s3-api-port",
0,
"Which port of the VPN network interface to serve the S3 API on."+
" Will be automatically assigned if not given.",
)
ctx.flags.IntVar(
&alloc.RPCPort,
"rpc-port",
0,
"Which port of the VPN network interface to serve RPC requests on."+
" Will be automatically assigned if not given. Once this port"+
" is defined for an allocation it cannot be changed.",
)
ctx.flags.IntVar(
&alloc.AdminPort,
"admin-port",
0,
"Which port of the VPN network interface to serve admin requests"+
" on. Will be automatically assigned if not given.",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if alloc.DataPath == "" || alloc.MetaPath == "" || alloc.Capacity == 0 {
return errors.New(
"--data-path, --meta-path, and --capacity are required",
)
}
config, err := ctx.getDaemonRPC().GetConfig(ctx)
if err != nil {
return fmt.Errorf("getting network config: %w", err)
}
config.Storage.Allocations = append(config.Storage.Allocations, alloc)
if err := ctx.getDaemonRPC().SetConfig(ctx, config); err != nil {
return fmt.Errorf("updating the network config: %w", err)
}
return nil
},
}
var subCmdStorageAllocationList = subCmd{
name: "list-allocation",
plural: "s",
descr: "Lists all storage which is currently allocated on this host",
do: doWithOutput(func(ctx subCmdCtx) (any, error) {
ctx, err := ctx.withParsedFlags()
if err != nil {
return nil, fmt.Errorf("parsing flags: %w", err)
}
config, err := ctx.getDaemonRPC().GetConfig(ctx)
if err != nil {
return nil, fmt.Errorf("getting network config: %w", err)
}
return indexStorageAllocations(config), nil
}),
}
var subCmdStorageAllocationRemove = subCmd{
name: "remove-allocation",
descr: "Removes an allocation which has been previously added. " +
"Allocations are identified by their index field from the output of " +
"`storage list-allocation(s)`.",
do: func(ctx subCmdCtx) error {
indexes := ctx.flags.IntSlice(
"index", nil,
"Index of the storage allocation which should be removed. Can be "+
"specified more than once",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if len(*indexes) == 0 {
return errors.New("At least one --index must be specified")
}
config, err := ctx.getDaemonRPC().GetConfig(ctx)
if err != nil {
return fmt.Errorf("getting network config: %w", err)
}
var (
allocs = indexStorageAllocations(config)
allocsByIndex = map[int]daecommon.ConfigStorageAllocation{}
)
for _, alloc := range allocs {
allocsByIndex[alloc.Index] = alloc.ConfigStorageAllocation
}
for _, index := range *indexes {
if _, ok := allocsByIndex[index]; !ok {
return fmt.Errorf(
"Index %d not found in configured storage allocations: %w",
index, err,
)
}
delete(allocsByIndex, index)
}
// we sort the new allocation set so that tests are deterministic
newAllocs := maps.Values(allocsByIndex)
slices.SortFunc(
newAllocs,
func(i, j daecommon.ConfigStorageAllocation) int {
return cmp.Compare(i.RPCPort, j.RPCPort)
},
)
config.Storage.Allocations = newAllocs
if err := ctx.getDaemonRPC().SetConfig(ctx, config); err != nil {
return fmt.Errorf("updating the network config: %w", err)
}
return nil
},
}
var subCmdStorage = subCmd{
name: "storage",
descr: "Sub-commands having to do with configuration of storage on this host",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdStorageAllocationAdd,
subCmdStorageAllocationList,
subCmdStorageAllocationRemove,
)
},
}

View File

@ -1,289 +0,0 @@
package main
import (
"context"
"isle/daemon"
"isle/daemon/daecommon"
"isle/toolkit"
"testing"
"github.com/stretchr/testify/assert"
)
func TestStorageAllocationAdd(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args []string
setExpectations func(*daemon.MockRPC)
wantAlloc daecommon.ConfigStorageAllocation
}{
{
name: "success/no ports",
args: []string{
"--data-path", "foo", "--meta-path=bar", "--capacity", "1",
},
wantAlloc: daecommon.ConfigStorageAllocation{
DataPath: "foo",
MetaPath: "bar",
Capacity: 1,
},
},
{
name: "success/all ports",
args: []string{
"--data-path", "foo",
"--meta-path=bar",
"--capacity", "1",
"--s3-api-port", "1000",
"--rpc-port=2000",
"--admin-port", "3000",
},
wantAlloc: daecommon.ConfigStorageAllocation{
DataPath: "foo",
MetaPath: "bar",
Capacity: 1,
S3APIPort: 1000,
RPCPort: 2000,
AdminPort: 3000,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
h = newRunHarness(t)
config = daecommon.NewNetworkConfig(nil)
)
h.daemonRPC.
On("GetConfig", toolkit.MockArg[context.Context]()).
Return(config, nil).
Once()
config.Storage.Allocations = append(
config.Storage.Allocations, test.wantAlloc,
)
h.daemonRPC.
On(
"SetConfig",
toolkit.MockArg[context.Context](),
config,
).
Return(nil).
Once()
args := []string{"storage", "add-allocation"}
args = append(args, test.args...)
assert.NoError(t, h.run(t, args...))
})
}
}
func TestStorageAllocationList(t *testing.T) {
t.Parallel()
tests := []struct {
name string
allocs []daecommon.ConfigStorageAllocation
want any
}{
{
name: "empty",
allocs: nil,
want: []any{},
},
{
// results should get sorted according to RPCPort, with index
// reflecting that order.
name: "success",
allocs: []daecommon.ConfigStorageAllocation{
{
DataPath: "b",
MetaPath: "B",
Capacity: 2,
S3APIPort: 2000,
RPCPort: 2001,
AdminPort: 2002,
},
{
DataPath: "a",
MetaPath: "A",
Capacity: 1,
S3APIPort: 1000,
RPCPort: 1001,
AdminPort: 1002,
},
},
want: []map[string]any{
{
"index": 0,
"data_path": "a",
"meta_path": "A",
"capacity": 1,
"s3_api_port": 1000,
"rpc_port": 1001,
"admin_port": 1002,
},
{
"index": 1,
"data_path": "b",
"meta_path": "B",
"capacity": 2,
"s3_api_port": 2000,
"rpc_port": 2001,
"admin_port": 2002,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var (
h = newRunHarness(t)
config daecommon.NetworkConfig
)
config.Storage.Allocations = test.allocs
h.daemonRPC.
On("GetConfig", toolkit.MockArg[context.Context]()).
Return(config, nil).
Once()
h.runAssertStdout(t, test.want, "storage", "list-allocations")
})
}
}
func TestStorageAllocationRemove(t *testing.T) {
t.Parallel()
config := func(rpcPorts ...int) daecommon.NetworkConfig {
return daecommon.NewNetworkConfig(func(c *daecommon.NetworkConfig) {
for _, rpcPort := range rpcPorts {
c.Storage.Allocations = append(
c.Storage.Allocations,
daecommon.ConfigStorageAllocation{RPCPort: rpcPort},
)
}
})
}
tests := []struct {
name string
args []string
setExpectations func(*daemon.MockRPC)
wantErr string
}{
{
name: "error/no indexes",
args: nil,
wantErr: "At least one --index must be specified",
},
{
name: "error/unknown index",
args: []string{"--index", "1"},
setExpectations: func(daemonRPC *daemon.MockRPC) {
daemonRPC.
On("GetConfig", toolkit.MockArg[context.Context]()).
Return(config(1000), nil).
Once()
},
wantErr: "Index 1 not found",
},
{
name: "success/remove single",
args: []string{"--index", "0"},
setExpectations: func(daemonRPC *daemon.MockRPC) {
config := config(1000, 2000)
daemonRPC.
On("GetConfig", toolkit.MockArg[context.Context]()).
Return(config, nil).
Once()
config.Storage.Allocations = config.Storage.Allocations[1:]
daemonRPC.
On(
"SetConfig",
toolkit.MockArg[context.Context](),
config,
).
Return(nil).
Once()
},
},
{
name: "success/remove multiple",
args: []string{"--index", "0", "--index", "2"},
setExpectations: func(daemonRPC *daemon.MockRPC) {
config := config(1000, 2000, 3000)
daemonRPC.
On("GetConfig", toolkit.MockArg[context.Context]()).
Return(config, nil).
Once()
config.Storage.Allocations = config.Storage.Allocations[1:2]
daemonRPC.
On(
"SetConfig",
toolkit.MockArg[context.Context](),
config,
).
Return(nil).
Once()
},
},
{
name: "success/remove all",
args: []string{"--index", "0", "--index", "1"},
setExpectations: func(daemonRPC *daemon.MockRPC) {
config := config(1000, 2000)
daemonRPC.
On("GetConfig", toolkit.MockArg[context.Context]()).
Return(config, nil).
Once()
config.Storage.Allocations = config.Storage.Allocations[:0]
daemonRPC.
On(
"SetConfig",
toolkit.MockArg[context.Context](),
config,
).
Return(nil).
Once()
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
h := newRunHarness(t)
if test.setExpectations != nil {
test.setExpectations(h.daemonRPC)
}
args := []string{"storage", "remove-allocation"}
args = append(args, test.args...)
err := h.run(t, args...)
if test.wantErr == "" {
assert.NoError(t, err)
} else {
assert.Contains(t, err.Error(), test.wantErr)
}
})
}
}

View File

@ -1,273 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"io"
"isle/daemon"
"isle/daemon/jsonrpc2"
"isle/jsonutil"
"isle/toolkit"
"os"
"strings"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
"github.com/spf13/pflag"
"gopkg.in/yaml.v3"
)
type subCmd struct {
name string
descr string
do func(subCmdCtx) error
// If set then the name will be allowed to be suffixed with this string.
plural string
// noNetwork, if true, means the call doesn't require a network to be
// specified on the command-line if there are more than one networks
// configured.
noNetwork bool
// Extra arguments on the command-line will be passed through to some
// underlying command.
passthroughArgs bool
}
type subCmdCtxOpts struct {
args []string // command-line arguments, excluding the subCmd itself.
subCmdNames []string // names of subCmds so far, including this one
daemonRPC daemon.RPC
stdout io.Writer
}
func (o *subCmdCtxOpts) withDefaults() *subCmdCtxOpts {
if o == nil {
o = new(subCmdCtxOpts)
}
if o.args == nil {
o.args = os.Args[1:]
}
if o.stdout == nil {
o.stdout = os.Stdout
}
return o
}
// subCmdCtx contains all information available to a subCmd's do method.
type subCmdCtx struct {
context.Context
logger *mlog.Logger
subCmd subCmd // the subCmd itself
opts *subCmdCtxOpts
flags *pflag.FlagSet
}
func newSubCmdCtx(
ctx context.Context,
logger *mlog.Logger,
subCmd subCmd,
opts *subCmdCtxOpts,
) subCmdCtx {
opts = opts.withDefaults()
return subCmdCtx{
Context: ctx,
logger: logger,
subCmd: subCmd,
opts: opts,
flags: pflag.NewFlagSet(subCmd.name, pflag.ExitOnError),
}
}
func usagePrefix(subCmdNames []string) string {
subCmdNamesStr := strings.Join(subCmdNames, " ")
if subCmdNamesStr != "" {
subCmdNamesStr += " "
}
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
}
func (ctx subCmdCtx) getDaemonRPC() daemon.RPC {
if ctx.opts.daemonRPC == nil {
// TODO Close is not being called on the HTTPClient
httpClient, baseURL := toolkit.NewUnixHTTPClient(
ctx.logger.WithNamespace("http-client"),
daemon.HTTPSocketPath(),
)
baseURL.Path = daemonHTTPRPCPath
ctx.opts.daemonRPC = daemon.RPCFromClient(
jsonrpc2.NewHTTPClient(httpClient, baseURL.String()),
)
}
return ctx.opts.daemonRPC
}
func (ctx subCmdCtx) withParsedFlags() (subCmdCtx, error) {
logLevel := logLevelFlag{mlog.LevelInfo}
ctx.flags.VarP(
&logLevel,
"log-level", "l",
"Maximum log level to output. Can be DEBUG, CHILD, INFO, WARN, ERROR, or FATAL.",
)
var network string
if !ctx.subCmd.noNetwork {
ctx.flags.StringVar(
&network,
"network", "",
"Which network to perform the command against, if more than one is joined. Can be an ID, name, or domain.",
)
}
ctx.flags.VisitAll(func(f *pflag.Flag) {
if f.Shorthand == "h" {
panic(fmt.Sprintf("flag %+v has reserved shorthand `-h`", f))
}
if f.Name == "help" {
panic(fmt.Sprintf("flag %+v has reserved name `--help`", f))
}
})
ctx.flags.Usage = func() {
var passthroughStr string
if ctx.subCmd.passthroughArgs {
passthroughStr = " [--] [args...]"
}
fmt.Fprintf(
os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n",
usagePrefix(ctx.opts.subCmdNames), ctx.subCmd.name, passthroughStr,
)
fmt.Fprintf(
os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(ctx.subCmd.name),
)
fmt.Fprintln(os.Stderr, ctx.flags.FlagUsages())
os.Stderr.Sync()
os.Exit(2)
}
if err := ctx.flags.Parse(ctx.opts.args); err != nil {
return ctx, err
}
ctx.Context = daemon.WithNetwork(ctx.Context, network)
ctx.logger = ctx.logger.WithMaxLevel(logLevel.Int())
return ctx, nil
}
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
printUsageExit := func(subCmdName string) {
fmt.Fprintf(os.Stderr, "unknown sub-command %q\n", subCmdName)
fmt.Fprintf(
os.Stderr,
"%s<subCmd> [-h|--help] [sub-command flags...]\n",
usagePrefix(ctx.opts.subCmdNames),
)
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
for _, subCmd := range subCmds {
name := subCmd.name
if subCmd.plural != "" {
name += "(" + subCmd.plural + ")"
}
fmt.Fprintf(os.Stderr, " %s\t%s\n", name, subCmd.descr)
}
fmt.Fprintf(os.Stderr, "\n")
os.Stderr.Sync()
os.Exit(2)
}
args := ctx.opts.args
if len(args) == 0 {
printUsageExit("")
}
subCmdsMap := map[string]subCmd{}
for _, subCmd := range subCmds {
subCmdsMap[subCmd.name] = subCmd
if subCmd.plural != "" {
subCmdsMap[subCmd.name+subCmd.plural] = subCmd
}
}
subCmdName, args := args[0], args[1:]
subCmd, ok := subCmdsMap[subCmdName]
if !ok {
printUsageExit(subCmdName)
}
nextSubCmdCtxOpts := *ctx.opts
nextSubCmdCtxOpts.args = args
nextSubCmdCtxOpts.subCmdNames = append(ctx.opts.subCmdNames, subCmdName)
nextSubCmdCtx := newSubCmdCtx(
ctx.Context, ctx.logger, subCmd, &nextSubCmdCtxOpts,
)
if err := subCmd.do(nextSubCmdCtx); err != nil {
return err
}
return nil
}
type outputFormat string
func (f outputFormat) MarshalText() ([]byte, error) { return []byte(f), nil }
func (f *outputFormat) UnmarshalText(b []byte) error {
*f = outputFormat(strings.ToLower(string(b)))
switch *f {
case "json", "yaml":
return nil
default:
return errors.New("invalid output format")
}
}
// doWithOutput wraps a subCmd's do function so that it will output some value
// to stdout. The value will be formatted according to a command-line argument.
func doWithOutput(fn func(subCmdCtx) (any, error)) func(subCmdCtx) error {
return func(ctx subCmdCtx) error {
type outputFormatFlag = textUnmarshalerFlag[outputFormat, *outputFormat]
outputFormat := outputFormatFlag{"yaml"}
ctx.flags.Var(
&outputFormat,
"format",
"How to format the output value. Can be 'json' or 'yaml'.",
)
res, err := fn(ctx)
if err != nil {
return err
}
switch outputFormat.V {
case "json":
return jsonutil.WriteIndented(ctx.opts.stdout, res)
case "yaml":
return yaml.NewEncoder(ctx.opts.stdout).Encode(res)
default:
panic(fmt.Sprintf("unexpected outputFormat %q", outputFormat))
}
}
}

Some files were not shown because too many files have changed in this diff Show More