Compare commits
220 Commits
3f3ad43cb2
...
main
Author | SHA1 | Date | |
---|---|---|---|
070524f686 | |||
6809445832 | |||
095489af2a | |||
6ec56f2a88 | |||
72bca72b29 | |||
f0cb29b553 | |||
bfa0dc457f | |||
2ec28cde61 | |||
5e33eafb80 | |||
335867644b | |||
06d85ca961 | |||
032bdb9e43 | |||
53a06af9ba | |||
734406d4bb | |||
04978fa9db | |||
8e264cf028 | |||
efdab29ae6 | |||
932c8e2244 | |||
6ac473edcb | |||
2cdec586b2 | |||
8dab458291 | |||
7274815cfd | |||
b7c097ef63 | |||
433328524d | |||
88ffa97c0f | |||
5c41cedea3 | |||
63cefd403e | |||
bbae88ab4b | |||
9e9e98584f | |||
f639d460cf | |||
cb6c11acef | |||
7f3cbf628f | |||
f146b77187 | |||
010c53e5c7 | |||
71bc182ab4 | |||
168b65ea1d | |||
48611df2cb | |||
61c5338b84 | |||
31af39ce4c | |||
64fdba0a48 | |||
de7aac1f25 | |||
16aca610b4 | |||
6c036d1183 | |||
df4eae8a5c | |||
c022c97b19 | |||
6d99fb5368 | |||
8c3e6a2845 | |||
86b2ba7bfa | |||
a840d0e701 | |||
ef86c1bbd1 | |||
fed79c6ec7 | |||
8d3b17e1cb | |||
038a28bb02 | |||
06a345ecd1 | |||
6c185f6263 | |||
53ad8a91b4 | |||
5138ed7c6a | |||
4f6a89ced0 | |||
39e12f6ebd | |||
d31be8455b | |||
ca62a37692 | |||
af69f1cfba | |||
1ea16d80e4 | |||
ee30199c4c | |||
1411370b0e | |||
c94f8e3475 | |||
7aa11ebe29 | |||
bc9a2b62ef | |||
e657061482 | |||
3980dc6083 | |||
691727fe99 | |||
5de93e3711 | |||
0f42d9367c | |||
6fff1dfaeb | |||
8dd6768786 | |||
67d17efde0 | |||
d2710db8f1 | |||
9d5c8ea4db | |||
86abdb6ae1 | |||
56f796e3fb | |||
b5059be7fa | |||
cb8fef38c4 | |||
cc121f0752 | |||
778db848c6 | |||
c5e919dc86 | |||
7ca8ff3467 | |||
30c8ca332a | |||
736b23429c | |||
1ee396c976 | |||
279c79a9f1 | |||
f9d033b89f | |||
ce5df164e1 | |||
7d8b274445 | |||
81368821b7 | |||
179059fd3d | |||
05e91cd657 | |||
c808fa81b9 | |||
c3609252a5 | |||
47e53dffb7 | |||
4664ec4a70 | |||
4e5d3b28ab | |||
a8893e4fc6 | |||
aa1a8ea806 | |||
8a1c8d2ed6 | |||
c645a8c767 | |||
65fa208a34 | |||
842c169169 | |||
dee4af012e | |||
68f417b5ba | |||
2768be00d8 | |||
f13a08abfb | |||
b36a38446e | |||
b97ff9b99b | |||
|
249c46c586 | ||
|
3d02be1be0 | ||
|
a7429bd176 | ||
|
745f7786e8 | ||
|
5ee80b1b7d | ||
|
96a3ecfe14 | ||
|
8dcc436aaa | ||
|
ceab16d05f | ||
|
3c3bd8649a | ||
|
98e5f4c98c | ||
|
0a482607d5 | ||
|
73db21f841 | ||
|
56f38ad451 | ||
|
a77617ae96 | ||
|
ae70278a9f | ||
|
0b486d5d27 | ||
|
d2d25d3621 | ||
|
bc798acffa | ||
|
48675ee095 | ||
|
aa0d489e88 | ||
|
2876b56afb | ||
|
e66f67da4a | ||
|
3d6ed8604a | ||
|
661e2b28cb | ||
|
b5e8ad274e | ||
|
633c7147b1 | ||
|
6480f6c843 | ||
|
fafd711b1b | ||
|
9fa32749b9 | ||
|
b7fb1d9c0a | ||
|
3d7651208f | ||
|
2b9601f031 | ||
|
4e3847ea84 | ||
|
257b961459 | ||
|
a1b3ff71b3 | ||
|
57f63750f3 | ||
|
1180540ce3 | ||
|
3a3bd56295 | ||
|
e9190e4dbb | ||
|
94c6ad8774 | ||
|
8e800951a6 | ||
|
b7d49bff5b | ||
|
1354c96ba9 | ||
|
a8856fba99 | ||
|
1379291c1e | ||
|
05f9064d10 | ||
|
5061fb5670 | ||
|
17fb9bbd77 | ||
|
1dc22701cd | ||
|
ca003eaf85 | ||
|
e96fccae1b | ||
|
0a6516b44e | ||
|
03ab15902c | ||
|
53194614df | ||
|
2181da14a1 | ||
|
b498ee271d | ||
|
877e519821 | ||
|
b1fa3be970 | ||
|
739e88d6c9 | ||
|
da100c6170 | ||
|
629a8ec9b2 | ||
|
90a30bef5e | ||
|
838c548706 | ||
|
c4b7abbcc4 | ||
|
97d4aacc15 | ||
|
0d7d69679f | ||
|
4bc0750797 | ||
|
d916d1a630 | ||
|
a153911948 | ||
|
0b094f057e | ||
|
ffd276bd3e | ||
|
e9ac1336ba | ||
|
c0ebca193d | ||
|
bd5a5552bc | ||
|
46685113e0 | ||
|
5c8c24e73e | ||
|
b935457439 | ||
|
6ba8b948c2 | ||
|
be25907444 | ||
|
3ac86e07cf | ||
|
7d95825f97 | ||
|
745fe31324 | ||
|
287313e00a | ||
|
7dceb659ef | ||
|
711d568036 | ||
|
b26f4bdd6a | ||
|
28159608c8 | ||
|
b23a4cafa6 | ||
|
08f47bd514 | ||
|
03618ba72c | ||
|
2200d85992 | ||
|
6ef21ff186 | ||
|
be2250fddd | ||
|
9288d8cf48 | ||
|
5e399209b2 | ||
|
47e45e0071 | ||
|
8ba88b4dfc | ||
|
8d92b9fe2b | ||
|
0d53d0c6d6 | ||
|
936ca8d48f | ||
|
41e0b56617 | ||
|
7a25e1b6e6 | ||
|
eba9b23e61 | ||
|
f720d7accd | ||
|
51e21c3e46 | ||
|
5e08061cd6 | ||
|
18422a1084 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +1,4 @@
|
|||||||
*-bin
|
*-bin
|
||||||
*-admin.tgz*
|
*admin.yml*
|
||||||
*-bootstrap.tgz
|
*bootstrap.yml*
|
||||||
result
|
result
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
export PATH=$APPDIR/bin
|
|
||||||
exec cryptic-net-main entrypoint "$@"
|
|
@ -1,6 +1,6 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Name=Cryptic Net
|
Name=Isle
|
||||||
Name[en]=Cryptic Net
|
Name[en]=Isle
|
||||||
Exec=AppRun
|
Exec=AppRun
|
||||||
|
|
||||||
Icon=cryptic-logo
|
Icon=cryptic-logo
|
@ -1,9 +0,0 @@
|
|||||||
|
|
||||||
ip="$1"
|
|
||||||
shift;
|
|
||||||
|
|
||||||
echo "waiting for $ip to become available..."
|
|
||||||
|
|
||||||
while true; do ping -c1 -W1 "$ip" &> /dev/null && break; done
|
|
||||||
|
|
||||||
exec "$@"
|
|
@ -1,76 +0,0 @@
|
|||||||
|
|
||||||
#
|
|
||||||
# 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
|
|
661
LICENSE.txt
Normal file
661
LICENSE.txt
Normal file
@ -0,0 +1,661 @@
|
|||||||
|
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
123
README.md
@ -4,110 +4,43 @@ rely on it for anything._**
|
|||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
# cryptic-net
|
# Isle
|
||||||
|
|
||||||
The cryptic-net project provides the foundation for an **autonomous community
|
Welcome to Isle's technical documentation. You can find a less technical
|
||||||
cloud infrastructure**.
|
entrypoint to Isle on [the Micropelago website][isle].
|
||||||
|
|
||||||
This project targets communities of individuals, where certain members of the
|
Isle runs on a host as a server daemon, and connects to other isle instances to
|
||||||
community would like to host services and applications from servers running in
|
form a peer-to-peer network. Isle networks are completely self-hosted; no
|
||||||
their homes or offices. These servers can range from simple Raspberry Pis to
|
third-parties are required for a network to function.
|
||||||
full-sized home PCs.
|
|
||||||
|
|
||||||
The core components of cryptic-net, currently, are:
|
Members of a network are able to build upon the capabilities provided by Isle to
|
||||||
|
host services for themselves and others. These capabilities include:
|
||||||
|
|
||||||
* A VPN which enables direct peer-to-peer communication. Even if most hosts in
|
* A VPN which enables direct peer-to-peer communication between network members.
|
||||||
the network are on a private LAN (e.g. their home WiFi network) or have a
|
Even if most hosts in the network are on a private LAN (e.g. their home WiFi
|
||||||
dynamic IP, they can still communicate directly with each other.
|
network) or have a dynamic IP, they can still communicate directly with each
|
||||||
|
other.
|
||||||
|
|
||||||
* An S3-compatible network filesystem. Each participant can provide as much
|
* An S3-compatible network filesystem. Each member can provide as much storage
|
||||||
storage as they care to, if any. Stored data is sharded and replicated across
|
as they care to, if any. Stored data is sharded and replicated across all
|
||||||
all hosts that choose to provide storage.
|
hosts that choose to provide storage.
|
||||||
|
|
||||||
These components are wrapped into a single binary, with all setup being
|
* A DNS server which provides automatic host discovery within the network.
|
||||||
automated. cryptic-net takes "just works" very seriously.
|
|
||||||
|
|
||||||
Participants are able to build upon these foundations to host services for
|
Every isle daemon is able to create or join multiple independent networks. In
|
||||||
themselves and others. They can be assured that their communications are private
|
this case the networks remain siloed from each other, such that members of one
|
||||||
and their storage is reliable, all with zero administrative overhead and zero
|
network are unable to access resources or communicate with members of the other.
|
||||||
third parties involved.
|
|
||||||
|
|
||||||
[nebula]: https://github.com/slackhq/nebula
|
[isle]: https://micropelago.net/isle/
|
||||||
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/
|
|
||||||
|
|
||||||
## Documentation
|
## Getting Started
|
||||||
|
|
||||||
_NOTE: There is currently only a single live cryptic-net which can be joined,
|
The following pages will guide you through setup of Isle, joining an existing
|
||||||
though generalizing the bootstrap process so others can create their own network
|
network, and all other functionality available via the command-line.
|
||||||
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._
|
|
||||||
|
|
||||||
cryptic-net users fall into different roles, depending on their level of
|
* [Installation](./docs/install.md)
|
||||||
involvement and expertise within their particular network. The documentation for
|
* [Command-line Usage](./docs/command-line.md)
|
||||||
cryptic-net is broken down by these categories, so that the reader can easily
|
* [Join a Network](./docs/user/join-a-network.md)
|
||||||
know which documents they need to care about.
|
|
||||||
|
|
||||||
### User Docs
|
Those who want to dive in and contribute to the Isle codebase should check out
|
||||||
|
the [Developer Documentation](./docs/dev/index.md).
|
||||||
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
|
|
||||||
|
208
default.nix
208
default.nix
@ -1,116 +1,170 @@
|
|||||||
{
|
{
|
||||||
|
buildSystem ? builtins.currentSystem,
|
||||||
|
hostSystem ? buildSystem,
|
||||||
|
pkgsNix ? (import ./nix/pkgs.nix),
|
||||||
|
|
||||||
pkgs ? (import ./nix/pkgs.nix).stable,
|
revision ? "dev",
|
||||||
bootstrap ? null,
|
releaseName ? "dev",
|
||||||
|
}: let
|
||||||
|
|
||||||
}: rec {
|
pkgs = pkgsNix.default {
|
||||||
|
inherit buildSystem hostSystem;
|
||||||
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
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
version = pkgs.stdenv.mkDerivation {
|
pkgsNative = pkgsNix.default {
|
||||||
name = "cryptic-net-version";
|
inherit buildSystem;
|
||||||
|
hostSystem = buildSystem;
|
||||||
|
};
|
||||||
|
|
||||||
buildInputs = [ pkgs.git pkgs.go ];
|
garageNix = (import ./nix/garage.nix);
|
||||||
src = ./.;
|
|
||||||
inherit bootstrap;
|
in rec {
|
||||||
|
|
||||||
|
version = pkgs.stdenv.mkDerivation {
|
||||||
|
name = "isle-version";
|
||||||
|
|
||||||
|
inherit buildSystem hostSystem revision releaseName;
|
||||||
|
|
||||||
|
nativeBuildInputs = [ pkgsNative.git ];
|
||||||
|
|
||||||
|
goVersion = pkgs.go.version;
|
||||||
|
garageVersion = garageNix.version;
|
||||||
|
nixpkgsVersion = pkgsNix.version;
|
||||||
|
|
||||||
builder = builtins.toFile "builder.sh" ''
|
builder = builtins.toFile "builder.sh" ''
|
||||||
source $stdenv/setup
|
source $stdenv/setup
|
||||||
|
|
||||||
versionFile=version
|
versionFile=version
|
||||||
|
|
||||||
if [ "$bootstrap" != "" ]; then
|
echo "Release: $releaseName" >> "$versionFile"
|
||||||
hostName=$(tar -xzf "$bootstrap" --to-stdout ./hostname)
|
echo "Platform: $hostSystem" >> "$versionFile"
|
||||||
echo "Built for host: $hostName" >> "$versionFile"
|
echo "Git Revision: $revision" >> "$versionFile"
|
||||||
fi
|
echo "Go Version: $goVersion" >> "$versionFile"
|
||||||
|
echo "Garage Version: $garageVersion" >> "$versionFile"
|
||||||
echo "Build date: $(date)" >> "$versionFile"
|
echo "NixPkgs Version: $nixpkgsVersion" >> "$versionFile"
|
||||||
echo "Git status: $(cd "$src" && git describe --always --long --dirty=' (dirty)')" >> "$versionFile"
|
echo "Build Platform: $buildSystem" >> "$versionFile"
|
||||||
echo "Go version: $(go version)" >> "$versionFile"
|
|
||||||
echo "Build host info: $(uname -srvm)" >> "$versionFile"
|
|
||||||
|
|
||||||
mkdir -p "$out"/share
|
mkdir -p "$out"/share
|
||||||
cp "$versionFile" "$out"/share
|
cp "$versionFile" "$out"/share
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
goWorkspace = pkgs.callPackage ./go-workspace {};
|
goBinaries = pkgs.buildGoModule {
|
||||||
|
pname = "isle-go-binaries";
|
||||||
|
version = "unstable";
|
||||||
|
|
||||||
dnsmasq = (pkgs.callPackage ./dnsmasq {
|
# If this seems pointless, that's because it is! buildGoModule doesn't like
|
||||||
glibcStatic = pkgs.glibc.static;
|
# it if the src derivation's name ends in "-go". So this mkDerivation here
|
||||||
}).env;
|
# 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"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
garage = (pkgs.callPackage ./garage {}).env;
|
vendorHash = "sha256-CYnZNk1wTw/88L6SxNJTUojWartbGdL44c4GKFc8s2k=";
|
||||||
|
|
||||||
waitFor = pkgs.callPackage ./nix/wait-for.nix {};
|
subPackages = [
|
||||||
|
"./cmd/entrypoint"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
appDir = pkgs.buildEnv {
|
dnsmasq = (pkgs.callPackage ./nix/dnsmasq.nix {
|
||||||
name = "cryptic-net-AppDir";
|
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";
|
||||||
paths = [
|
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
|
./AppDir
|
||||||
version
|
version
|
||||||
dnsmasq
|
dnsmasq
|
||||||
|
nebula
|
||||||
garage
|
garage
|
||||||
waitFor
|
pkgs.minio-client
|
||||||
goWorkspace.crypticNetMain
|
];
|
||||||
|
|
||||||
] ++ (if bootstrap != null then [ rootedBootstrap ] else []);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
appimagetool = pkgs.callPackage ./nix/appimagetool.nix {};
|
appDir = pkgs.stdenv.mkDerivation {
|
||||||
|
name = "isle-AppDir";
|
||||||
|
|
||||||
appImage = pkgs.stdenv.mkDerivation {
|
src = appDirBase;
|
||||||
name = "cryptic-net-AppImage";
|
inherit goBinaries;
|
||||||
src = appDir;
|
|
||||||
|
|
||||||
buildInputs = [ appimagetool ];
|
|
||||||
|
|
||||||
ARCH = "x86_64";
|
|
||||||
|
|
||||||
builder = builtins.toFile "build.sh" ''
|
builder = builtins.toFile "build.sh" ''
|
||||||
source $stdenv/setup
|
source $stdenv/setup
|
||||||
cp -rL "$src" cryptic-net
|
cp -rL "$src" "$out"
|
||||||
chmod +w cryptic-net -R
|
chmod +w "$out" -R
|
||||||
mkdir $out
|
|
||||||
appimagetool cryptic-net "$out/cryptic-net"
|
cd "$out"
|
||||||
|
cp $goBinaries/bin/entrypoint ./AppRun
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
service = pkgs.writeText "cryptic-service" ''
|
devShell = pkgs.mkShell {
|
||||||
[Unit]
|
buildInputs = [
|
||||||
Description=cryptic nebula
|
pkgs.go
|
||||||
Requires=network.target
|
pkgs.golangci-lint
|
||||||
After=network.target
|
pkgs.gopls
|
||||||
|
(pkgs.callPackage ./nix/gowrap.nix {})
|
||||||
|
pkgs.go-mockery
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
true # placeholder
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
[Service]
|
testShell = pkgs.mkShell {
|
||||||
Restart=always
|
APPDIR = appDirBase;
|
||||||
RestartSec=1s
|
buildInputs = [
|
||||||
User=root
|
pkgs.go
|
||||||
ExecStart=${appImage}/cryptic-net
|
];
|
||||||
|
};
|
||||||
|
|
||||||
[Install]
|
build = rec {
|
||||||
WantedBy=multi-user.target
|
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;
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
86
dist/linux/arch/default.nix
vendored
Normal file
86
dist/linux/arch/default.nix
vendored
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
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.
|
||||||
|
}
|
16
dist/linux/isle.service
vendored
Normal file
16
dist/linux/isle.service
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[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
|
@ -1,34 +0,0 @@
|
|||||||
# 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')
|
|
||||||
|
|
||||||
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.cryptic.io/$hostIP" >> "$conf_path"
|
|
||||||
|
|
||||||
done
|
|
||||||
)
|
|
||||||
|
|
||||||
cat "$_DAEMON_YML_PATH" | \
|
|
||||||
yq '.dns.resolvers | .[] | "server=" + .' \
|
|
||||||
>> "$conf_path"
|
|
||||||
|
|
||||||
exec bin/dnsmasq -d -C "$conf_path"
|
|
@ -1,39 +0,0 @@
|
|||||||
{
|
|
||||||
|
|
||||||
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
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
# 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
|
|
||||||
#
|
|
||||||
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
|
||||||
|
|
@ -4,20 +4,6 @@ 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
|
in mind that the steps described here must be done for _each_ host the user
|
||||||
wishes to add.
|
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
|
## Step 1: Choose Hostname
|
||||||
|
|
||||||
The user will need to provide you with a name for their host. The name should
|
The user will need to provide you with a name for their host. The name should
|
||||||
@ -29,66 +15,20 @@ conform to the following rules:
|
|||||||
|
|
||||||
* It should end with a letter or number.
|
* It should end with a letter or number.
|
||||||
|
|
||||||
## Step 2: Add Host to Network
|
## Step 2: Create a `bootstrap.json` File
|
||||||
|
|
||||||
The admin should choose an IP for the host. The IP you choose for the new host
|
To create a `bootstrap.json` file for the new host, the admin should perform the
|
||||||
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:
|
following command from their own host:
|
||||||
|
|
||||||
```
|
```
|
||||||
cryptic-net hosts make-bootstrap \
|
isle hosts create --hostname <name> >bootstrap.json
|
||||||
--name <name> \
|
|
||||||
--admin-path <path to admin.tgz> \
|
|
||||||
> bootstrap.tgz
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting `bootstrap.tgz` file should be treated as a secret file that is
|
The resulting `bootstrap.json` file should be treated as a secret file and
|
||||||
shared only with the user it was generated for. The `bootstrap.tgz` file should
|
shared only with the user it was generated for. The `bootstrap.json` file should
|
||||||
not be re-used between hosts either.
|
not be re-used between hosts.
|
||||||
|
|
||||||
If the user already has access to a `cryptic-net` binary then the new
|
The user can now proceed with calling `isle network join`, as described in the
|
||||||
`bootstrap.tgz` file can be given to them as-is, and they can proceed with
|
[Getting Started][getting-started] document.
|
||||||
running their host's `cryptic-net daemon`.
|
|
||||||
|
|
||||||
### Encrypted `admin.tgz`
|
[getting-started]: ../user/getting-started.md
|
||||||
|
|
||||||
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.
|
|
||||||
|
101
docs/admin/creating-a-new-network.md
Normal file
101
docs/admin/creating-a-new-network.md
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# 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/
|
12
docs/admin/removing-a-host-from-the-network.md
Normal file
12
docs/admin/removing-a-host-from-the-network.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# 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.
|
54
docs/command-line.md
Normal file
54
docs/command-line.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# 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)
|
25
docs/dev/architecture.md
Normal file
25
docs/dev/architecture.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# 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
|
49
docs/dev/architecture.plantuml
Normal file
49
docs/dev/architecture.plantuml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
@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
|
1
docs/dev/architecture.svg
Normal file
1
docs/dev/architecture.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
47
docs/dev/building.md
Normal file
47
docs/dev/building.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# 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.
|
@ -1,68 +0,0 @@
|
|||||||
@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
Before Width: | Height: | Size: 18 KiB |
@ -1,21 +1,26 @@
|
|||||||
# Design Principles
|
# Design Principles
|
||||||
|
|
||||||
The following points form the basis for all design decisions made within the
|
The following points form the basis for all design decisions made within the
|
||||||
cryptic-net project.
|
Isle project.
|
||||||
|
|
||||||
* The UX is aggressively optimized to eliminate manual intervention by users.
|
* "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.
|
||||||
All other concerns are secondary. The concept of "UX" extends beyond GUI
|
All other concerns are secondary. The concept of "UX" extends beyond GUI
|
||||||
interfaces, and encompasses all interactions of any sort with a cryptic-net
|
interfaces, and encompasses all interactions of any sort with an isle
|
||||||
process.
|
process.
|
||||||
|
|
||||||
* All resources within a cryptic-net are expected to be hosted on hardware owned
|
* All resources within an isle network are expected to be hosted on hardware
|
||||||
by community members, for example home media servers or gaming rigs. Thus, a
|
owned by community members, for example home media servers or gaming rigs.
|
||||||
cryptic-net is fully autonomous.
|
Thus, an isle network is fully autonomous.
|
||||||
|
|
||||||
* Hardware resources are expected to be heterogenous and geographically
|
* Hardware resources are expected to be heterogenous and geographically
|
||||||
dispersed.
|
dispersed.
|
||||||
|
|
||||||
* It is expected that a single host might be a part of multiple, independent
|
* It is expected that a single host might be a part of multiple, independent
|
||||||
cryptic-net networks. These should not conflict with each other, nor share
|
isle networks. These should not conflict with each other, nor share resources.
|
||||||
resources.
|
|
||||||
|
|
||||||
|
25
docs/dev/glossary.md
Normal file
25
docs/dev/glossary.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# 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.
|
16
docs/dev/index.md
Normal file
16
docs/dev/index.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# 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)
|
@ -1,6 +1,6 @@
|
|||||||
# Rebuilding Documentation
|
# Rebuilding Documentation
|
||||||
|
|
||||||
Most documentation for cryptic-net takes the form of markdown (`.md`) files,
|
Most documentation for Isle takes the form of markdown (`.md`) files,
|
||||||
which do not require any build step. There are a few other kinds of files, such
|
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
|
as `.plantuml` files, which do require a build step. If these are changed then
|
||||||
their artifacts should be rebuilt by doing:
|
their artifacts should be rebuilt by doing:
|
||||||
|
32
docs/dev/releases.md
Normal file
32
docs/dev/releases.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# 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.
|
39
docs/dev/testing.md
Normal file
39
docs/dev/testing.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# 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
|
86
docs/install.md
Normal file
86
docs/install.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# 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.)
|
@ -1,7 +1,7 @@
|
|||||||
# Contributing a Lighthouse
|
# Contributing a Lighthouse
|
||||||
|
|
||||||
The [nebula][nebula] project provides the VPN component which is used by
|
The [nebula][nebula] project provides the VPN component which is used by
|
||||||
cryptic-net. Every nebula network requires at least one (but preferably more)
|
Isle. Every nebula network requires at least one (but preferably more)
|
||||||
publicly accessible hosts. These hosts are called lighthouses.
|
publicly accessible hosts. These hosts are called lighthouses.
|
||||||
|
|
||||||
Lighthouses do _not_ route traffic between hosts on the VPN. Rather, they
|
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.
|
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
|
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 for cryptic-net.
|
[dDNS][ddns] set up, then it can contribute a lighthouse.
|
||||||
|
|
||||||
[nebula]: https://github.com/slackhq/nebula
|
[nebula]: https://github.com/slackhq/nebula
|
||||||
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
|
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
|
||||||
@ -26,21 +26,14 @@ traffic on that port to your host.
|
|||||||
|
|
||||||
Configure your host's firewall to allow all UDP traffic on that port.
|
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
|
## Edit daemon.yml
|
||||||
|
|
||||||
Open your `daemon.yml` file in a text editor, and find the `vpn.public_addr`
|
Open your `/etc/isle/daemon.yml` file in a text editor, and find the
|
||||||
field. Update that field to reflect your host's IP/DNS name and your chosen UDP
|
`vpn.public_addr` field. Update that field to reflect your host's IP/DNS name
|
||||||
port.
|
and your chosen UDP port.
|
||||||
|
|
||||||
## Restart the Daemon
|
## Restart the Daemon
|
||||||
|
|
||||||
With the `daemon.yml` configured, you should restart your `cryptic-net daemon`
|
With the `daemon.yml` configured, you should restart your `isle daemon`
|
||||||
process. On startup the daemon will add its public address to the global
|
process. On startup the daemon will add its public address to the global
|
||||||
configuration, which other hosts will pick up on and begin using.
|
configuration, which other hosts will pick up on and begin using.
|
||||||
|
@ -1,19 +1,12 @@
|
|||||||
# Contributing Storage
|
# Contributing Storage
|
||||||
|
|
||||||
If your host machine can be reasonably sure of being online most, if not all, of
|
This document is for you if your host machine can be reliably be online at all
|
||||||
the time, and has 100GB or more of unused drive space you'd like to contribute
|
times and has 1GB or more of unused drive space you'd like to contribute to the
|
||||||
to the network, then this document is for you.
|
network.
|
||||||
|
|
||||||
## Create daemon.yml
|
## Edit `daemon.yml`
|
||||||
|
|
||||||
First, if you haven't already, [create a `daemon.yml`
|
Open your `/etc/isle/daemon.yml` file in a text editor, and find the
|
||||||
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.
|
`storage.allocations` section.
|
||||||
|
|
||||||
Each allocation in the allocations list describes the space being contributed
|
Each allocation in the allocations list describes the space being contributed
|
||||||
@ -23,7 +16,7 @@ one allocation listed.
|
|||||||
The comments in the file should be self-explanatory, but ask your admin if you
|
The comments in the file should be self-explanatory, but ask your admin if you
|
||||||
need any clarification.
|
need any clarification.
|
||||||
|
|
||||||
Here are an example set of allocations for a host which is contributing space
|
Here is an example set of allocations for a host which is contributing space
|
||||||
from two separate drives:
|
from two separate drives:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -31,37 +24,44 @@ storage:
|
|||||||
allocations:
|
allocations:
|
||||||
|
|
||||||
# 1.2 TB are being shared from drive1
|
# 1.2 TB are being shared from drive1
|
||||||
- data_path: /mnt/drive1/cryptic-net/data
|
- data_path: /mnt/drive1/isle/data
|
||||||
meta_path: /mnt/drive1/cryptic-net/meta
|
meta_path: /mnt/drive1/isle/meta
|
||||||
capacity: 1200
|
capacity: 1200
|
||||||
api_port: 3900
|
|
||||||
rpc_port: 3901
|
|
||||||
web_port: 3902
|
|
||||||
|
|
||||||
# 100 GB (the minimum) are being shared from drive2
|
# 100 GB are being shared from drive2
|
||||||
- data_path: /mnt/drive2/cryptic-net/data
|
- data_path: /mnt/drive2/isle/data
|
||||||
meta_path: /mnt/drive2/cryptic-net/meta
|
meta_path: /mnt/drive2/isle/meta
|
||||||
capacity: 100
|
capacity: 100
|
||||||
api_port: 3910
|
|
||||||
rpc_port: 3911
|
|
||||||
web_port: 3912
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Setup Firewall
|
## Set Up Your Firewall
|
||||||
|
|
||||||
You will need to configure your hosts's firewall to allow traffic from
|
See the doc on [Firewalls](./firewalls.md), to be sure that your host's firewall
|
||||||
cryptic-net IPs on the ports you specified in your allocations.
|
is properly set up for providing storage.
|
||||||
|
|
||||||
## Restart the Daemon
|
## Restart the Daemon
|
||||||
|
|
||||||
With the `daemon.yml` configured, you should restart your `cryptic-net daemon`
|
With the `daemon.yml` configured, you should restart your `isle daemon`
|
||||||
process.
|
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
|
## Further Reading
|
||||||
|
|
||||||
cryptic-net uses the [garage][garage] project for its storage system. See the
|
Isle uses the [garage][garage] project for its storage system. See the
|
||||||
[Managing Garage](managing-garage.md) document for more
|
[Managing Garage](managing-garage.md) document for more
|
||||||
information on how to interact directly with the garage instance being run by
|
information on how to interact directly with the garage instance being run by
|
||||||
cryptic-net.
|
isle.
|
||||||
|
|
||||||
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/
|
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/
|
||||||
|
48
docs/operator/firewalls.md
Normal file
48
docs/operator/firewalls.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# 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.
|
@ -1,11 +1,11 @@
|
|||||||
# Managing Garage
|
# Managing Garage
|
||||||
|
|
||||||
The garage project provides the network storage component for
|
The garage project provides the network storage component for
|
||||||
cryptic-net. If you're reading this document then you would likely benefit
|
Isle. If you're reading this document then you would likely benefit
|
||||||
greatly from reading the [garage documentation][garage] on their website. It's
|
greatly from reading the [garage documentation][garage] on their website. It's
|
||||||
extremely well written and concise.
|
extremely well written and concise.
|
||||||
|
|
||||||
Note that the `cryptic-net daemon` process will handle all setup steps described
|
Note that the `isle daemon` process will handle all setup steps described
|
||||||
in that documentation, but it's still good to have an understanding of how
|
in that documentation, but it's still good to have an understanding of how
|
||||||
garage works and what it can do.
|
garage works and what it can do.
|
||||||
|
|
||||||
@ -13,12 +13,12 @@ garage works and what it can do.
|
|||||||
|
|
||||||
## Garage Runtime Note
|
## Garage Runtime Note
|
||||||
|
|
||||||
There is an important thing to note regarding how cryptic-net runs garage. As
|
There is an important thing to note regarding how isle runs garage. As
|
||||||
described in the [Contributing Storage](contributing-storage.md) document, a
|
described in the [Contributing Storage](contributing-storage.md) document, a
|
||||||
single `cryptic-net daemon` process can be configured to provide any number of
|
single `isle daemon` process can be configured to provide any number of
|
||||||
storage allocations.
|
storage allocations.
|
||||||
|
|
||||||
For each allocation which is configured, `cryptic-net daemon` will configure and
|
For each allocation which is configured, `isle daemon` will configure and
|
||||||
run a separate `garage server` instance as a sub-process. Each garage will use
|
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
|
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
|
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
|
## Garage CLI
|
||||||
|
|
||||||
Every `cryptic-net` binary contains a full `garage` binary embedded into it.
|
Every `isle` binary contains a full `garage` binary embedded into it.
|
||||||
This binary can be accessed directly like so:
|
This binary can be accessed directly like so:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo cryptic-net garage cli <subcmd> <args>
|
sudo isle garage cli <subcmd> <args>
|
||||||
```
|
```
|
||||||
|
|
||||||
Before handing off execution to the `garage` binary, the `cryptic-net` process
|
Before handing off execution to the `garage` binary, the `isle` process
|
||||||
will automatically set up the RPC host and secret environment variables.
|
will automatically set up the RPC host and secret environment variables.
|
||||||
|
|
||||||
If the host which is running the command has more than one allocation
|
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:
|
To display the current layout of the garage cluster:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo cryptic-net garage cli layout show
|
sudo isle garage cli layout show
|
||||||
```
|
```
|
||||||
|
|
||||||
**(DO NOT CHANGE THE CLUSTER LAYOUT UNLESS YOU KNOW WHAT YOU'RE DOING!)**
|
**(DO NOT CHANGE THE CLUSTER LAYOUT UNLESS YOU KNOW WHAT YOU'RE DOING!)**
|
||||||
@ -55,11 +55,11 @@ sudo cryptic-net garage cli layout show
|
|||||||
To create a new bucket:
|
To create a new bucket:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo cryptic-net garage cli bucket create new-bucket
|
sudo isle garage cli bucket create new-bucket
|
||||||
```
|
```
|
||||||
|
|
||||||
To list existing buckets:
|
To list existing buckets:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo cryptic-net garage cli bucket list
|
sudo isle garage cli bucket list
|
||||||
```
|
```
|
||||||
|
100
docs/roadmap.md
100
docs/roadmap.md
@ -1,100 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,14 +1,20 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
pkgs ? (import ../nix/pkgs.nix).stable,
|
buildSystem ? builtins.currentSystem,
|
||||||
|
hostSystem ? buildSystem,
|
||||||
|
pkgsNix ? (import ../nix/pkgs.nix),
|
||||||
|
|
||||||
}: pkgs.mkShell {
|
}: let
|
||||||
name = "cryptic-net-build-docs";
|
pkgs = pkgsNix.default {
|
||||||
buildInputs = [ pkgs.plantuml ];
|
inherit buildSystem hostSystem;
|
||||||
|
};
|
||||||
shellHook = ''
|
in
|
||||||
set -e
|
pkgs.mkShell {
|
||||||
plantuml -tsvg ./dev/*.plantuml
|
name = "isle-build-docs";
|
||||||
exit 0
|
buildInputs = [ pkgs.plantuml ];
|
||||||
'';
|
shellHook = ''
|
||||||
}
|
set -e
|
||||||
|
plantuml -tsvg ./dev/*.plantuml
|
||||||
|
exit 0
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
# 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.
|
|
||||||
|
|
@ -1,117 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
20
docs/user/join-a-network.md
Normal file
20
docs/user/join-a-network.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# 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
|
@ -1,36 +1,35 @@
|
|||||||
# Using DNS
|
# Using DNS
|
||||||
|
|
||||||
Every `cryptic-net daemon` process ships with a DNS server which runs
|
Every `isle daemon` process ships with a DNS server which runs
|
||||||
automatically. This server will listen on port 53 on the VPN IP of that
|
automatically. This server will listen on port 53 on the VPN IP of that
|
||||||
particular host.
|
particular host.
|
||||||
|
|
||||||
The server will serve requests for `<hostname>.hosts.cryptic.io` hostnames,
|
The server will serve requests for `<hostname>.hosts.<domain>` hostnames,
|
||||||
where `<hostname>` is any host's name in the `bootstrap/nebula/hosts` directory.
|
where `<hostname>` is the name of any host in the network, and `<domain`> is the
|
||||||
The returned IP will be the corresponding IP for the host, as listed in the
|
network's domain name.
|
||||||
host's `bootstrap/nebula/hosts` file.
|
|
||||||
|
|
||||||
If a request for a non `.cryptic.io` hostname is received then the server will
|
If a request for a hostname not within the network's domain is received then the
|
||||||
forward the request to a pre-configured public resolver. The set of public
|
server will forward the request to a pre-configured public resolver. The set of
|
||||||
resolvers used can be configured using the
|
public resolvers used can be configured in the `/etc/isle/daemon.yml` file.
|
||||||
[daemon.yml](creating-a-daemonyml-file.md) file.
|
|
||||||
|
|
||||||
This DNS server is an optional feature of cryptic-net, and not required in
|
This DNS server is an optional feature of Isle, and not required in general for
|
||||||
general for making use of the network.
|
making use of the network.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
As an example of how to make use of this DNS server, let's say my host's IP on
|
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`. In order to configure the host to use the
|
the network is `10.10.1.1`, and my network's domain is `cool.internal`.
|
||||||
cryptic-net DNS server for all DNS requests, I could do something like this:
|
In order to configure the host to use the isle DNS server for all DNS
|
||||||
|
requests, I could do something like this:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo su
|
sudo su
|
||||||
echo "nameserver 10.10.1.1" > /etc/resolv.conf
|
echo "nameserver 10.10.1.1" > /etc/resolv.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
From that point, all DNS requests on my host would hit the cryptic-net DNS
|
From that point, all DNS requests on my host would hit the isle DNS
|
||||||
server. If I request `my-host.cryptic.io`, it would respond with the appropriate
|
server. If I request `my-host.hosts.cool.internal`, it would respond with the
|
||||||
private IP.
|
appropriate private IP.
|
||||||
|
|
||||||
NOTE that configuration of dns resolvers is very OS-specific, even amongst Linux
|
NOTE that configuration of dns resolvers is very OS-specific, even amongst Linux
|
||||||
distributions, so ensure you know how your resolver configuration works before
|
distributions, so ensure you know how your resolver configuration works before
|
||||||
|
7
flake.lock
Normal file
7
flake.lock
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"root": {}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
38
flake.nix
Normal file
38
flake.nix
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# 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
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
|
|
||||||
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
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
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
|
|
||||||
)
|
|
@ -1,9 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
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";
|
|
||||||
}
|
|
@ -1,122 +0,0 @@
|
|||||||
// 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 (
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Admin is used for accessing all information contained within an admin.tgz.
|
|
||||||
type Admin struct {
|
|
||||||
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.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.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()
|
|
||||||
}
|
|
@ -1,191 +0,0 @@
|
|||||||
// 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/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 (
|
|
||||||
hostNamePath = "hostname"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Bootstrap is used for accessing all information contained within a
|
|
||||||
// bootstrap.tgz file.
|
|
||||||
type Bootstrap struct {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = yamlutil.LoadYamlFSFile(
|
|
||||||
&b.GarageGlobalBucketS3APICredentials,
|
|
||||||
bootstrapFS,
|
|
||||||
garageGlobalBucketKeyYmlPath,
|
|
||||||
); err != nil {
|
|
||||||
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", garageGlobalBucketKeyYmlPath, 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)
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
garageGlobalBucketKeyB, err := yaml.Marshal(b.GarageGlobalBucketS3APICredentials)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("yaml encoding garage global bucket creds: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteFileBytes(garageGlobalBucketKeyYmlPath, garageGlobalBucketKeyB)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
@ -1,81 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
@ -1,423 +0,0 @@
|
|||||||
package entrypoint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
crypticnet "cryptic-net"
|
|
||||||
"cryptic-net/bootstrap"
|
|
||||||
"cryptic-net/garage"
|
|
||||||
"cryptic-net/yamlutil"
|
|
||||||
|
|
||||||
"github.com/cryptic-io/pmux/pmuxlib"
|
|
||||||
"github.com/imdario/mergo"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 writeDaemonYml(userDaemonYmlPath, builtinDaemonYmlPath, runtimeDirPath string) error {
|
|
||||||
|
|
||||||
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(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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
s3Client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating client for global bucket: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
appDirPath := env.AppDirPath
|
|
||||||
|
|
||||||
builtinDaemonYmlPath := filepath.Join(appDirPath, "etc", "daemon.yml")
|
|
||||||
|
|
||||||
if *dumpConfig {
|
|
||||||
|
|
||||||
builtinDaemonYml, err := os.ReadFile(builtinDaemonYmlPath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading default daemon.yml at %q: %w", builtinDaemonYmlPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stdout.Write(builtinDaemonYml); err != nil {
|
|
||||||
return fmt.Errorf("writing default daemon.yml to stdout: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 := writeDaemonYml(*daemonYmlPath, builtinDaemonYmlPath, runtimeDirPath); err != nil {
|
|
||||||
return fmt.Errorf("generating 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 {
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,118 +0,0 @@
|
|||||||
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,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,246 +0,0 @@
|
|||||||
package entrypoint
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cryptic-net/admin"
|
|
||||||
"cryptic-net/bootstrap"
|
|
||||||
"cryptic-net/nebula"
|
|
||||||
"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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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 subCmdHostsMakeBootstrap = 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{
|
|
||||||
Hosts: hosts,
|
|
||||||
HostName: *name,
|
|
||||||
|
|
||||||
NebulaHostCert: nebulaHostCert,
|
|
||||||
|
|
||||||
GarageRPCSecret: adm.GarageRPCSecret,
|
|
||||||
GarageGlobalBucketS3APICredentials: adm.GarageGlobalBucketS3APICredentials,
|
|
||||||
}
|
|
||||||
|
|
||||||
return newBootstrap.WriteTo(os.Stdout)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
subCmdHostsList,
|
|
||||||
subCmdHostsDelete,
|
|
||||||
subCmdHostsMakeBootstrap,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
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(
|
|
||||||
subCmdDaemon,
|
|
||||||
subCmdHosts,
|
|
||||||
subCmdGarage,
|
|
||||||
subCmdVersion,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,129 +0,0 @@
|
|||||||
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})
|
|
||||||
}
|
|
@ -1,256 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,134 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
// Package globals defines global constants and variables which are valid
|
|
||||||
// across all cryptic-net processes and sub-processes.
|
|
||||||
package crypticnet
|
|
@ -1,226 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
// 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"
|
|
||||||
)
|
|
@ -1,41 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,101 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,160 +0,0 @@
|
|||||||
// 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO this should one day not be hardcoded
|
|
||||||
var ipCIDRMask = func() net.IPMask {
|
|
||||||
_, ipNet, err := net.ParseCIDR("10.10.0.0/16")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return ipNet.Mask
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 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) (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),
|
|
||||||
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
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
4
go/.golangci.yml
Normal file
4
go/.golangci.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# https://github.com/golangci/golangci-lint/issues/4733
|
||||||
|
linters-settings:
|
||||||
|
errcheck:
|
||||||
|
ignore : ""
|
195
go/bootstrap/bootstrap.go
Normal file
195
go/bootstrap/bootstrap.go
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
// 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
|
||||||
|
}
|
30
go/bootstrap/garage.go
Normal file
30
go/bootstrap/garage.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
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")
|
||||||
|
}
|
109
go/bootstrap/hosts.go
Normal file
109
go/bootstrap/hosts.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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
|
||||||
|
}
|
14
go/cmd/entrypoint/client.go
Normal file
14
go/cmd/entrypoint/client.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"isle/bootstrap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ctx subCmdCtx) getHosts() ([]bootstrap.Host, error) {
|
||||||
|
res, err := newDaemonRPCClient().GetHosts(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("calling GetHosts: %w", err)
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
96
go/cmd/entrypoint/daemon.go
Normal file
96
go/cmd/entrypoint/daemon.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := ctx.logger()
|
||||||
|
defer logger.Close()
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
logger.WithNamespace("loader"),
|
||||||
|
envBinDirPath,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("instantiating network loader: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
daemonInst, err := daemon.New(ctx, logger, networkLoader, daemonConfig)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("starting daemon: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
logger.Info(ctx, "Stopping child processes")
|
||||||
|
if err := daemonInst.Shutdown(); err != nil {
|
||||||
|
logger.Error(ctx, "Shutting down daemon cleanly failed, there may be orphaned child processes", err)
|
||||||
|
}
|
||||||
|
logger.Info(ctx, "Child processes successfully stopped")
|
||||||
|
}()
|
||||||
|
|
||||||
|
{
|
||||||
|
logger := 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
|
||||||
|
},
|
||||||
|
}
|
74
go/cmd/entrypoint/daemon_util.go
Normal file
74
go/cmd/entrypoint/daemon_util.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"isle/daemon"
|
||||||
|
"isle/daemon/jsonrpc2"
|
||||||
|
"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 newDaemonRPCClient() daemon.RPC {
|
||||||
|
return daemon.RPCFromClient(
|
||||||
|
jsonrpc2.NewUnixHTTPClient(
|
||||||
|
daemon.HTTPSocketPath(), daemonHTTPRPCPath,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
64
go/cmd/entrypoint/flags.go
Normal file
64
go/cmd/entrypoint/flags.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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" }
|
163
go/cmd/entrypoint/garage.go
Normal file
163
go/cmd/entrypoint/garage.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
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 := newDaemonRPCClient().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",
|
||||||
|
do: func(ctx subCmdCtx) error {
|
||||||
|
ctx, err := ctx.withParsedFlags()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientParams, err := newDaemonRPCClient().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.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,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
142
go/cmd/entrypoint/host.go
Normal file
142
go/cmd/entrypoint/host.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
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 := newDaemonRPCClient().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 := newDaemonRPCClient().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,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
68
go/cmd/entrypoint/main.go
Normal file
68
go/cmd/entrypoint/main.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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!")
|
||||||
|
}()
|
||||||
|
|
||||||
|
err := subCmdCtx{
|
||||||
|
Context: ctx,
|
||||||
|
args: os.Args[1:],
|
||||||
|
}.doSubCmd(
|
||||||
|
subCmdDaemon,
|
||||||
|
subCmdGarage,
|
||||||
|
subCmdHost,
|
||||||
|
subCmdNebula,
|
||||||
|
subCmdNetwork,
|
||||||
|
subCmdVersion,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(ctx, "error running command", err)
|
||||||
|
}
|
||||||
|
}
|
135
go/cmd/entrypoint/nebula.go
Normal file
135
go/cmd/entrypoint/nebula.go
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
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 := newDaemonRPCClient().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 := newDaemonRPCClient().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,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
134
go/cmd/entrypoint/network.go
Normal file
134
go/cmd/entrypoint/network.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
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 = newDaemonRPCClient().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 newDaemonRPCClient().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 newDaemonRPCClient().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 newDaemonRPCClient().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,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
239
go/cmd/entrypoint/sub_cmd.go
Normal file
239
go/cmd/entrypoint/sub_cmd.go
Normal file
@ -0,0 +1,239 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"isle/daemon"
|
||||||
|
"isle/jsonutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type flagSet struct {
|
||||||
|
*pflag.FlagSet
|
||||||
|
|
||||||
|
network string
|
||||||
|
logLevel logLevelFlag
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// subCmdCtx contains all information available to a subCmd's do method.
|
||||||
|
type subCmdCtx struct {
|
||||||
|
context.Context
|
||||||
|
|
||||||
|
subCmd subCmd // the subCmd itself
|
||||||
|
args []string // command-line arguments, excluding the subCmd itself.
|
||||||
|
subCmdNames []string // names of subCmds so far, including this one
|
||||||
|
|
||||||
|
flags *flagSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSubCmdCtx(
|
||||||
|
ctx context.Context,
|
||||||
|
subCmd subCmd,
|
||||||
|
args []string,
|
||||||
|
subCmdNames []string,
|
||||||
|
) subCmdCtx {
|
||||||
|
flags := pflag.NewFlagSet(subCmd.name, pflag.ExitOnError)
|
||||||
|
flags.Usage = func() {
|
||||||
|
var passthroughStr string
|
||||||
|
if subCmd.passthroughArgs {
|
||||||
|
passthroughStr = " [--] [args...]"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(
|
||||||
|
os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n",
|
||||||
|
usagePrefix(subCmdNames), subCmd.name, passthroughStr,
|
||||||
|
)
|
||||||
|
fmt.Fprintf(os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(subCmd.name))
|
||||||
|
fmt.Fprintln(os.Stderr, flags.FlagUsages())
|
||||||
|
|
||||||
|
os.Stderr.Sync()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs := &flagSet{
|
||||||
|
FlagSet: flags,
|
||||||
|
logLevel: logLevelFlag{mlog.LevelInfo},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !subCmd.noNetwork {
|
||||||
|
fs.FlagSet.StringVar(
|
||||||
|
&fs.network, "network", "", "Which network to perform the command against, if more than one is joined. Can be an ID, name, or domain.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.FlagSet.VarP(
|
||||||
|
&fs.logLevel,
|
||||||
|
"log-level", "l",
|
||||||
|
"Maximum log level to output. Can be DEBUG, CHILD, INFO, WARN, ERROR, or FATAL.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return subCmdCtx{
|
||||||
|
Context: ctx,
|
||||||
|
subCmd: subCmd,
|
||||||
|
args: args,
|
||||||
|
subCmdNames: subCmdNames,
|
||||||
|
flags: fs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) logger() *mlog.Logger {
|
||||||
|
return mlog.NewLogger(&mlog.LoggerOpts{
|
||||||
|
MaxLevel: ctx.flags.logLevel.Int(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx subCmdCtx) withParsedFlags() (subCmdCtx, error) {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := ctx.flags.Parse(ctx.args); err != nil {
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Context = daemon.WithNetwork(ctx.Context, ctx.flags.network)
|
||||||
|
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.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.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSubCmdCtx := newSubCmdCtx(
|
||||||
|
ctx.Context,
|
||||||
|
subCmd,
|
||||||
|
args,
|
||||||
|
append(ctx.subCmdNames, subCmdName),
|
||||||
|
)
|
||||||
|
|
||||||
|
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(os.Stdout, res)
|
||||||
|
case "yaml":
|
||||||
|
return yaml.NewEncoder(os.Stdout).Encode(res)
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unexpected outputFormat %q", outputFormat))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package entrypoint
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -7,11 +7,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var subCmdVersion = subCmd{
|
var subCmdVersion = subCmd{
|
||||||
name: "version",
|
name: "version",
|
||||||
descr: "Dumps version and build info to stdout",
|
descr: "Dumps version and build info to stdout",
|
||||||
do: func(subCmdCtx subCmdCtx) error {
|
noNetwork: true,
|
||||||
|
do: func(ctx subCmdCtx) error {
|
||||||
|
|
||||||
versionPath := filepath.Join(subCmdCtx.env.AppDirPath, "share/version")
|
versionPath := filepath.Join(envAppDirPath, "share/version")
|
||||||
|
|
||||||
version, err := os.ReadFile(versionPath)
|
version, err := os.ReadFile(versionPath)
|
||||||
|
|
272
go/daemon/children/children.go
Normal file
272
go/daemon/children/children.go
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
// Package children manages the creation, lifetime, and shutdown of child
|
||||||
|
// processes created by the daemon.
|
||||||
|
package children
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.betamike.com/micropelago/pmux/pmuxlib"
|
||||||
|
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
|
||||||
|
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||||
|
|
||||||
|
"isle/bootstrap"
|
||||||
|
"isle/daemon/daecommon"
|
||||||
|
"isle/secrets"
|
||||||
|
"isle/toolkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Children manages all child processes of a network. Child processes are
|
||||||
|
// comprised of:
|
||||||
|
// - nebula
|
||||||
|
// - dnsmasq
|
||||||
|
// - garage (0 or more, depending on configured storage allocations)
|
||||||
|
type Children struct {
|
||||||
|
logger *mlog.Logger
|
||||||
|
binDirPath string
|
||||||
|
runtimeDir toolkit.Dir
|
||||||
|
garageAdminToken string
|
||||||
|
|
||||||
|
garageRPCSecret string
|
||||||
|
|
||||||
|
nebulaProc *pmuxlib.Process
|
||||||
|
dnsmasqProc *pmuxlib.Process
|
||||||
|
garageProcs map[string]*pmuxlib.Process
|
||||||
|
}
|
||||||
|
|
||||||
|
// New initializes and returns a Children instance. If initialization fails an
|
||||||
|
// error is returned.
|
||||||
|
func New(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
binDirPath string,
|
||||||
|
secretsStore secrets.Store,
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
runtimeDir toolkit.Dir,
|
||||||
|
garageAdminToken string,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) (
|
||||||
|
*Children, error,
|
||||||
|
) {
|
||||||
|
logger.Info(ctx, "Loading secrets")
|
||||||
|
garageRPCSecret, err := daecommon.GetGarageRPCSecret(ctx, secretsStore)
|
||||||
|
if err != nil && !errors.Is(err, secrets.ErrNotFound) {
|
||||||
|
return nil, fmt.Errorf("loading garage RPC secret: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Children{
|
||||||
|
logger: logger,
|
||||||
|
binDirPath: binDirPath,
|
||||||
|
runtimeDir: runtimeDir,
|
||||||
|
garageAdminToken: garageAdminToken,
|
||||||
|
garageRPCSecret: garageRPCSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.nebulaProc, err = nebulaPmuxProc(
|
||||||
|
ctx,
|
||||||
|
c.logger,
|
||||||
|
c.runtimeDir.Path,
|
||||||
|
c.binDirPath,
|
||||||
|
networkConfig,
|
||||||
|
hostBootstrap,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("starting nebula: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := waitForNebula(ctx, c.logger, hostBootstrap); err != nil {
|
||||||
|
logger.Warn(ctx, "Failed waiting for nebula to initialize, shutting down child processes", err)
|
||||||
|
c.Shutdown()
|
||||||
|
return nil, fmt.Errorf("waiting for nebula to start: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.dnsmasqProc, err = dnsmasqPmuxProc(
|
||||||
|
ctx,
|
||||||
|
c.logger,
|
||||||
|
c.runtimeDir.Path,
|
||||||
|
c.binDirPath,
|
||||||
|
networkConfig,
|
||||||
|
hostBootstrap,
|
||||||
|
); err != nil {
|
||||||
|
logger.Warn(ctx, "Failed to start dnsmasq, shutting down child processes", err)
|
||||||
|
c.Shutdown()
|
||||||
|
return nil, fmt.Errorf("starting dnsmasq: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO wait for dnsmasq to come up
|
||||||
|
|
||||||
|
if c.garageProcs, err = garagePmuxProcs(
|
||||||
|
ctx,
|
||||||
|
c.logger,
|
||||||
|
garageRPCSecret,
|
||||||
|
c.runtimeDir.Path,
|
||||||
|
c.binDirPath,
|
||||||
|
networkConfig,
|
||||||
|
garageAdminToken,
|
||||||
|
hostBootstrap,
|
||||||
|
); err != nil {
|
||||||
|
logger.Warn(ctx, "Failed to start garage processes, shutting down child processes", err)
|
||||||
|
c.Shutdown()
|
||||||
|
return nil, fmt.Errorf("starting garage processes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := waitForGarage(
|
||||||
|
ctx, c.logger, networkConfig, garageAdminToken, hostBootstrap,
|
||||||
|
); err != nil {
|
||||||
|
logger.Warn(ctx, "Failed waiting for garage processes to initialize, shutting down child processes", err)
|
||||||
|
c.Shutdown()
|
||||||
|
return nil, fmt.Errorf("waiting for garage processes to initialize: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO block until process has been confirmed to have come back up
|
||||||
|
// successfully.
|
||||||
|
func (c *Children) reloadDNSMasq(
|
||||||
|
ctx context.Context,
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) error {
|
||||||
|
if _, changed, err := dnsmasqWriteConfig(
|
||||||
|
ctx, c.logger, c.runtimeDir.Path, networkConfig, hostBootstrap,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("writing new dnsmasq config: %w", err)
|
||||||
|
} else if !changed {
|
||||||
|
c.logger.Info(ctx, "No changes to dnsmasq config file")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logger.Info(ctx, "dnsmasq config file has changed, restarting process")
|
||||||
|
c.dnsmasqProc.Restart()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Children) reloadNebula(
|
||||||
|
ctx context.Context,
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) error {
|
||||||
|
if _, changed, err := nebulaWriteConfig(
|
||||||
|
ctx, c.logger, c.runtimeDir.Path, networkConfig, hostBootstrap,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("writing a new nebula config: %w", err)
|
||||||
|
} else if !changed {
|
||||||
|
c.logger.Info(ctx, "No changes to nebula config file")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logger.Info(ctx, "nebula config file has changed, restarting process")
|
||||||
|
c.nebulaProc.Restart()
|
||||||
|
|
||||||
|
if err := waitForNebula(ctx, c.logger, hostBootstrap); err != nil {
|
||||||
|
return fmt.Errorf("waiting for nebula to start: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Children) reloadGarage(
|
||||||
|
ctx context.Context,
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) error {
|
||||||
|
allocs := networkConfig.Storage.Allocations
|
||||||
|
if len(allocs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var anyChanged bool
|
||||||
|
for _, alloc := range allocs {
|
||||||
|
var (
|
||||||
|
procName = garagePmuxProcName(alloc)
|
||||||
|
ctx = mctx.Annotate(
|
||||||
|
ctx,
|
||||||
|
"garageProcName", procName,
|
||||||
|
"garageDataPath", alloc.DataPath,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO it's possible that the config changed, but only the bootstrap
|
||||||
|
// peers, in which case we don't need to restart the node.
|
||||||
|
childConfigPath, changed, err := garageWriteChildConfig(
|
||||||
|
ctx,
|
||||||
|
c.logger,
|
||||||
|
c.garageRPCSecret,
|
||||||
|
c.runtimeDir.Path,
|
||||||
|
c.garageAdminToken,
|
||||||
|
hostBootstrap,
|
||||||
|
alloc,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
|
||||||
|
} else if !changed {
|
||||||
|
c.logger.Info(ctx, "No changes to garage config file")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
anyChanged = true
|
||||||
|
|
||||||
|
if proc, ok := c.garageProcs[procName]; ok {
|
||||||
|
c.logger.Info(ctx, "garage config has changed, restarting process")
|
||||||
|
proc.Restart()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logger.Info(ctx, "garage config has been added, creating process")
|
||||||
|
c.garageProcs[procName] = garagePmuxProc(
|
||||||
|
ctx, c.logger, c.binDirPath, procName, childConfigPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if anyChanged {
|
||||||
|
if err := waitForGarage(
|
||||||
|
ctx, c.logger, networkConfig, c.garageAdminToken, hostBootstrap,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("waiting for garage to start: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload applies a ReloadDiff to the Children, using the given bootstrap which
|
||||||
|
// must be the same one which was passed to CalculateReloadDiff.
|
||||||
|
func (c *Children) Reload(
|
||||||
|
ctx context.Context,
|
||||||
|
newNetworkConfig daecommon.NetworkConfig,
|
||||||
|
newBootstrap bootstrap.Bootstrap,
|
||||||
|
) error {
|
||||||
|
if err := c.reloadNebula(ctx, newNetworkConfig, newBootstrap); err != nil {
|
||||||
|
return fmt.Errorf("reloading nebula: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
if err := c.reloadDNSMasq(ctx, newNetworkConfig, newBootstrap); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("reloading dnsmasq: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.reloadGarage(ctx, newNetworkConfig, newBootstrap); err != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("reloading garage: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown blocks until all child processes have gracefully shut themselves
|
||||||
|
// down.
|
||||||
|
func (c *Children) Shutdown() {
|
||||||
|
for _, proc := range c.garageProcs {
|
||||||
|
proc.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.dnsmasqProc != nil {
|
||||||
|
c.dnsmasqProc.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.nebulaProc != nil {
|
||||||
|
c.nebulaProc.Stop()
|
||||||
|
}
|
||||||
|
}
|
84
go/daemon/children/dnsmasq.go
Normal file
84
go/daemon/children/dnsmasq.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package children
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"isle/bootstrap"
|
||||||
|
"isle/daemon/daecommon"
|
||||||
|
"isle/dnsmasq"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"code.betamike.com/micropelago/pmux/pmuxlib"
|
||||||
|
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func dnsmasqWriteConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
runtimeDirPath string,
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) (
|
||||||
|
string, bool, error,
|
||||||
|
) {
|
||||||
|
hosts := make([]dnsmasq.ConfDataHost, 0, len(hostBootstrap.Hosts))
|
||||||
|
for _, host := range hostBootstrap.Hosts {
|
||||||
|
hosts = append(hosts, dnsmasq.ConfDataHost{
|
||||||
|
Name: string(host.Name),
|
||||||
|
IP: host.IP().String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
confPath = filepath.Join(runtimeDirPath, "dnsmasq.conf")
|
||||||
|
confData = dnsmasq.ConfData{
|
||||||
|
Resolvers: networkConfig.DNS.Resolvers,
|
||||||
|
Domain: hostBootstrap.NetworkCreationParams.Domain,
|
||||||
|
IP: hostBootstrap.ThisHost().IP().String(),
|
||||||
|
Hosts: hosts,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
changed, err := dnsmasq.WriteConfFile(ctx, logger, confPath, confData)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf(
|
||||||
|
"writing dnsmasq.conf to %q: %w", confPath, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return confPath, changed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO consider a shared dnsmasq across all the daemon's networks.
|
||||||
|
// This would have a few benefits:
|
||||||
|
// - Less processes, less problems
|
||||||
|
// - Less configuration for the user in the case of more than one network.
|
||||||
|
// - Can listen on 127.0.0.x:53, rather than on the nebula address. This
|
||||||
|
// allows DNS to come up before nebula, which is helpful when nebula depends
|
||||||
|
// on DNS.
|
||||||
|
func dnsmasqPmuxProc(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
runtimeDirPath, binDirPath string,
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) (
|
||||||
|
*pmuxlib.Process, error,
|
||||||
|
) {
|
||||||
|
confPath, _, err := dnsmasqWriteConfig(
|
||||||
|
ctx, logger, runtimeDirPath, networkConfig, hostBootstrap,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"writing dnsmasq config: %w", err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := pmuxlib.ProcessConfig{
|
||||||
|
Cmd: filepath.Join(binDirPath, "dnsmasq"),
|
||||||
|
Args: []string{"-d", "-C", confPath},
|
||||||
|
}
|
||||||
|
cfg = withPmuxLoggers(ctx, logger, "dnsmasq", cfg)
|
||||||
|
|
||||||
|
return pmuxlib.NewProcess(cfg), nil
|
||||||
|
}
|
178
go/daemon/children/garage.go
Normal file
178
go/daemon/children/garage.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package children
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"isle/bootstrap"
|
||||||
|
"isle/daemon/daecommon"
|
||||||
|
"isle/garage"
|
||||||
|
"isle/garage/garagesrv"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"code.betamike.com/micropelago/pmux/pmuxlib"
|
||||||
|
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
|
||||||
|
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func garageAdminClientLogger(logger *mlog.Logger) *mlog.Logger {
|
||||||
|
return logger.WithNamespace("garageAdminClient")
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForGarage(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
adminToken string,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
allocs := networkConfig.Storage.Allocations
|
||||||
|
if len(allocs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
adminClientLogger := garageAdminClientLogger(logger)
|
||||||
|
|
||||||
|
for _, alloc := range allocs {
|
||||||
|
|
||||||
|
adminAddr := net.JoinHostPort(
|
||||||
|
hostBootstrap.ThisHost().IP().String(),
|
||||||
|
strconv.Itoa(alloc.AdminPort),
|
||||||
|
)
|
||||||
|
|
||||||
|
adminClient := garage.NewAdminClient(
|
||||||
|
adminClientLogger, adminAddr, adminToken,
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := mctx.Annotate(
|
||||||
|
ctx, "garageAdminAddr", adminAddr, "garageDataPath", alloc.DataPath,
|
||||||
|
)
|
||||||
|
logger.Info(ctx, "Waiting for garage instance to be healthy")
|
||||||
|
|
||||||
|
if err := adminClient.Wait(ctx); err != nil {
|
||||||
|
return fmt.Errorf("waiting for garage instance %q to start up: %w", adminAddr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
adminClient.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func garageWriteChildConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
rpcSecret, runtimeDirPath, adminToken string,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
alloc daecommon.ConfigStorageAllocation,
|
||||||
|
) (
|
||||||
|
string, bool, error,
|
||||||
|
) {
|
||||||
|
var (
|
||||||
|
thisHost = hostBootstrap.ThisHost()
|
||||||
|
id = daecommon.BootstrapGarageHostForAlloc(thisHost, alloc).ID
|
||||||
|
|
||||||
|
node = garage.LocalNode{
|
||||||
|
RemoteNode: garage.RemoteNode{
|
||||||
|
ID: id,
|
||||||
|
IP: thisHost.IP().String(),
|
||||||
|
RPCPort: alloc.RPCPort,
|
||||||
|
S3APIPort: alloc.S3APIPort,
|
||||||
|
},
|
||||||
|
AdminPort: alloc.AdminPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
garageTomlPath = filepath.Join(
|
||||||
|
runtimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
changed, err := garagesrv.WriteGarageTomlFile(
|
||||||
|
ctx,
|
||||||
|
logger,
|
||||||
|
garageTomlPath,
|
||||||
|
garagesrv.GarageTomlData{
|
||||||
|
MetaPath: alloc.MetaPath,
|
||||||
|
DataPath: alloc.DataPath,
|
||||||
|
|
||||||
|
RPCSecret: rpcSecret,
|
||||||
|
AdminToken: adminToken,
|
||||||
|
|
||||||
|
LocalNode: node,
|
||||||
|
BootstrapPeers: hostBootstrap.GarageNodes(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf(
|
||||||
|
"creating garage.toml file at %q: %w", garageTomlPath, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return garageTomlPath, changed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func garagePmuxProcName(alloc daecommon.ConfigStorageAllocation) string {
|
||||||
|
return fmt.Sprintf("garage-%d", alloc.RPCPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func garagePmuxProc(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
binDirPath string,
|
||||||
|
procName string,
|
||||||
|
childConfigPath string,
|
||||||
|
) *pmuxlib.Process {
|
||||||
|
cfg := pmuxlib.ProcessConfig{
|
||||||
|
Cmd: filepath.Join(binDirPath, "garage"),
|
||||||
|
Args: []string{"-c", childConfigPath, "server"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg = withPmuxLoggers(ctx, logger, procName, cfg)
|
||||||
|
|
||||||
|
return pmuxlib.NewProcess(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func garagePmuxProcs(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
rpcSecret, runtimeDirPath, binDirPath string,
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
adminToken string,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) (
|
||||||
|
map[string]*pmuxlib.Process, error,
|
||||||
|
) {
|
||||||
|
var (
|
||||||
|
pmuxProcs = map[string]*pmuxlib.Process{}
|
||||||
|
allocs = networkConfig.Storage.Allocations
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(allocs) > 0 && rpcSecret == "" {
|
||||||
|
return nil, errors.New("Storage allocations defined, but garage RPC secret is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, alloc := range allocs {
|
||||||
|
childConfigPath, _, err := garageWriteChildConfig(
|
||||||
|
ctx,
|
||||||
|
logger,
|
||||||
|
rpcSecret, runtimeDirPath, adminToken,
|
||||||
|
hostBootstrap,
|
||||||
|
alloc,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
procName := garagePmuxProcName(alloc)
|
||||||
|
pmuxProcs[procName] = garagePmuxProc(
|
||||||
|
ctx, logger, binDirPath, procName, childConfigPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pmuxProcs, nil
|
||||||
|
}
|
30
go/daemon/children/jigs.go
Normal file
30
go/daemon/children/jigs.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package children
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// until keeps trying fn until it returns nil, returning true. If the context is
|
||||||
|
// canceled then until returns false.
|
||||||
|
func until(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
descr string,
|
||||||
|
fn func(context.Context) error,
|
||||||
|
) bool {
|
||||||
|
for {
|
||||||
|
logger.Info(ctx, descr)
|
||||||
|
err := fn(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return true
|
||||||
|
} else if ctxErr := ctx.Err(); ctxErr != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Warn(ctx, descr+" failed, retrying in one second", err)
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
59
go/daemon/children/logging.go
Normal file
59
go/daemon/children/logging.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package children
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"isle/toolkit"
|
||||||
|
|
||||||
|
"code.betamike.com/micropelago/pmux/pmuxlib"
|
||||||
|
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pmuxLogger struct {
|
||||||
|
ctx context.Context
|
||||||
|
logger *mlog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPmuxStdoutLogger(
|
||||||
|
ctx context.Context, logger *mlog.Logger, name string,
|
||||||
|
) pmuxlib.Logger {
|
||||||
|
return &pmuxLogger{ctx, logger.WithNamespace(name).WithNamespace("out")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPmuxStderrLogger(
|
||||||
|
ctx context.Context, logger *mlog.Logger, name string,
|
||||||
|
) pmuxlib.Logger {
|
||||||
|
return &pmuxLogger{ctx, logger.WithNamespace(name).WithNamespace("err")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPmuxSysLogger(
|
||||||
|
ctx context.Context, logger *mlog.Logger, name string,
|
||||||
|
) pmuxlib.Logger {
|
||||||
|
return &pmuxLogger{ctx, logger.WithNamespace(name).WithNamespace("sys")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *pmuxLogger) Println(line string) {
|
||||||
|
l.logger.Log(mlog.Message{
|
||||||
|
Context: l.ctx,
|
||||||
|
Level: toolkit.LogLevelChild,
|
||||||
|
Description: line,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *pmuxLogger) Printf(format string, args ...any) {
|
||||||
|
l.Println(fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
func withPmuxLoggers(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
name string,
|
||||||
|
cfg pmuxlib.ProcessConfig,
|
||||||
|
) pmuxlib.ProcessConfig {
|
||||||
|
cfg.StdoutLogger = newPmuxStdoutLogger(ctx, logger, name)
|
||||||
|
cfg.StderrLogger = newPmuxStderrLogger(ctx, logger, name)
|
||||||
|
cfg.SysLogger = newPmuxSysLogger(ctx, logger, name)
|
||||||
|
return cfg
|
||||||
|
}
|
200
go/daemon/children/nebula.go
Normal file
200
go/daemon/children/nebula.go
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
package children
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"isle/bootstrap"
|
||||||
|
"isle/daemon/daecommon"
|
||||||
|
"isle/toolkit"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"code.betamike.com/micropelago/pmux/pmuxlib"
|
||||||
|
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
|
||||||
|
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
|
||||||
|
"github.com/slackhq/nebula/cert"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// waitForNebula waits for the nebula interface to have been started up. It does
|
||||||
|
// this by attempting to create a UDP connection which has the nebula IP set as
|
||||||
|
// its source. If this succeeds we can assume that at the very least the nebula
|
||||||
|
// interface has been initialized.
|
||||||
|
func waitForNebula(
|
||||||
|
ctx context.Context, logger *mlog.Logger, hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) error {
|
||||||
|
var (
|
||||||
|
ip = net.IP(hostBootstrap.ThisHost().IP().AsSlice())
|
||||||
|
lUDPAddr = &net.UDPAddr{IP: ip, Port: 0}
|
||||||
|
rUDPAddr = &net.UDPAddr{IP: ip, Port: 45535}
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx = mctx.Annotate(ctx, "lUDPAddr", lUDPAddr, "rUDPAddr", rUDPAddr)
|
||||||
|
|
||||||
|
until(
|
||||||
|
ctx,
|
||||||
|
logger,
|
||||||
|
"Checking if nebula is online by creating UDP socket from nebula IP",
|
||||||
|
func(context.Context) error {
|
||||||
|
conn, err := net.DialUDP("udp", lUDPAddr, rUDPAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO this needs to be produce a deterministic config value.
|
||||||
|
func nebulaConfig(
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) (
|
||||||
|
map[string]any, error,
|
||||||
|
) {
|
||||||
|
var (
|
||||||
|
lighthouseHostIPs []string
|
||||||
|
staticHostMap = map[string][]string{}
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, host := range hostBootstrap.Hosts {
|
||||||
|
|
||||||
|
if host.Nebula.PublicAddr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := host.IP().String()
|
||||||
|
lighthouseHostIPs = append(lighthouseHostIPs, ip)
|
||||||
|
staticHostMap[ip] = []string{host.Nebula.PublicAddr}
|
||||||
|
}
|
||||||
|
|
||||||
|
caCertPEM, err := hostBootstrap.CAPublicCredentials.Cert.Unwrap().MarshalToPEM()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshaling CA cert to PEM: :%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostCertPEM, err := hostBootstrap.PublicCredentials.Cert.Unwrap().MarshalToPEM()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshaling host cert to PEM: :%w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hostKeyPEM := cert.MarshalX25519PrivateKey(
|
||||||
|
hostBootstrap.PrivateCredentials.EncryptingPrivateKey.Bytes(),
|
||||||
|
)
|
||||||
|
|
||||||
|
config := map[string]any{
|
||||||
|
"pki": map[string]string{
|
||||||
|
"ca": string(caCertPEM),
|
||||||
|
"cert": string(hostCertPEM),
|
||||||
|
"key": string(hostKeyPEM),
|
||||||
|
},
|
||||||
|
"static_host_map": staticHostMap,
|
||||||
|
"punchy": map[string]bool{
|
||||||
|
"punch": true,
|
||||||
|
"respond": true,
|
||||||
|
},
|
||||||
|
"tun": map[string]any{
|
||||||
|
"dev": networkConfig.VPN.Tun.Device,
|
||||||
|
},
|
||||||
|
"firewall": networkConfig.VPN.Firewall,
|
||||||
|
}
|
||||||
|
|
||||||
|
if publicAddr := networkConfig.VPN.PublicAddr; publicAddr == "" {
|
||||||
|
|
||||||
|
config["listen"] = map[string]string{
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": "0",
|
||||||
|
}
|
||||||
|
|
||||||
|
config["lighthouse"] = map[string]any{
|
||||||
|
"hosts": lighthouseHostIPs,
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort(publicAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"parsing public address %q: %w", publicAddr, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This helps with integration testing, so we can set a test to listen
|
||||||
|
// on some local IP without conflicting with something else running on
|
||||||
|
// the host.
|
||||||
|
if hostIP := net.ParseIP(host); hostIP == nil || !hostIP.IsLoopback() {
|
||||||
|
host = "0.0.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
config["listen"] = map[string]string{
|
||||||
|
"host": host,
|
||||||
|
"port": port,
|
||||||
|
}
|
||||||
|
|
||||||
|
config["lighthouse"] = map[string]any{
|
||||||
|
"hosts": []string{},
|
||||||
|
"am_lighthouse": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nebulaWriteConfig(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
runtimeDirPath string,
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) (
|
||||||
|
string, bool, error,
|
||||||
|
) {
|
||||||
|
config, err := nebulaConfig(networkConfig, hostBootstrap)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf("creating nebula config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nebulaYmlPath := filepath.Join(runtimeDirPath, "nebula.yml")
|
||||||
|
|
||||||
|
changed, err := toolkit.WriteFileCheckChanged(
|
||||||
|
ctx, logger, nebulaYmlPath, 0600, func(w io.Writer) error {
|
||||||
|
return yaml.NewEncoder(w).Encode(config)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", false, fmt.Errorf(
|
||||||
|
"writing nebula.yml to %q: %w", nebulaYmlPath, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nebulaYmlPath, changed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nebulaPmuxProc(
|
||||||
|
ctx context.Context,
|
||||||
|
logger *mlog.Logger,
|
||||||
|
runtimeDirPath, binDirPath string,
|
||||||
|
networkConfig daecommon.NetworkConfig,
|
||||||
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
|
) (
|
||||||
|
*pmuxlib.Process, error,
|
||||||
|
) {
|
||||||
|
nebulaYmlPath, _, err := nebulaWriteConfig(
|
||||||
|
ctx, logger, runtimeDirPath, networkConfig, hostBootstrap,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("writing nebula config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := pmuxlib.ProcessConfig{
|
||||||
|
Cmd: filepath.Join(binDirPath, "nebula"),
|
||||||
|
Args: []string{"-config", nebulaYmlPath},
|
||||||
|
}
|
||||||
|
cfg = withPmuxLoggers(ctx, logger, "nebula", cfg)
|
||||||
|
|
||||||
|
return pmuxlib.NewProcess(cfg), nil
|
||||||
|
}
|
136
go/daemon/client.go
Normal file
136
go/daemon/client.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
// Code generated by gowrap. DO NOT EDIT.
|
||||||
|
// template: jsonrpc2/client_gen.tpl
|
||||||
|
// gowrap: http://github.com/hexdigest/gowrap
|
||||||
|
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
//go:generate gowrap gen -p isle/daemon -i RPC -t jsonrpc2/client_gen.tpl -o client.go -l ""
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"isle/bootstrap"
|
||||||
|
"isle/daemon/daecommon"
|
||||||
|
"isle/daemon/jsonrpc2"
|
||||||
|
"isle/daemon/network"
|
||||||
|
"isle/nebula"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rpcClient struct {
|
||||||
|
client jsonrpc2.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCFromClient wraps a Client so that it implements the
|
||||||
|
// RPC interface.
|
||||||
|
func RPCFromClient(client jsonrpc2.Client) RPC {
|
||||||
|
return &rpcClient{client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) CreateHost(ctx context.Context, h1 nebula.HostName, c2 network.CreateHostOpts) (j1 network.JoiningBootstrap, err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
&j1,
|
||||||
|
"CreateHost",
|
||||||
|
h1,
|
||||||
|
c2,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) CreateNebulaCertificate(ctx context.Context, h1 nebula.HostName, e1 nebula.EncryptingPublicKey) (c2 nebula.Certificate, err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
&c2,
|
||||||
|
"CreateNebulaCertificate",
|
||||||
|
h1,
|
||||||
|
e1,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) CreateNetwork(ctx context.Context, name string, domain string, ipNet nebula.IPNet, hostName nebula.HostName) (err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
nil,
|
||||||
|
"CreateNetwork",
|
||||||
|
name,
|
||||||
|
domain,
|
||||||
|
ipNet,
|
||||||
|
hostName,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) GetConfig(ctx context.Context) (n1 daecommon.NetworkConfig, err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
&n1,
|
||||||
|
"GetConfig",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) GetGarageClientParams(ctx context.Context) (g1 network.GarageClientParams, err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
&g1,
|
||||||
|
"GetGarageClientParams",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) GetHosts(ctx context.Context) (ha1 []bootstrap.Host, err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
&ha1,
|
||||||
|
"GetHosts",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) GetNebulaCAPublicCredentials(ctx context.Context) (c2 nebula.CAPublicCredentials, err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
&c2,
|
||||||
|
"GetNebulaCAPublicCredentials",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) GetNetworks(ctx context.Context) (ca1 []bootstrap.CreationParams, err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
&ca1,
|
||||||
|
"GetNetworks",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) JoinNetwork(ctx context.Context, j1 network.JoiningBootstrap) (err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
nil,
|
||||||
|
"JoinNetwork",
|
||||||
|
j1,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) RemoveHost(ctx context.Context, hostName nebula.HostName) (err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
nil,
|
||||||
|
"RemoveHost",
|
||||||
|
hostName,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *rpcClient) SetConfig(ctx context.Context, n1 daecommon.NetworkConfig) (err error) {
|
||||||
|
err = c.client.Call(
|
||||||
|
ctx,
|
||||||
|
nil,
|
||||||
|
"SetConfig",
|
||||||
|
n1,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user