Compare commits
No commits in common. "5c8c24e73e665ae119cf92f197feddabdce7a67a" and "f720d7accdc2abda9bb6c6fcb5b83174a60a22bc" have entirely different histories.
5c8c24e73e
...
f720d7accd
@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
export PATH=$APPDIR/bin
|
||||
exec entrypoint "$@"
|
||||
exec cryptic-net-main entrypoint "$@"
|
||||
|
9
AppDir/bin/wait-for-ip
Normal file
9
AppDir/bin/wait-for-ip
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
ip="$1"
|
||||
shift;
|
||||
|
||||
echo "waiting for $ip to become available..."
|
||||
|
||||
while true; do ping -c1 -W1 "$ip" &> /dev/null && break; done
|
||||
|
||||
exec "$@"
|
@ -9,8 +9,8 @@
|
||||
# A DNS service runs as part of every cryptic-net process.
|
||||
dns:
|
||||
|
||||
# list of IPs that the DNS service will use to resolve requests outside the
|
||||
# network's domain.
|
||||
# list of IPs that the DNS service will use to resolve non-cryptic.io
|
||||
# hostnames.
|
||||
resolvers:
|
||||
- 1.1.1.1
|
||||
- 8.8.8.8
|
||||
@ -66,18 +66,11 @@ storage:
|
||||
#
|
||||
# The ports are all required and must all be unique within and across
|
||||
# allocations.
|
||||
#
|
||||
# THe ports are all _optional_, and will be automatically assigned if they are
|
||||
# not specified. If ports any ports are specified then all should be
|
||||
# specified, and each should be unique across all allocations.
|
||||
#
|
||||
# Once assigned (either implicitly or explicitly) the rpc_port of an
|
||||
# allocation should not be changed.
|
||||
allocations:
|
||||
|
||||
#- data_path: /foo/bar/data
|
||||
# meta_path: /foo/bar/meta
|
||||
# capacity: 1200
|
||||
# #s3_api_port: 3900
|
||||
# #rpc_port: 3901
|
||||
# #admin_port: 3902
|
||||
# api_port: 3900
|
||||
# rpc_port: 3901
|
||||
# admin_port: 3902
|
||||
|
18
README.md
18
README.md
@ -24,9 +24,6 @@ The core components of cryptic-net, currently, are:
|
||||
storage as they care to, if any. Stored data is sharded and replicated across
|
||||
all hosts that choose to provide storage.
|
||||
|
||||
* A DNS server which provides automatic host and service (coming soon) discovery
|
||||
within the network.
|
||||
|
||||
These components are wrapped into a single binary, with all setup being
|
||||
automated. cryptic-net takes "just works" very seriously.
|
||||
|
||||
@ -35,12 +32,20 @@ themselves and others. They can be assured that their communications are private
|
||||
and their storage is reliable, all with zero administrative overhead and zero
|
||||
third parties involved.
|
||||
|
||||
[nebula]: https://github.com/slackhq/nebula
|
||||
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/
|
||||
|
||||
## Documentation
|
||||
|
||||
_NOTE: There is currently only a single live cryptic-net which can be joined,
|
||||
though generalizing the bootstrap process so others can create their own network
|
||||
is [planned][roadmap]. If you do not know the admins of this cryptic-net then
|
||||
unfortunately there's not much you can do right now._
|
||||
|
||||
cryptic-net users fall into different roles, depending on their level of
|
||||
involvement and expertise within their particular network. The documentation for
|
||||
cryptic-net is broken down by these categories, so that the reader can easily
|
||||
decide which documents they need to care about.
|
||||
know which documents they need to care about.
|
||||
|
||||
### User Docs
|
||||
|
||||
@ -83,20 +88,19 @@ likely operators as well.
|
||||
|
||||
Documentation for admins:
|
||||
|
||||
* [Creating a New Network](docs/admin/creating-a-new-network.md)
|
||||
* [Adding a Host to the Network](docs/admin/adding-a-host-to-the-network.md)
|
||||
* Removing a Host From the Network (TODO)
|
||||
|
||||
### Dev Docs
|
||||
|
||||
Devs may or may not be participants in any particular cryptic-net. They instead
|
||||
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://code.betamike.com/cryptic-io/pmux) process tree created
|
||||
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)
|
||||
|
||||
|
48
default.nix
48
default.nix
@ -1,13 +1,9 @@
|
||||
{
|
||||
|
||||
pkgsAttrs ? (import ./nix/pkgs.nix),
|
||||
pkgs ? (import ./nix/pkgs.nix).stable,
|
||||
bootstrap ? null,
|
||||
|
||||
}: let
|
||||
|
||||
pkgs = pkgsAttrs.pkgs;
|
||||
|
||||
in rec {
|
||||
}: rec {
|
||||
|
||||
rootedBootstrap = pkgs.stdenv.mkDerivation {
|
||||
name = "cryptic-net-rooted-bootstrap";
|
||||
@ -17,7 +13,7 @@ in rec {
|
||||
builder = builtins.toFile "builder.sh" ''
|
||||
source $stdenv/setup
|
||||
mkdir -p "$out"/share
|
||||
cp "$src" "$out"/share/bootstrap.yml
|
||||
cp "$src" "$out"/share/bootstrap.tgz
|
||||
'';
|
||||
};
|
||||
|
||||
@ -25,45 +21,36 @@ in rec {
|
||||
name = "cryptic-net-version";
|
||||
|
||||
buildInputs = [ pkgs.git pkgs.go ];
|
||||
|
||||
src = ./.;
|
||||
inherit bootstrap;
|
||||
nixPkgsVersion = pkgsAttrs.version;
|
||||
nixPkgsRev = pkgsAttrs.rev;
|
||||
builtByUser = builtins.getEnv "USER";
|
||||
|
||||
builder = builtins.toFile "builder.sh" ''
|
||||
source $stdenv/setup
|
||||
|
||||
versionFile=version
|
||||
|
||||
cp -r "$src" srcCp
|
||||
|
||||
if [ "$bootstrap" != "" ]; then
|
||||
hostName=$(tar -xzf "$bootstrap" --to-stdout ./hostname)
|
||||
echo "Built for host: $hostName" >> "$versionFile"
|
||||
fi
|
||||
|
||||
echo "Build date: $(date) ($(date +%s))" >> "$versionFile"
|
||||
echo "Built by: $builtByUser" >> "$versionFile"
|
||||
echo "Git rev: $(cd srcCp && git describe --always --long --dirty=' (dirty)')" >> "$versionFile"
|
||||
echo "Build date: $(date)" >> "$versionFile"
|
||||
echo "Git status: $(cd "$src" && git describe --always --long --dirty=' (dirty)')" >> "$versionFile"
|
||||
echo "Go version: $(go version)" >> "$versionFile"
|
||||
echo "Nixpkgs version: $nixPkgsVersion ($nixPkgsRev)" >> "$versionFile"
|
||||
echo "Build host info: $(uname -srvm)" >> "$versionFile"
|
||||
|
||||
mkdir -p "$out"/share
|
||||
cp "$versionFile" "$out"/share
|
||||
'';
|
||||
};
|
||||
|
||||
entrypoint = pkgs.callPackage ./entrypoint {};
|
||||
goWorkspace = pkgs.callPackage ./go-workspace {};
|
||||
|
||||
dnsmasq = (pkgs.callPackage ./nix/dnsmasq.nix {
|
||||
dnsmasq = (pkgs.callPackage ./dnsmasq {
|
||||
glibcStatic = pkgs.glibc.static;
|
||||
});
|
||||
}).env;
|
||||
|
||||
nebula = pkgs.callPackage ./nix/nebula.nix {};
|
||||
|
||||
garage = (pkgs.callPackage ./nix/garage.nix {}).env;
|
||||
garage = (pkgs.callPackage ./garage {}).env;
|
||||
|
||||
waitFor = pkgs.callPackage ./nix/wait-for.nix {};
|
||||
|
||||
@ -71,12 +58,23 @@ in rec {
|
||||
name = "cryptic-net-AppDir";
|
||||
paths = [
|
||||
|
||||
pkgs.pkgsStatic.bash
|
||||
pkgs.pkgsStatic.coreutils
|
||||
pkgs.pkgsStatic.unixtools.ping
|
||||
pkgs.pkgsStatic.netcat # required by waitFor
|
||||
pkgs.pkgsStatic.gnutar
|
||||
pkgs.pkgsStatic.gzip
|
||||
|
||||
# custom packages from ./pkgs.nix
|
||||
pkgs.yq-go
|
||||
pkgs.nebula
|
||||
|
||||
./AppDir
|
||||
version
|
||||
dnsmasq
|
||||
nebula
|
||||
garage
|
||||
entrypoint
|
||||
waitFor
|
||||
goWorkspace.crypticNetMain
|
||||
|
||||
] ++ (if bootstrap != null then [ rootedBootstrap ] else []);
|
||||
};
|
||||
|
34
dnsmasq/bin/dnsmasq-entrypoint
Normal file
34
dnsmasq/bin/dnsmasq-entrypoint
Normal file
@ -0,0 +1,34 @@
|
||||
# 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"
|
39
dnsmasq/default.nix
Normal file
39
dnsmasq/default.nix
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
|
||||
stdenv,
|
||||
buildEnv,
|
||||
glibcStatic,
|
||||
rebase,
|
||||
|
||||
}: rec {
|
||||
|
||||
dnsmasq = stdenv.mkDerivation rec {
|
||||
pname = "dnsmasq";
|
||||
version = "2.85";
|
||||
|
||||
src = builtins.fetchurl {
|
||||
url = "https://www.thekelleys.org.uk/dnsmasq/${pname}-${version}.tar.xz";
|
||||
sha256 = "sha256-rZjTgD32h+W5OAgPPSXGKP5ByHh1LQP7xhmXh/7jEvo=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [ glibcStatic ];
|
||||
|
||||
makeFlags = [
|
||||
"LDFLAGS=-static"
|
||||
"DESTDIR="
|
||||
"BINDIR=$(out)/bin"
|
||||
"MANDIR=$(out)/man"
|
||||
"LOCALEDIR=$(out)/share/locale"
|
||||
];
|
||||
};
|
||||
|
||||
env = buildEnv {
|
||||
name = "cryptic-net-dnsmasq";
|
||||
paths = [
|
||||
(rebase "cryptic-net-dnsmasq-bin" ./bin "bin")
|
||||
(rebase "cryptic-net-dnsmasq-etc" ./etc "etc/dnsmasq")
|
||||
dnsmasq
|
||||
];
|
||||
};
|
||||
|
||||
}
|
41
dnsmasq/etc/base.conf
Normal file
41
dnsmasq/etc/base.conf
Normal file
@ -0,0 +1,41 @@
|
||||
# Configuration file for dnsmasq.
|
||||
#
|
||||
# Format is one option per line, legal options are the same
|
||||
# as the long options legal on the command line. See
|
||||
# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details.
|
||||
|
||||
# Listen on this specific port instead of the standard DNS port
|
||||
# (53). Setting this to zero completely disables DNS function,
|
||||
# leaving only DHCP and/or TFTP.
|
||||
port=53
|
||||
|
||||
# If you don't want dnsmasq to read /etc/resolv.conf or any other
|
||||
# file, getting its servers from this file instead (see below), then
|
||||
# uncomment this.
|
||||
no-resolv
|
||||
|
||||
# On systems which support it, dnsmasq binds the wildcard address,
|
||||
# even when it is listening on only some interfaces. It then discards
|
||||
# requests that it shouldn't reply to. This has the advantage of
|
||||
# working even when interfaces come and go and change address. If you
|
||||
# want dnsmasq to really bind only the interfaces it is listening on,
|
||||
# uncomment this option. About the only time you may need this is when
|
||||
# running another nameserver on the same machine.
|
||||
bind-interfaces
|
||||
|
||||
# If you don't want dnsmasq to read /etc/hosts, uncomment the
|
||||
# following line.
|
||||
no-hosts
|
||||
|
||||
# Unset user and group so that dnsmasq doesn't drop privileges to another user.
|
||||
# If this isn't done then dnsmasq fails to start up, since it fails to access
|
||||
# /etc/passwd correctly, probably due to nix.
|
||||
user=
|
||||
group=
|
||||
|
||||
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
#
|
||||
# Everything below is generated dynamically based on runtime configuration
|
||||
#
|
||||
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
@ -7,12 +7,12 @@ 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.yml` file for their host,
|
||||
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.yml` embedded into it. The user can be given this
|
||||
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
|
||||
@ -29,63 +29,66 @@ conform to the following rules:
|
||||
|
||||
* It should end with a letter or number.
|
||||
|
||||
## Step 2: Choose IP
|
||||
## Step 2: Add Host to Network
|
||||
|
||||
The admin should choose an IP for the host. The IP you choose for the new host
|
||||
should be one which is not yet used by any other host, and which is in subnet
|
||||
which was configured when creating the network.
|
||||
should be one which is not yet used by any other host, and which is in the VPN's
|
||||
set of allowed IPs.
|
||||
|
||||
## Step 3: Create a `bootstrap.yml` File
|
||||
The admin should perform the following command from their own host:
|
||||
|
||||
Access to an `admin.yml` file is required for this step.
|
||||
```
|
||||
cryptic-net hosts add --name <name> --ip <ip>
|
||||
```
|
||||
|
||||
To create a `bootstrap.yml` file for the new host, the admin should perform the
|
||||
## 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:
|
||||
|
||||
```
|
||||
cryptic-net hosts make-bootstrap \
|
||||
--name <name> \
|
||||
--ip <ip> \
|
||||
--admin-path <path to admin.yml> \
|
||||
> bootstrap.yml
|
||||
--admin-path <path to admin.tgz> \
|
||||
> bootstrap.tgz
|
||||
```
|
||||
|
||||
The resulting `bootstrap.yml` file should be treated as a secret file that is
|
||||
shared only with the user it was generated for. The `bootstrap.yml` file should
|
||||
The resulting `bootstrap.tgz` file should be treated as a secret file that is
|
||||
shared only with the user it was generated for. The `bootstrap.tgz` file should
|
||||
not be re-used between hosts either.
|
||||
|
||||
If the user already has access to a `cryptic-net` binary then the new
|
||||
`bootstrap.yml` file can be given to them as-is, and they can proceed with
|
||||
`bootstrap.tgz` file can be given to them as-is, and they can proceed with
|
||||
running their host's `cryptic-net daemon`.
|
||||
|
||||
### Encrypted `admin.yml`
|
||||
### Encrypted `admin.tgz`
|
||||
|
||||
If `admin.yml` is kept in an encrypted format on disk (it should be!) then the
|
||||
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.yml` then the following could be used to
|
||||
generate a `bootstrap.yml`:
|
||||
GPG is being used to secure `admin.tgz` then the following could be used to
|
||||
generate a `bootstrap.tgz`:
|
||||
|
||||
```
|
||||
gpg -d <path to admin.yml.gpg> | cryptic-net hosts make-boostrap \
|
||||
gpg -d <path to admin.tgz.gpg> | cryptic-net hosts make-boostrap \
|
||||
--name <name> \
|
||||
--ip <ip> \
|
||||
--admin-path - \
|
||||
> bootstrap.yml
|
||||
> bootstrap.tgz
|
||||
```
|
||||
|
||||
Note that the value of `--admin-path` is `-`, indicating that `admin.yml` should
|
||||
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.yml` into a custom binary for the user (to
|
||||
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.yml> -A appImage
|
||||
nix-build --arg bootstrap <path to bootstrap.tgz> -A appImage
|
||||
```
|
||||
|
||||
The resulting binary can be found in the `result` directory which is created.
|
||||
|
||||
This binary should be treated like a `bootstrap.yml` in terms of its uniqueness
|
||||
and sensitivity.
|
||||
Note that this binary should be treated like a `bootstrap.tgz` in terms of its
|
||||
uniqueness and sensitivity.
|
||||
|
@ -1,149 +0,0 @@
|
||||
# Creating a New Network
|
||||
|
||||
This guide is for those who wish to start a new cryptic-net network of their
|
||||
own.
|
||||
|
||||
By starting a new cryptic-net 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 300 GB of disk storage space.
|
||||
|
||||
* At least 3 directories should be chosen, each of which will be committing at
|
||||
least 100GB. 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: Create a `daemon.yml` File
|
||||
|
||||
A `daemon.yml` will need to be created for use during network creation. You can
|
||||
create a new `daemon.yml` with default values filled in by doing:
|
||||
|
||||
```
|
||||
cryptic-net admin create-network --dump-config > /path/to/daemon.yml
|
||||
```
|
||||
|
||||
Open this file 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.
|
||||
|
||||
## 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.
|
||||
|
||||
* 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: cryptic-net 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: Prepare to Encrypt `admin.yml`
|
||||
|
||||
The `admin.yml` file (which will be created in the next step) is the most
|
||||
sensitive part of a cryptic-net network. If it falls into the wrong hands it can
|
||||
be used to completely compromise your network, impersonate hosts on the network,
|
||||
and will likely lead to someone stealing or deleting all of your data.
|
||||
|
||||
Therefore it is important that the file remains encrypted when it is not being
|
||||
used, and that it is never stored to disk in its decrypted form.
|
||||
|
||||
This guide assumes that you have GPG already set up with your own secret key,
|
||||
and that you are familiar with how it works. There is no requirement to use GPG,
|
||||
if you care to use a different method.
|
||||
|
||||
## Step 4: Create the `admin.yml` File
|
||||
|
||||
To create the `admin.yml` file, which effectively creates the network itself,
|
||||
you can run:
|
||||
|
||||
```
|
||||
sudo cryptic-net admin create-network \
|
||||
--config /path/to/daemon.yml \
|
||||
--domain <domain> \
|
||||
--ip <ip/subnet-prefix> \
|
||||
--name <hostname> \
|
||||
| gpg -e -r <my gpg email> \
|
||||
> admin.yml.gpg
|
||||
```
|
||||
|
||||
A couple of notes here:
|
||||
|
||||
* The `--ip` parameter is formed from both the subnet and the IP you chose
|
||||
within it. So if your subnet is `10.10.0.0/16`, and your chosen IP in that
|
||||
subnet is `10.10.4.20`, then your `--ip` parameter will be `10.10.4.20/16`.
|
||||
|
||||
* Only one gpg recipient is specified. If you intend on including other users as
|
||||
network administrators you can add them to the recipients list at this step,
|
||||
so they will be able to use the `admin.yml` file as well. You can also
|
||||
manually add them as recipients later.
|
||||
|
||||
You will see a lot of output, as `create-network` starts up many child processes
|
||||
in order to set the network up. It should exit successfully on its own after a
|
||||
few seconds.
|
||||
|
||||
At this point you should have an `admin.yml.gpg` file in your current directory.
|
||||
|
||||
## Step 5: Run the Daemon
|
||||
|
||||
The cryptic-net daemon can be run now, using the following command:
|
||||
|
||||
```
|
||||
sudo cryptic-net daemon -c /path/to/daemon.yml
|
||||
```
|
||||
|
||||
**NOTE** that you _must_ use the same `daemon.yml` file used when creating the
|
||||
network for the daemon itself.
|
||||
|
||||
At this point your host, and your network, are ready to go! You can reference
|
||||
the [Getting Started](../user/getting-started.md) document to set up your
|
||||
host's daemon process in a more permanent way.
|
||||
|
||||
## Next Steps
|
||||
|
||||
* Add users
|
||||
* Fix directories
|
||||
|
||||
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
|
@ -11,31 +11,55 @@ state AppDir {
|
||||
AppRun : * Set PATH to APPDIR/bin
|
||||
}
|
||||
|
||||
state "./bin/entrypoint daemon -c ./daemon.yml" as entrypoint {
|
||||
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.yml into $_DATA_DIR_PATH, if it's not there
|
||||
entrypoint : * Merge daemon.yml config into bootstrap.yml
|
||||
entrypoint : * Create $_RUNTIME_DIR_PATH/dnsmasq.conf
|
||||
entrypoint : * Create $_RUNTIME_DIR_PATH/nebula.yml
|
||||
entrypoint : * Copy bootstrap.tgz into $_DATA_DIR_PATH, if it's not there
|
||||
entrypoint : * Merge daemon.yml config into bootstrap.tgz
|
||||
entrypoint : * Create $_RUNTIME_DIR_PATH/garage-N.toml\n (one per storage allocation)
|
||||
entrypoint : * Run child processes
|
||||
entrypoint : * (in the background) Updates garage cluster layout
|
||||
entrypoint : * (in the background) Stores host info in global bucket
|
||||
}
|
||||
|
||||
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 --> dnsmasq : child
|
||||
|
||||
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 --> nebula : child
|
||||
|
||||
state "./bin/garage -c $_RUNTIME_DIR_PATH/garage-N.toml server" as garage
|
||||
entrypoint --> nebulaEntrypoint : child
|
||||
nebulaEntrypoint --> nebula : exec
|
||||
|
||||
state "Garage processes (only if any storage allocs are defined)" as garageChildren {
|
||||
|
||||
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 --> garage : child (one per storage allocation)
|
||||
entrypoint --> 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: 11 KiB After Width: | Height: | Size: 17 KiB |
@ -4,14 +4,14 @@ If your host machine can be reasonably sure of being online most, if not all, of
|
||||
the time, and has 100GB or more of unused drive space you'd like to contribute
|
||||
to the network, then this document is for you.
|
||||
|
||||
## Create `daemon.yml`
|
||||
## 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 storage locations and
|
||||
capacities you want to contribute.
|
||||
|
||||
## Edit `daemon.yml`
|
||||
## Edit daemon.yml
|
||||
|
||||
Open your `daemon.yml` file in a text editor, and find the
|
||||
`storage.allocations` section.
|
||||
@ -34,21 +34,27 @@ storage:
|
||||
- data_path: /mnt/drive1/cryptic-net/data
|
||||
meta_path: /mnt/drive1/cryptic-net/meta
|
||||
capacity: 1200
|
||||
api_port: 3900
|
||||
rpc_port: 3901
|
||||
|
||||
# 100 GB (the minimum) are being shared from drive2
|
||||
- data_path: /mnt/drive2/cryptic-net/data
|
||||
meta_path: /mnt/drive2/cryptic-net/meta
|
||||
capacity: 100
|
||||
api_port: 3910
|
||||
rpc_port: 3911
|
||||
```
|
||||
|
||||
## Setup Firewall
|
||||
|
||||
You will need to configure your hosts's firewall to allow traffic from
|
||||
cryptic-net IPs on the ports you specified in your allocations.
|
||||
|
||||
## Restart the Daemon
|
||||
|
||||
With the `daemon.yml` configured, you should restart your `cryptic-net daemon`
|
||||
process.
|
||||
|
||||
The `cryptic-net daemon` will automatically allow the ports used for your
|
||||
storage allocations in the vpn firewall.
|
||||
|
||||
## Further Reading
|
||||
|
||||
cryptic-net uses the [garage][garage] project for its storage system. See the
|
||||
|
@ -14,6 +14,12 @@ 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.
|
||||
@ -63,7 +69,7 @@ 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.yml` file, rather than requiring manual setup by
|
||||
- Allow importing a `bootstrap.tgz` file, rather than requiring manual setup by
|
||||
users.
|
||||
|
||||
- Set device's DNS settings. There is an [open
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
|
||||
pkgs ? (import ../nix/pkgs.nix).pkgs,
|
||||
pkgs ? (import ../nix/pkgs.nix).stable,
|
||||
|
||||
}: pkgs.mkShell {
|
||||
name = "cryptic-net-build-docs";
|
||||
|
@ -1,8 +1,8 @@
|
||||
# Creating a daemon.yml File
|
||||
|
||||
The `cryptic-net daemon` process has generally sane defaults and does not need
|
||||
to be configured for most users. This document describes how to use the
|
||||
`daemon.yml` file to handle those cases where configuration is necessary.
|
||||
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
|
||||
|
||||
@ -28,5 +28,5 @@ 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 service file accordingly.
|
||||
not), and you will need to modify the `cryptic-net.service` accordingly.
|
||||
|
||||
|
@ -13,7 +13,7 @@ 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 your network to give you one!
|
||||
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
|
||||
@ -37,7 +37,7 @@ 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.yml` for
|
||||
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.
|
||||
|
@ -4,13 +4,14 @@ Every `cryptic-net daemon` process ships with a DNS server which runs
|
||||
automatically. This server will listen on port 53 on the VPN IP of that
|
||||
particular host.
|
||||
|
||||
The server will serve requests for `<hostname>.hosts.<domain>` hostnames,
|
||||
where `<hostname>` is the name of any host in the network, and `<domain`> is the
|
||||
network's domain name.
|
||||
The server will serve requests for `<hostname>.hosts.cryptic.io` hostnames,
|
||||
where `<hostname>` is any host's name in the `bootstrap/nebula/hosts` directory.
|
||||
The returned IP will be the corresponding IP for the host, as listed in the
|
||||
host's `bootstrap/nebula/hosts` file.
|
||||
|
||||
If a request for a hostname not within the network's domain is received then the
|
||||
server will forward the request to a pre-configured public resolver. The set of
|
||||
public resolvers used can be configured using the
|
||||
If a request for a non `.cryptic.io` hostname is received then the server will
|
||||
forward the request to a pre-configured public resolver. The set of public
|
||||
resolvers used can be configured using the
|
||||
[daemon.yml](creating-a-daemonyml-file.md) file.
|
||||
|
||||
This DNS server is an optional feature of cryptic-net, and not required in
|
||||
@ -19,9 +20,8 @@ general for making use of the network.
|
||||
## Example
|
||||
|
||||
As an example of how to make use of this DNS server, let's say my host's IP on
|
||||
the network is `10.10.1.1`, and my network's domain is `cool.internal`.
|
||||
In order to configure the host to use the cryptic-net DNS server for all DNS
|
||||
requests, I could do something like this:
|
||||
the network is `10.10.1.1`. In order to configure the host to use the
|
||||
cryptic-net DNS server for all DNS requests, I could do something like this:
|
||||
|
||||
```
|
||||
sudo su
|
||||
@ -29,8 +29,8 @@ echo "nameserver 10.10.1.1" > /etc/resolv.conf
|
||||
```
|
||||
|
||||
From that point, all DNS requests on my host would hit the cryptic-net DNS
|
||||
server. If I request `my-host.hosts.cool.internal`, it would respond with the
|
||||
appropriate private IP.
|
||||
server. If I request `my-host.cryptic.io`, it would respond with the appropriate
|
||||
private IP.
|
||||
|
||||
NOTE that configuration of dns resolvers is very OS-specific, even amongst Linux
|
||||
distributions, so ensure you know how your resolver configuration works before
|
||||
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
|
||||
buildGoModule,
|
||||
|
||||
}: buildGoModule {
|
||||
|
||||
pname = "cryptic-net-entrypoint";
|
||||
version = "unstable";
|
||||
src = ./src;
|
||||
vendorSha256 = "sha256-1mHD0tmITlGjeo6F+Dvd2TdEPzxWtndy/J+uGHWKen4=";
|
||||
subPackages = [
|
||||
"cmd/entrypoint"
|
||||
];
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
// Package admin deals with the parsing and creation of admin.yml files.
|
||||
package admin
|
||||
|
||||
import (
|
||||
"cryptic-net/garage"
|
||||
"cryptic-net/nebula"
|
||||
"io"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// 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 `yaml:"id"`
|
||||
Domain string `yaml:"domain"`
|
||||
}
|
||||
|
||||
// Admin is used for accessing all information contained within an admin.yml.
|
||||
type Admin struct {
|
||||
CreationParams CreationParams `yaml:"creation_params"`
|
||||
|
||||
Nebula struct {
|
||||
CACredentials nebula.CACredentials `yaml:"ca_credentials"`
|
||||
} `yaml:"nebula"`
|
||||
|
||||
Garage struct {
|
||||
RPCSecret string `yaml:"rpc_secret"`
|
||||
GlobalBucketS3APICredentials garage.S3APICredentials `yaml:"global_bucket_s3_api_credentials"`
|
||||
} `yaml:"garage"`
|
||||
}
|
||||
|
||||
// FromReader reads an admin.yml from the given io.Reader.
|
||||
func FromReader(r io.Reader) (Admin, error) {
|
||||
var a Admin
|
||||
err := yaml.NewDecoder(r).Decode(&a)
|
||||
return a, err
|
||||
}
|
||||
|
||||
// WriteTo writes the Admin as an admin.yml to the given io.Writer.
|
||||
func (a Admin) WriteTo(into io.Writer) error {
|
||||
return yaml.NewEncoder(into).Encode(a)
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
// Package bootstrap deals with the parsing and creation of bootstrap.yml files.
|
||||
// It also contains some helpers which rely on bootstrap data.
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"cryptic-net/admin"
|
||||
"cryptic-net/garage"
|
||||
"cryptic-net/nebula"
|
||||
"crypto/sha512"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// DataDirPath returns the path within the user's data directory where the
|
||||
// bootstrap file is stored.
|
||||
func DataDirPath(dataDirPath string) string {
|
||||
return filepath.Join(dataDirPath, "bootstrap.yml")
|
||||
}
|
||||
|
||||
// AppDirPath returns the path within the AppDir where an embedded bootstrap
|
||||
// file might be found.
|
||||
func AppDirPath(appDirPath string) string {
|
||||
return filepath.Join(appDirPath, "share/bootstrap.yml")
|
||||
}
|
||||
|
||||
// Bootstrap is used for accessing all information contained within a
|
||||
// bootstrap.yml file.
|
||||
type Bootstrap struct {
|
||||
AdminCreationParams admin.CreationParams `yaml:"admin_creation_params"`
|
||||
|
||||
Hosts map[string]Host `yaml:"hosts"`
|
||||
HostName string `yaml:"hostname"`
|
||||
|
||||
Nebula struct {
|
||||
HostCredentials nebula.HostCredentials `yaml:"host_credentials"`
|
||||
} `yaml:"nebula"`
|
||||
|
||||
Garage struct {
|
||||
RPCSecret string `yaml:"rpc_secret"`
|
||||
AdminToken string `yaml:"admin_token"`
|
||||
GlobalBucketS3APICredentials garage.S3APICredentials `yaml:"global_bucket_s3_api_credentials"`
|
||||
} `yaml:"garage"`
|
||||
}
|
||||
|
||||
// FromReader reads a bootstrap.yml file from the given io.Reader.
|
||||
func FromReader(r io.Reader) (Bootstrap, error) {
|
||||
var b Bootstrap
|
||||
err := yaml.NewDecoder(r).Decode(&b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// FromFile reads a bootstrap.yml 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.yml to the given io.Writer.
|
||||
func (b Bootstrap) WriteTo(into io.Writer) error {
|
||||
return yaml.NewEncoder(into).Encode(b)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"cryptic-net/garage"
|
||||
"cryptic-net/nebula"
|
||||
"fmt"
|
||||
"os"
|
||||
"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.signed file for this host
|
||||
// into garage so that other hosts are able to see relevant configuration for
|
||||
// it.
|
||||
func (b Bootstrap) PutGarageBoostrapHost(ctx context.Context) error {
|
||||
|
||||
host := b.ThisHost()
|
||||
client := b.GlobalBucketS3APIClient()
|
||||
|
||||
hostB, err := yaml.Marshal(host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("yaml encoding host data: %w", err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
err = nebula.SignAndWrap(buf, b.Nebula.HostCredentials.HostKeyPEM, hostB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("signing encoded host data: %w", err)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(
|
||||
garageGlobalBucketBootstrapHostsDirPath,
|
||||
host.Name+".yml.signed",
|
||||
)
|
||||
|
||||
_, 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.signed 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.signed",
|
||||
)
|
||||
|
||||
return client.RemoveObject(
|
||||
ctx, garage.GlobalBucket, filePath,
|
||||
minio.RemoveObjectOptions{},
|
||||
)
|
||||
}
|
||||
|
||||
// GetGarageBootstrapHosts loads the <hostname>.yml.signed file for all hosts
|
||||
// stored in garage.
|
||||
func (b Bootstrap) GetGarageBootstrapHosts(
|
||||
ctx context.Context,
|
||||
) (
|
||||
map[string]Host, error,
|
||||
) {
|
||||
|
||||
caCertPEM := b.Nebula.HostCredentials.CACertPEM
|
||||
client := b.GlobalBucketS3APIClient()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
hostB, sig, err := nebula.Unwrap(obj)
|
||||
obj.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unwrapping signature from %q: %w", objInfo.Key, err)
|
||||
}
|
||||
|
||||
var host Host
|
||||
if err = yaml.Unmarshal(hostB, &host); err != nil {
|
||||
return nil, fmt.Errorf("yaml decoding object %q: %w", objInfo.Key, err)
|
||||
}
|
||||
|
||||
hostCertPEM := host.Nebula.CertPEM
|
||||
|
||||
if err := nebula.ValidateSignature(hostCertPEM, hostB, sig); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid host data for %q: %v\n", objInfo.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := nebula.ValidateHostCertPEM(caCertPEM, hostCertPEM); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid nebula cert for %q: %v\n", objInfo.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
hosts[host.Name] = host
|
||||
}
|
||||
|
||||
return hosts, nil
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"cryptic-net/nebula"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// NebulaHost describes the nebula configuration of a Host which is relevant for
|
||||
// other hosts to know.
|
||||
type NebulaHost struct {
|
||||
CertPEM string `yaml:"crt"`
|
||||
PublicAddr string `yaml:"public_addr,omitempty"`
|
||||
}
|
||||
|
||||
// GarageHost describes a single garage instance in the GarageHost.
|
||||
type GarageHostInstance struct {
|
||||
ID string `yaml:"id"`
|
||||
RPCPort int `yaml:"rpc_port"`
|
||||
S3APIPort int `yaml:"s3_api_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"`
|
||||
}
|
||||
|
||||
// IP returns the IP address encoded in the Host's nebula certificate, or panics
|
||||
// if there is an error.
|
||||
func (h Host) IP() net.IP {
|
||||
|
||||
ip, err := nebula.IPFromHostCertPEM(h.Nebula.CertPEM)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("could not parse IP out of cert for host %q: %w", h.Name, err))
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
@ -1,311 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cryptic-net/admin"
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/daemon"
|
||||
"cryptic-net/garage"
|
||||
"cryptic-net/nebula"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.betamike.com/cryptic-io/pmux/pmuxlib"
|
||||
)
|
||||
|
||||
func randStr(l int) string {
|
||||
b := make([]byte, l)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
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.yml from stdin: %w", err)
|
||||
}
|
||||
|
||||
return adm, nil
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return admin.Admin{}, fmt.Errorf("opening file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return admin.FromReader(f)
|
||||
}
|
||||
|
||||
var subCmdAdminCreateNetwork = subCmd{
|
||||
name: "create-network",
|
||||
descr: "Creates a new cryptic-net network, outputting the resulting admin.yml to stdout",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
|
||||
flags := subCmdCtx.flagSet(false)
|
||||
|
||||
daemonConfigPath := flags.StringP(
|
||||
"config-path", "c", "",
|
||||
"Optional path to a daemon.yml file to load configuration from.",
|
||||
)
|
||||
|
||||
dumpConfig := flags.Bool(
|
||||
"dump-config", false,
|
||||
"Write the default configuration file to stdout and exit.",
|
||||
)
|
||||
|
||||
domain := flags.StringP(
|
||||
"domain", "d", "",
|
||||
"Domain name that should be used as the root domain in the network.",
|
||||
)
|
||||
|
||||
ipNetStr := flags.StringP(
|
||||
"ip-net", "i", "",
|
||||
`IP+prefix (e.g. "10.10.0.1/16") which denotes the IP of this host, which will be the first host in the network, and the range of IPs which other hosts in the network can be assigned`,
|
||||
)
|
||||
|
||||
hostName := flags.StringP(
|
||||
"name", "n", "",
|
||||
"Name of this host, which will be the first host in the network",
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if *dumpConfig {
|
||||
return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath)
|
||||
}
|
||||
|
||||
if *domain == "" || *ipNetStr == "" || *hostName == "" {
|
||||
return errors.New("--domain, --ip-net, and --name are required")
|
||||
}
|
||||
|
||||
*domain = strings.TrimRight(strings.TrimLeft(*domain, "."), ".")
|
||||
|
||||
ip, subnet, err := net.ParseCIDR(*ipNetStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing %q as a CIDR: %w", *ipNetStr, err)
|
||||
}
|
||||
|
||||
if err := validateHostName(*hostName); err != nil {
|
||||
return fmt.Errorf("invalid hostname %q: %w", *hostName, err)
|
||||
}
|
||||
|
||||
runtimeDirCleanup, err := setupAndLockRuntimeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up runtime directory: %w", err)
|
||||
}
|
||||
defer runtimeDirCleanup()
|
||||
|
||||
daemonConfig, err := daemon.LoadConfig(envAppDirPath, *daemonConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading daemon config: %w", err)
|
||||
}
|
||||
|
||||
if len(daemonConfig.Storage.Allocations) < 3 {
|
||||
return fmt.Errorf("daemon config with at least 3 allocations was not provided")
|
||||
}
|
||||
|
||||
nebulaCACreds, err := nebula.NewCACredentials(*domain, subnet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating nebula CA cert: %w", err)
|
||||
}
|
||||
|
||||
nebulaHostCreds, err := nebula.NewHostCredentials(nebulaCACreds, *hostName, ip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating nebula cert for host: %w", err)
|
||||
}
|
||||
|
||||
adminCreationParams := admin.CreationParams{
|
||||
ID: randStr(32),
|
||||
Domain: *domain,
|
||||
}
|
||||
|
||||
hostBootstrap := bootstrap.Bootstrap{
|
||||
AdminCreationParams: adminCreationParams,
|
||||
Hosts: map[string]bootstrap.Host{
|
||||
*hostName: bootstrap.Host{
|
||||
Name: *hostName,
|
||||
Nebula: bootstrap.NebulaHost{
|
||||
CertPEM: nebulaHostCreds.HostCertPEM,
|
||||
},
|
||||
},
|
||||
},
|
||||
HostName: *hostName,
|
||||
}
|
||||
|
||||
hostBootstrap.Nebula.HostCredentials = nebulaHostCreds
|
||||
hostBootstrap.Garage.RPCSecret = randStr(32)
|
||||
hostBootstrap.Garage.AdminToken = randStr(32)
|
||||
hostBootstrap.Garage.GlobalBucketS3APICredentials = garage.NewS3APICredentials()
|
||||
|
||||
if hostBootstrap, err = mergeDaemonConfigIntoBootstrap(hostBootstrap, daemonConfig); err != nil {
|
||||
return fmt.Errorf("merging daemon config into bootstrap data: %w", err)
|
||||
}
|
||||
|
||||
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(hostBootstrap, daemonConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating nebula config: %w", err)
|
||||
}
|
||||
|
||||
garagePmuxProcConfigs, err := garagePmuxProcConfigs(hostBootstrap, daemonConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating garage configs: %w", err)
|
||||
}
|
||||
|
||||
pmuxConfig := pmuxlib.Config{
|
||||
Processes: append(
|
||||
[]pmuxlib.ProcessConfig{
|
||||
nebulaPmuxProcConfig,
|
||||
},
|
||||
garagePmuxProcConfigs...,
|
||||
),
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(subCmdCtx.ctx)
|
||||
pmuxDoneCh := make(chan struct{})
|
||||
|
||||
fmt.Fprintln(os.Stderr, "starting child processes")
|
||||
go func() {
|
||||
// NOTE both stdout and stderr are sent to stderr, so that the user
|
||||
// can pipe the resulting admin.yml to stdout.
|
||||
pmuxlib.Run(ctx, os.Stderr, os.Stderr, pmuxConfig)
|
||||
close(pmuxDoneCh)
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
cancel()
|
||||
fmt.Fprintln(os.Stderr, "waiting for child processes to exit")
|
||||
<-pmuxDoneCh
|
||||
}()
|
||||
|
||||
fmt.Fprintln(os.Stderr, "waiting for garage instances to come online")
|
||||
if err := waitForGarageAndNebula(ctx, hostBootstrap, daemonConfig); err != nil {
|
||||
return fmt.Errorf("waiting for garage to start up: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "applying initial garage layout")
|
||||
if err := garageApplyLayout(ctx, hostBootstrap, daemonConfig); err != nil {
|
||||
return fmt.Errorf("applying initial garage layout: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "initializing garage shared global bucket")
|
||||
err = garageInitializeGlobalBucket(ctx, hostBootstrap, daemonConfig)
|
||||
|
||||
if cErr := (garage.AdminClientError{}); errors.As(err, &cErr) && cErr.StatusCode == 409 {
|
||||
return fmt.Errorf("shared global bucket has already been created, are the storage allocations from a previously initialized cryptic-net being used?")
|
||||
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("initializing garage shared global bucket: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "cluster initialized successfully, writing admin.yml to stdout")
|
||||
|
||||
adm := admin.Admin{
|
||||
CreationParams: adminCreationParams,
|
||||
}
|
||||
adm.Nebula.CACredentials = nebulaCACreds
|
||||
adm.Garage.RPCSecret = hostBootstrap.Garage.RPCSecret
|
||||
adm.Garage.GlobalBucketS3APICredentials = hostBootstrap.Garage.GlobalBucketS3APICredentials
|
||||
|
||||
if err := adm.WriteTo(os.Stdout); err != nil {
|
||||
return fmt.Errorf("writing admin.yml to stdout")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdAdminMakeBootstrap = subCmd{
|
||||
name: "make-bootstrap",
|
||||
descr: "Creates a new bootstrap.yml 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.yml for",
|
||||
)
|
||||
|
||||
ipStr := flags.StringP(
|
||||
"ip", "i", "",
|
||||
"IP of the new host",
|
||||
)
|
||||
|
||||
adminPath := flags.StringP(
|
||||
"admin-path", "a", "",
|
||||
`Path to admin.yml 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 == "" || *ipStr == "" || *adminPath == "" {
|
||||
return errors.New("--name, --ip, and --admin-path are required")
|
||||
}
|
||||
|
||||
if err := validateHostName(*name); err != nil {
|
||||
return fmt.Errorf("invalid hostname %q: %w", *name, err)
|
||||
}
|
||||
|
||||
ip := net.ParseIP(*ipStr)
|
||||
|
||||
if ip == nil {
|
||||
return fmt.Errorf("invalid ip %q", *ipStr)
|
||||
}
|
||||
|
||||
adm, err := readAdmin(*adminPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading admin.yml with --admin-path of %q: %w", *adminPath, err)
|
||||
}
|
||||
|
||||
hostBootstrap, err := loadHostBootstrap()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||
}
|
||||
|
||||
nebulaHostCreds, err := nebula.NewHostCredentials(adm.Nebula.CACredentials, *name, ip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating new nebula host key/cert: %w", err)
|
||||
}
|
||||
|
||||
newHostBootstrap := bootstrap.Bootstrap{
|
||||
AdminCreationParams: adm.CreationParams,
|
||||
|
||||
Hosts: hostBootstrap.Hosts,
|
||||
HostName: *name,
|
||||
}
|
||||
|
||||
newHostBootstrap.Nebula.HostCredentials = nebulaHostCreds
|
||||
newHostBootstrap.Garage.RPCSecret = adm.Garage.RPCSecret
|
||||
newHostBootstrap.Garage.AdminToken = randStr(32)
|
||||
newHostBootstrap.Garage.GlobalBucketS3APICredentials = adm.Garage.GlobalBucketS3APICredentials
|
||||
|
||||
return newHostBootstrap.WriteTo(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdAdmin = subCmd{
|
||||
name: "admin",
|
||||
descr: "Sub-commands which only admins can run",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
subCmdAdminCreateNetwork,
|
||||
subCmdAdminMakeBootstrap,
|
||||
)
|
||||
},
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cryptic-net/bootstrap"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func loadHostBootstrap() (bootstrap.Bootstrap, error) {
|
||||
|
||||
dataDirPath := bootstrap.DataDirPath(envDataDirPath)
|
||||
|
||||
hostBootstrap, err := bootstrap.FromFile(dataDirPath)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return bootstrap.Bootstrap{}, errors.New("%q not found, has the daemon ever been run?")
|
||||
|
||||
} else if err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf("loading %q: %w", dataDirPath, err)
|
||||
}
|
||||
|
||||
return hostBootstrap, nil
|
||||
}
|
||||
|
||||
func writeBootstrapToDataDir(hostBootstrap bootstrap.Bootstrap) error {
|
||||
|
||||
path := bootstrap.DataDirPath(envDataDirPath)
|
||||
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)
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
return hostBootstrap.WriteTo(f)
|
||||
}
|
@ -1,305 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/daemon"
|
||||
|
||||
"code.betamike.com/cryptic-io/pmux/pmuxlib"
|
||||
)
|
||||
|
||||
// The daemon sub-command deals with starting an actual cryptic-net daemon
|
||||
// process, which is required to be running for most other cryptic-net
|
||||
// functionality. The sub-command does the following:
|
||||
//
|
||||
// * Creates and locks the runtime directory.
|
||||
//
|
||||
// * Creates the data directory and copies the appdir bootstrap file into there,
|
||||
// if it's not already there.
|
||||
//
|
||||
// * Merges daemon 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.
|
||||
|
||||
// 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 and true is returned.
|
||||
func reloadBootstrap(
|
||||
ctx context.Context,
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
) (
|
||||
bootstrap.Bootstrap, bool, error,
|
||||
) {
|
||||
|
||||
thisHost := hostBootstrap.ThisHost()
|
||||
|
||||
newHosts, err := hostBootstrap.GetGarageBootstrapHosts(ctx)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err)
|
||||
}
|
||||
|
||||
// the daemon's view of this host's bootstrap info takes precedence over
|
||||
// whatever is in garage
|
||||
newHosts[thisHost.Name] = thisHost
|
||||
|
||||
newHostsHash, err := bootstrap.HostsHash(newHosts)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, false, fmt.Errorf("calculating hash of new hosts: %w", err)
|
||||
}
|
||||
|
||||
currHostsHash, err := bootstrap.HostsHash(hostBootstrap.Hosts)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, false, fmt.Errorf("calculating hash of current hosts: %w", err)
|
||||
}
|
||||
|
||||
if bytes.Equal(newHostsHash, currHostsHash) {
|
||||
return hostBootstrap, false, nil
|
||||
}
|
||||
|
||||
hostBootstrap.Hosts = newHosts
|
||||
return hostBootstrap, true, nil
|
||||
}
|
||||
|
||||
// runs a single pmux process of 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, and returns a copy of hostBootstrap with
|
||||
// updated boostrap info.
|
||||
func runDaemonPmuxOnce(
|
||||
ctx context.Context,
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
daemonConfig daemon.Config,
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
) {
|
||||
|
||||
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(hostBootstrap, daemonConfig)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf("generating nebula config: %w", err)
|
||||
}
|
||||
|
||||
dnsmasqPmuxProcConfig, err := dnsmasqPmuxProcConfig(hostBootstrap, daemonConfig)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf("generating dnsmasq config: %w", err)
|
||||
}
|
||||
|
||||
garagePmuxProcConfigs, err := garagePmuxProcConfigs(hostBootstrap, daemonConfig)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf("generating garage children configs: %w", err)
|
||||
}
|
||||
|
||||
pmuxConfig := pmuxlib.Config{
|
||||
Processes: append(
|
||||
[]pmuxlib.ProcessConfig{
|
||||
nebulaPmuxProcConfig,
|
||||
dnsmasqPmuxProcConfig,
|
||||
},
|
||||
garagePmuxProcConfigs...,
|
||||
),
|
||||
}
|
||||
|
||||
doneCh := ctx.Done()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
pmuxlib.Run(ctx, os.Stdout, os.Stderr, pmuxConfig)
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if err := waitForGarageAndNebula(ctx, hostBootstrap, daemonConfig); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "aborted waiting for garage instances to be accessible: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err := doOnce(ctx, func(ctx context.Context) error {
|
||||
fmt.Fprintln(os.Stderr, "updating host info in garage")
|
||||
return hostBootstrap.PutGarageBoostrapHost(ctx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "aborted updating host info in garage: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if len(daemonConfig.Storage.Allocations) > 0 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if err := waitForGarageAndNebula(ctx, hostBootstrap, daemonConfig); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "aborted waiting for garage instances to be accessible: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
err := doOnce(ctx, func(ctx context.Context) error {
|
||||
fmt.Fprintln(os.Stderr, "applying garage layout")
|
||||
return garageApplyLayout(ctx, hostBootstrap, daemonConfig)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "aborted applying garage layout: %v\n", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(3 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
case <-doneCh:
|
||||
return bootstrap.Bootstrap{}, ctx.Err()
|
||||
|
||||
case <-ticker.C:
|
||||
|
||||
fmt.Fprintln(os.Stderr, "checking for changes to bootstrap")
|
||||
|
||||
var (
|
||||
changed bool
|
||||
err error
|
||||
)
|
||||
|
||||
if hostBootstrap, changed, err = reloadBootstrap(ctx, hostBootstrap); err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf("reloading bootstrap: %w", err)
|
||||
|
||||
} else if changed {
|
||||
fmt.Fprintln(os.Stderr, "bootstrap info has changed, restarting all processes")
|
||||
return hostBootstrap, 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)
|
||||
|
||||
daemonConfigPath := 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.yml 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)
|
||||
}
|
||||
|
||||
if *dumpConfig {
|
||||
return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath)
|
||||
}
|
||||
|
||||
runtimeDirCleanup, err := setupAndLockRuntimeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up runtime directory: %w", err)
|
||||
}
|
||||
defer runtimeDirCleanup()
|
||||
|
||||
var (
|
||||
bootstrapDataDirPath = bootstrap.DataDirPath(envDataDirPath)
|
||||
bootstrapAppDirPath = bootstrap.AppDirPath(envAppDirPath)
|
||||
|
||||
hostBootstrapPath string
|
||||
hostBootstrap bootstrap.Bootstrap
|
||||
foundHostBootstrap bool
|
||||
)
|
||||
|
||||
tryLoadBootstrap := func(path string) bool {
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
} else if hostBootstrap, err = bootstrap.FromFile(path); errors.Is(err, fs.ErrNotExist) {
|
||||
err = nil
|
||||
return false
|
||||
|
||||
} else if err != nil {
|
||||
err = fmt.Errorf("parsing bootstrap.yml at %q: %w", path, err)
|
||||
return false
|
||||
}
|
||||
|
||||
hostBootstrapPath = path
|
||||
return true
|
||||
}
|
||||
|
||||
foundHostBootstrap = tryLoadBootstrap(bootstrapDataDirPath)
|
||||
foundHostBootstrap = !foundHostBootstrap && *bootstrapPath != "" && tryLoadBootstrap(*bootstrapPath)
|
||||
foundHostBootstrap = !foundHostBootstrap && tryLoadBootstrap(bootstrapAppDirPath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("attempting to load bootstrap.yml file: %w", err)
|
||||
|
||||
} else if !foundHostBootstrap {
|
||||
return errors.New("No bootstrap.yml file could be found, and one is not provided with --bootstrap-path")
|
||||
|
||||
} else if hostBootstrapPath != bootstrapDataDirPath {
|
||||
|
||||
// If the bootstrap file is not being stored in the data dir, copy
|
||||
// it there, so it can be loaded from there next time.
|
||||
if err := writeBootstrapToDataDir(hostBootstrap); err != nil {
|
||||
return fmt.Errorf("writing bootstrap.yml to data dir: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
daemonConfig, err := daemon.LoadConfig(envAppDirPath, *daemonConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading daemon config: %w", err)
|
||||
}
|
||||
|
||||
// we update this Host's data using whatever configuration has been
|
||||
// provided by the daemon config. This way the daemon has the most
|
||||
// up-to-date possible bootstrap. This updated bootstrap will later get
|
||||
// updated in garage using bootstrap.PutGarageBoostrapHost, so other
|
||||
// hosts will see it as well.
|
||||
if hostBootstrap, err = mergeDaemonConfigIntoBootstrap(hostBootstrap, daemonConfig); err != nil {
|
||||
return fmt.Errorf("merging daemon config into bootstrap data: %w", err)
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
hostBootstrap, err = runDaemonPmuxOnce(subCmdCtx.ctx, hostBootstrap, daemonConfig)
|
||||
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("running pmux for daemon: %w", err)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/daemon"
|
||||
"cryptic-net/garage"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
func mergeDaemonConfigIntoBootstrap(
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
daemonConfig daemon.Config,
|
||||
) (
|
||||
bootstrap.Bootstrap, error,
|
||||
) {
|
||||
host := hostBootstrap.ThisHost()
|
||||
|
||||
host.Nebula.PublicAddr = daemonConfig.VPN.PublicAddr
|
||||
|
||||
host.Garage = nil
|
||||
|
||||
if allocs := daemonConfig.Storage.Allocations; len(allocs) > 0 {
|
||||
|
||||
host.Garage = new(bootstrap.GarageHost)
|
||||
|
||||
for _, alloc := range allocs {
|
||||
|
||||
id, err := garage.InitAlloc(alloc.MetaPath)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf("initializing alloc at %q: %w", alloc.MetaPath, err)
|
||||
}
|
||||
|
||||
host.Garage.Instances = append(host.Garage.Instances, bootstrap.GarageHostInstance{
|
||||
ID: id,
|
||||
RPCPort: alloc.RPCPort,
|
||||
S3APIPort: alloc.S3APIPort,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
hostBootstrap.Hosts[host.Name] = host
|
||||
|
||||
if err := writeBootstrapToDataDir(hostBootstrap); err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf("writing bootstrap file: %w", err)
|
||||
}
|
||||
|
||||
return hostBootstrap, nil
|
||||
}
|
||||
|
||||
func doOnce(ctx context.Context, fn func(context.Context) error) error {
|
||||
for {
|
||||
if err := fn(ctx); err == nil {
|
||||
return nil
|
||||
} else if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/daemon"
|
||||
"cryptic-net/dnsmasq"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"code.betamike.com/cryptic-io/pmux/pmuxlib"
|
||||
)
|
||||
|
||||
func dnsmasqPmuxProcConfig(
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
daemonConfig daemon.Config,
|
||||
) (
|
||||
pmuxlib.ProcessConfig, error,
|
||||
) {
|
||||
|
||||
confPath := filepath.Join(envRuntimeDirPath, "dnsmasq.conf")
|
||||
|
||||
hostsSlice := make([]dnsmasq.ConfDataHost, 0, len(hostBootstrap.Hosts))
|
||||
for _, host := range hostBootstrap.Hosts {
|
||||
hostsSlice = append(hostsSlice, dnsmasq.ConfDataHost{
|
||||
Name: host.Name,
|
||||
IP: host.IP().String(),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(hostsSlice, func(i, j int) bool {
|
||||
return hostsSlice[i].IP < hostsSlice[j].IP
|
||||
})
|
||||
|
||||
confData := dnsmasq.ConfData{
|
||||
Resolvers: daemonConfig.DNS.Resolvers,
|
||||
Domain: hostBootstrap.AdminCreationParams.Domain,
|
||||
IP: hostBootstrap.ThisHost().IP().String(),
|
||||
Hosts: hostsSlice,
|
||||
}
|
||||
|
||||
if err := dnsmasq.WriteConfFile(confPath, confData); err != nil {
|
||||
return pmuxlib.ProcessConfig{}, fmt.Errorf("writing dnsmasq.conf to %q: %w", confPath, err)
|
||||
}
|
||||
|
||||
return pmuxlib.ProcessConfig{
|
||||
Name: "dnsmasq",
|
||||
Cmd: "dnsmasq",
|
||||
Args: []string{"-d", "-C", confPath},
|
||||
}, nil
|
||||
}
|
@ -1,288 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/daemon"
|
||||
"cryptic-net/garage"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"code.betamike.com/cryptic-io/pmux/pmuxlib"
|
||||
)
|
||||
|
||||
// newGarageAdminClient will return an AdminClient for a local garage instance,
|
||||
// or it will _panic_ if there is no local instance configured.
|
||||
func newGarageAdminClient(
|
||||
hostBootstrap bootstrap.Bootstrap, daemonConfig daemon.Config,
|
||||
) *garage.AdminClient {
|
||||
|
||||
thisHost := hostBootstrap.ThisHost()
|
||||
|
||||
return garage.NewAdminClient(
|
||||
net.JoinHostPort(
|
||||
thisHost.IP().String(),
|
||||
strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort),
|
||||
),
|
||||
hostBootstrap.Garage.AdminToken,
|
||||
)
|
||||
}
|
||||
|
||||
func waitForGarageAndNebula(
|
||||
ctx context.Context,
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
daemonConfig daemon.Config,
|
||||
) error {
|
||||
|
||||
allocs := daemonConfig.Storage.Allocations
|
||||
|
||||
// if this host doesn't have any allocations specified then fall back to
|
||||
// waiting for nebula
|
||||
if len(allocs) == 0 {
|
||||
return waitForNebula(ctx, hostBootstrap)
|
||||
}
|
||||
|
||||
for _, alloc := range allocs {
|
||||
|
||||
adminAddr := net.JoinHostPort(
|
||||
hostBootstrap.ThisHost().IP().String(),
|
||||
strconv.Itoa(alloc.AdminPort),
|
||||
)
|
||||
|
||||
adminClient := garage.NewAdminClient(
|
||||
adminAddr,
|
||||
hostBootstrap.Garage.AdminToken,
|
||||
)
|
||||
|
||||
if err := adminClient.Wait(ctx); err != nil {
|
||||
return fmt.Errorf("waiting for instance %q to start up: %w", adminAddr, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// bootstrapGarageHostForAlloc returns the bootstrap.GarageHostInstance which
|
||||
// corresponds with the given alloc from the daemon config. This will panic if
|
||||
// no associated instance can be found.
|
||||
//
|
||||
// This assumes that mergeDaemonConfigIntoBootstrap has already been called.
|
||||
func bootstrapGarageHostForAlloc(
|
||||
host bootstrap.Host,
|
||||
alloc daemon.ConfigStorageAllocation,
|
||||
) bootstrap.GarageHostInstance {
|
||||
|
||||
for _, inst := range host.Garage.Instances {
|
||||
if inst.RPCPort == alloc.RPCPort {
|
||||
return inst
|
||||
}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("could not find alloc %+v in the bootstrap data", alloc))
|
||||
}
|
||||
|
||||
func garageWriteChildConfig(
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
alloc daemon.ConfigStorageAllocation,
|
||||
) (
|
||||
string, error,
|
||||
) {
|
||||
|
||||
thisHost := hostBootstrap.ThisHost()
|
||||
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID
|
||||
|
||||
peer := garage.LocalPeer{
|
||||
RemotePeer: garage.RemotePeer{
|
||||
ID: id,
|
||||
IP: thisHost.IP().String(),
|
||||
RPCPort: alloc.RPCPort,
|
||||
S3APIPort: alloc.S3APIPort,
|
||||
},
|
||||
AdminPort: alloc.AdminPort,
|
||||
}
|
||||
|
||||
garageTomlPath := filepath.Join(
|
||||
envRuntimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort),
|
||||
)
|
||||
|
||||
err := garage.WriteGarageTomlFile(garageTomlPath, garage.GarageTomlData{
|
||||
MetaPath: alloc.MetaPath,
|
||||
DataPath: alloc.DataPath,
|
||||
|
||||
RPCSecret: hostBootstrap.Garage.RPCSecret,
|
||||
AdminToken: hostBootstrap.Garage.AdminToken,
|
||||
|
||||
RPCAddr: peer.RPCAddr(),
|
||||
S3APIAddr: peer.S3APIAddr(),
|
||||
AdminAddr: peer.AdminAddr(),
|
||||
|
||||
BootstrapPeers: hostBootstrap.GarageRPCPeerAddrs(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating garage.toml file at %q: %w", garageTomlPath, err)
|
||||
}
|
||||
|
||||
return garageTomlPath, nil
|
||||
}
|
||||
|
||||
func garagePmuxProcConfigs(
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
daemonConfig daemon.Config,
|
||||
) (
|
||||
[]pmuxlib.ProcessConfig, error,
|
||||
) {
|
||||
|
||||
var pmuxProcConfigs []pmuxlib.ProcessConfig
|
||||
|
||||
for _, alloc := range daemonConfig.Storage.Allocations {
|
||||
|
||||
childConfigPath, err := garageWriteChildConfig(hostBootstrap, alloc)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
|
||||
}
|
||||
|
||||
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
|
||||
Name: fmt.Sprintf("garage-%d", alloc.RPCPort),
|
||||
Cmd: "garage",
|
||||
Args: []string{"-c", childConfigPath, "server"},
|
||||
StartAfterFunc: func(ctx context.Context) error {
|
||||
return waitForNebula(ctx, hostBootstrap)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return pmuxProcConfigs, nil
|
||||
}
|
||||
|
||||
func garageInitializeGlobalBucket(
|
||||
ctx context.Context,
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
daemonConfig daemon.Config,
|
||||
) error {
|
||||
|
||||
var (
|
||||
adminClient = newGarageAdminClient(hostBootstrap, daemonConfig)
|
||||
globalBucketCreds = hostBootstrap.Garage.GlobalBucketS3APICredentials
|
||||
)
|
||||
|
||||
// first attempt to import the key
|
||||
err := adminClient.Do(ctx, nil, "POST", "/v0/key/import", map[string]string{
|
||||
"accessKeyId": globalBucketCreds.ID,
|
||||
"secretAccessKey": globalBucketCreds.Secret,
|
||||
"name": "shared-global-bucket-key",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("importing global bucket key into garage: %w", err)
|
||||
}
|
||||
|
||||
// create global bucket
|
||||
err = adminClient.Do(ctx, nil, "POST", "/v0/bucket", map[string]string{
|
||||
"globalAlias": garage.GlobalBucket,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating global bucket: %w", err)
|
||||
}
|
||||
|
||||
// retrieve newly created bucket's id
|
||||
var getBucketRes struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
err = adminClient.Do(
|
||||
ctx, &getBucketRes,
|
||||
"GET", "/v0/bucket?globalAlias="+garage.GlobalBucket, nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching global bucket id: %w", err)
|
||||
}
|
||||
|
||||
// allow shared global bucket key to perform all operations
|
||||
err = adminClient.Do(ctx, nil, "POST", "/v0/bucket/allow", map[string]interface{}{
|
||||
"bucketId": getBucketRes.ID,
|
||||
"accessKeyId": globalBucketCreds.ID,
|
||||
"permissions": map[string]bool{
|
||||
"read": true,
|
||||
"write": true,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("granting permissions to shared global bucket key: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func garageApplyLayout(
|
||||
ctx context.Context,
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
daemonConfig daemon.Config,
|
||||
) error {
|
||||
|
||||
var (
|
||||
adminClient = newGarageAdminClient(hostBootstrap, daemonConfig)
|
||||
thisHost = hostBootstrap.ThisHost()
|
||||
hostName = thisHost.Name
|
||||
allocs = daemonConfig.Storage.Allocations
|
||||
)
|
||||
|
||||
type peerLayout struct {
|
||||
Capacity int `json:"capacity"`
|
||||
Zone string `json:"zone"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
{
|
||||
clusterLayout := map[string]peerLayout{}
|
||||
|
||||
for _, alloc := range allocs {
|
||||
|
||||
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID
|
||||
|
||||
clusterLayout[id] = peerLayout{
|
||||
Capacity: alloc.Capacity / 100,
|
||||
Zone: hostName,
|
||||
Tags: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
err := adminClient.Do(ctx, nil, "POST", "/v0/layout", clusterLayout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("staging layout changes: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
var clusterLayout struct {
|
||||
Version int `json:"version"`
|
||||
StagedRoleChanges map[string]peerLayout `json:"stagedRoleChanges"`
|
||||
}
|
||||
|
||||
if err := adminClient.Do(ctx, &clusterLayout, "GET", "/v0/layout", nil); err != nil {
|
||||
return fmt.Errorf("retrieving staged layout change: %w", err)
|
||||
}
|
||||
|
||||
if len(clusterLayout.StagedRoleChanges) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
applyClusterLayout := struct {
|
||||
Version int `json:"version"`
|
||||
}{
|
||||
Version: clusterLayout.Version + 1,
|
||||
}
|
||||
|
||||
err := adminClient.Do(ctx, nil, "POST", "/v0/layout/apply", applyClusterLayout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("applying new layout (new version:%d): %w", applyClusterLayout.Version, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
)
|
||||
|
||||
// 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 getAppDirPath() string {
|
||||
appDirPath := os.Getenv("APPDIR")
|
||||
if appDirPath == "" {
|
||||
appDirPath = "."
|
||||
}
|
||||
return appDirPath
|
||||
}
|
||||
|
||||
var (
|
||||
envAppDirPath = getAppDirPath()
|
||||
envRuntimeDirPath = filepath.Join(xdg.RuntimeDir, "cryptic-net")
|
||||
envDataDirPath = filepath.Join(xdg.DataHome, "cryptic-net")
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
ctx, 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)
|
||||
}()
|
||||
|
||||
err := subCmdCtx{
|
||||
args: os.Args[1:],
|
||||
ctx: ctx,
|
||||
}.doSubCmd(
|
||||
subCmdAdmin,
|
||||
subCmdDaemon,
|
||||
subCmdGarage,
|
||||
subCmdHosts,
|
||||
subCmdVersion,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/daemon"
|
||||
"cryptic-net/yamlutil"
|
||||
"fmt"
|
||||
"net"
|
||||
"path/filepath"
|
||||
|
||||
"code.betamike.com/cryptic-io/pmux/pmuxlib"
|
||||
)
|
||||
|
||||
// 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, hostBootstrap bootstrap.Bootstrap) error {
|
||||
|
||||
ip := hostBootstrap.ThisHost().IP()
|
||||
|
||||
lUdpAddr := &net.UDPAddr{IP: ip, Port: 0}
|
||||
rUdpAddr := &net.UDPAddr{IP: ip, Port: 45535}
|
||||
|
||||
return doOnce(ctx, func(context.Context) error {
|
||||
conn, err := net.DialUDP("udp", lUdpAddr, rUdpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn.Close()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func nebulaPmuxProcConfig(
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
daemonConfig daemon.Config,
|
||||
) (
|
||||
pmuxlib.ProcessConfig, 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}
|
||||
}
|
||||
|
||||
config := map[string]interface{}{
|
||||
"pki": map[string]string{
|
||||
"ca": hostBootstrap.Nebula.HostCredentials.CACertPEM,
|
||||
"cert": hostBootstrap.Nebula.HostCredentials.HostCertPEM,
|
||||
"key": hostBootstrap.Nebula.HostCredentials.HostKeyPEM,
|
||||
},
|
||||
"static_host_map": staticHostMap,
|
||||
"punchy": map[string]bool{
|
||||
"punch": true,
|
||||
"respond": true,
|
||||
},
|
||||
"tun": map[string]interface{}{
|
||||
"dev": "cryptic-net-nebula",
|
||||
},
|
||||
"firewall": daemonConfig.VPN.Firewall,
|
||||
}
|
||||
|
||||
if publicAddr := daemonConfig.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 {
|
||||
return pmuxlib.ProcessConfig{}, fmt.Errorf("parsing public address %q: %w", publicAddr, err)
|
||||
}
|
||||
|
||||
config["listen"] = map[string]string{
|
||||
"host": "0.0.0.0",
|
||||
"port": port,
|
||||
}
|
||||
|
||||
config["lighthouse"] = map[string]interface{}{
|
||||
"hosts": []string{},
|
||||
"am_lighthouse": true,
|
||||
}
|
||||
}
|
||||
|
||||
nebulaYmlPath := filepath.Join(envRuntimeDirPath, "nebula.yml")
|
||||
|
||||
if err := yamlutil.WriteYamlFile(config, nebulaYmlPath); err != nil {
|
||||
return pmuxlib.ProcessConfig{}, fmt.Errorf("writing nebula.yml to %q: %w", nebulaYmlPath, err)
|
||||
}
|
||||
|
||||
return pmuxlib.ProcessConfig{
|
||||
Name: "nebula",
|
||||
Cmd: "nebula",
|
||||
Args: []string{"-config", nebulaYmlPath},
|
||||
}, nil
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
// Package daemon contains types and functions related specifically to the
|
||||
// cryptic-net daemon.
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"cryptic-net/yamlutil"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func defaultConfigPath(appDirPath string) string {
|
||||
return filepath.Join(appDirPath, "etc", "daemon.yml")
|
||||
}
|
||||
|
||||
// CopyDefaultConfig copies the daemon config file embedded in the AppDir into
|
||||
// the given io.Writer.
|
||||
func CopyDefaultConfig(into io.Writer, appDirPath string) error {
|
||||
|
||||
defaultConfigPath := defaultConfigPath(appDirPath)
|
||||
|
||||
f, err := os.Open(defaultConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening daemon config at %q: %w", defaultConfigPath, err)
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(into, f); err != nil {
|
||||
return fmt.Errorf("copying daemon config from %q: %w", defaultConfigPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadConfig loads the daemon config from userConfigPath, merges it with
|
||||
// the default found in the appDirPath, and returns the result.
|
||||
//
|
||||
// If userConfigPath is not given then the default is loaded and returned.
|
||||
func LoadConfig(
|
||||
appDirPath, userConfigPath string,
|
||||
) (
|
||||
Config, error,
|
||||
) {
|
||||
|
||||
defaultConfigPath := defaultConfigPath(appDirPath)
|
||||
|
||||
var fullDaemon map[string]interface{}
|
||||
|
||||
if err := yamlutil.LoadYamlFile(&fullDaemon, defaultConfigPath); err != nil {
|
||||
return Config{}, fmt.Errorf("parsing default daemon config file: %w", err)
|
||||
}
|
||||
|
||||
if userConfigPath != "" {
|
||||
|
||||
var daemonConfig map[string]interface{}
|
||||
if err := yamlutil.LoadYamlFile(&daemonConfig, userConfigPath); err != nil {
|
||||
return Config{}, fmt.Errorf("parsing %q: %w", userConfigPath, err)
|
||||
}
|
||||
|
||||
err := mergo.Merge(&fullDaemon, daemonConfig, mergo.WithOverride)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("merging contents of file %q: %w", userConfigPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
fullDaemonB, err := yaml.Marshal(fullDaemon)
|
||||
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("yaml marshaling: %w", err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(fullDaemonB, &config); err != nil {
|
||||
return Config{}, fmt.Errorf("yaml unmarshaling back into Config struct: %w", err)
|
||||
}
|
||||
|
||||
config.fillDefaults()
|
||||
|
||||
return config, nil
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
// Package dnsmasq contains helper functions and types which are useful for
|
||||
// setting up dnsmasq configs, processes, and deployments.
|
||||
package dnsmasq
|
@ -1,63 +0,0 @@
|
||||
package dnsmasq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// ConfDataHost describes a host which can be resolved by dnsmasq.
|
||||
type ConfDataHost struct {
|
||||
Name string
|
||||
IP string
|
||||
}
|
||||
|
||||
// ConfData describes all the data needed to populate a dnsmasq.conf file.
|
||||
type ConfData struct {
|
||||
Resolvers []string
|
||||
Domain string
|
||||
IP string
|
||||
Hosts []ConfDataHost
|
||||
}
|
||||
|
||||
var confTpl = template.Must(template.New("").Parse(`
|
||||
port=53
|
||||
|
||||
bind-interfaces
|
||||
listen-address={{ .IP }}
|
||||
|
||||
no-resolv
|
||||
no-hosts
|
||||
|
||||
user=
|
||||
group=
|
||||
|
||||
{{- range $host := .Hosts }}
|
||||
address=/{{ $host.Name }}.hosts.{{ .Domain }}/{{ $host.Nebula.IP }}
|
||||
{{ end -}}
|
||||
|
||||
{{- range .Resolvers }}
|
||||
server={{ . }}
|
||||
{{ end -}}
|
||||
`))
|
||||
|
||||
// WriteConfFile renders a dnsmasq.conf using the given data to a new
|
||||
// file at the given path.
|
||||
func WriteConfFile(path string, data ConfData) 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()
|
||||
|
||||
if err := confTpl.Execute(file, data); err != nil {
|
||||
return fmt.Errorf("rendering template to file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
package garage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AdminClientError gets returned from AdminClient's Do method for non-200
|
||||
// errors.
|
||||
type AdminClientError struct {
|
||||
StatusCode int
|
||||
Body []byte
|
||||
}
|
||||
|
||||
func (e AdminClientError) Error() string {
|
||||
return fmt.Sprintf("%d response from admin: %q", e.StatusCode, e.Body)
|
||||
}
|
||||
|
||||
// AdminClient is a helper type for performing actions against the garage admin
|
||||
// interface.
|
||||
type AdminClient struct {
|
||||
c *http.Client
|
||||
addr string
|
||||
adminToken string
|
||||
}
|
||||
|
||||
// NewAdminClient initializes and returns an AdminClient which will use the
|
||||
// given address and adminToken for all requests made.
|
||||
func NewAdminClient(addr, adminToken string) *AdminClient {
|
||||
return &AdminClient{
|
||||
c: &http.Client{
|
||||
Transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||
},
|
||||
addr: addr,
|
||||
adminToken: adminToken,
|
||||
}
|
||||
}
|
||||
|
||||
// Do performs an HTTP request with the given method (GET, POST) and path, and
|
||||
// using the json marshaling of the given body as the request body (unless body
|
||||
// is nil). It will JSON unmarshal the response into rcv, unless rcv is nil.
|
||||
func (c *AdminClient) Do(
|
||||
ctx context.Context, rcv interface{}, method, path string, body interface{},
|
||||
) error {
|
||||
|
||||
var bodyR io.Reader
|
||||
|
||||
if body != nil {
|
||||
bodyBuf := new(bytes.Buffer)
|
||||
bodyR = bodyBuf
|
||||
|
||||
if err := json.NewEncoder(bodyBuf).Encode(body); err != nil {
|
||||
return fmt.Errorf("json marshaling body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
urlStr := fmt.Sprintf("http://%s%s", c.addr, path)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, urlStr, bodyR)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+c.adminToken)
|
||||
|
||||
res, err := c.c.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("performing http request: %w", err)
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
return AdminClientError{
|
||||
StatusCode: res.StatusCode,
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
|
||||
if rcv == nil {
|
||||
|
||||
if _, err := io.Copy(io.Discard, res.Body); err != nil {
|
||||
return fmt.Errorf("discarding response body: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(res.Body).Decode(rcv); err != nil {
|
||||
return fmt.Errorf("decoding json response body: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wait will block until the instance connected to can see at least
|
||||
// ReplicationFactor-1 other garage instances. If the context is canceled it
|
||||
// will return the context error.
|
||||
func (c *AdminClient) Wait(ctx context.Context) error {
|
||||
for {
|
||||
|
||||
var clusterStatus struct {
|
||||
KnownNodes map[string]struct {
|
||||
IsUp bool `json:"is_up"`
|
||||
} `json:"knownNodes"`
|
||||
}
|
||||
|
||||
err := c.Do(ctx, &clusterStatus, "GET", "/v0/status", nil)
|
||||
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
|
||||
} else if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var numUp int
|
||||
|
||||
for _, knownNode := range clusterStatus.KnownNodes {
|
||||
if knownNode.IsUp {
|
||||
numUp++
|
||||
}
|
||||
}
|
||||
|
||||
if numUp >= ReplicationFactor-1 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
// Package garage contains helper functions and types which are useful for
|
||||
// setting up garage configs, processes, and deployments.
|
||||
package garage
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
// ReplicationFactor indicates the replication factor set on the garage
|
||||
// cluster. We currently only support a factor of 3.
|
||||
ReplicationFactor = 3
|
||||
)
|
||||
|
||||
func nodeKeyPath(metaDirPath string) string {
|
||||
return filepath.Join(metaDirPath, "node_key")
|
||||
}
|
||||
|
||||
func nodeKeyPubPath(metaDirPath string) string {
|
||||
return filepath.Join(metaDirPath, "node_key.pub")
|
||||
}
|
||||
|
||||
// LoadAllocID returns the peer ID (ie the public key) of the node at the given
|
||||
// meta directory.
|
||||
func LoadAllocID(metaDirPath string) (string, error) {
|
||||
nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
|
||||
|
||||
pubKey, err := os.ReadFile(nodeKeyPubPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading %q: %w", nodeKeyPubPath, err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(pubKey), nil
|
||||
}
|
||||
|
||||
// InitAlloc initializes the meta directory and keys for a particular
|
||||
// allocation, if it hasn't been done so already. It returns the peer ID (ie the
|
||||
// public key) in any case.
|
||||
func InitAlloc(metaDirPath string) (string, error) {
|
||||
|
||||
var err error
|
||||
|
||||
exists := func(path string) bool {
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
} else if _, err = os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
||||
err = nil
|
||||
return false
|
||||
|
||||
} else if err != nil {
|
||||
err = fmt.Errorf("checking if %q exists: %w", path, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
nodeKeyPath := nodeKeyPath(metaDirPath)
|
||||
nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
|
||||
|
||||
nodeKeyPathExists := exists(nodeKeyPath)
|
||||
nodeKeyPubPathExists := exists(nodeKeyPubPath)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
} else if nodeKeyPubPathExists != nodeKeyPathExists {
|
||||
return "", fmt.Errorf("%q or %q exist without the other existing", nodeKeyPath, nodeKeyPubPath)
|
||||
|
||||
} else if nodeKeyPathExists {
|
||||
return LoadAllocID(metaDirPath)
|
||||
}
|
||||
|
||||
// node key hasn't been written, write it
|
||||
|
||||
if err := os.MkdirAll(metaDirPath, 0750); err != nil {
|
||||
return "", fmt.Errorf("making directory %q: %w", metaDirPath, err)
|
||||
}
|
||||
|
||||
pubKey, privKey := GeneratePeerKey()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(pubKey), nil
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package garage
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// RemotePeer describes all information necessary to connect to a given garage
|
||||
// node.
|
||||
type RemotePeer struct {
|
||||
ID string
|
||||
IP string
|
||||
RPCPort int
|
||||
S3APIPort int
|
||||
}
|
||||
|
||||
// LocalPeer describes the configuration of a local garage instance.
|
||||
type LocalPeer struct {
|
||||
RemotePeer
|
||||
|
||||
AdminPort int
|
||||
}
|
||||
|
||||
// GeneratePeerKey generates and returns a public/private key pair for a garage
|
||||
// instance.
|
||||
func GeneratePeerKey() (pubKey, privKey []byte) {
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return pubKey, privKey
|
||||
}
|
||||
|
||||
// RPCAddr returns the address of the peer's RPC port.
|
||||
func (p RemotePeer) 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.
|
||||
func (p RemotePeer) RPCPeerAddr() string {
|
||||
return fmt.Sprintf("%s@%s", p.ID, p.RPCAddr())
|
||||
}
|
||||
|
||||
// S3APIAddr returns the address of the peer's S3 API port.
|
||||
func (p RemotePeer) S3APIAddr() string {
|
||||
return net.JoinHostPort(p.IP, strconv.Itoa(p.S3APIPort))
|
||||
}
|
||||
|
||||
// AdminAddr returns the address of the peer's S3 API port.
|
||||
func (p LocalPeer) AdminAddr() string {
|
||||
return net.JoinHostPort(p.IP, strconv.Itoa(p.AdminPort))
|
||||
}
|
@ -1,272 +0,0 @@
|
||||
// Package nebula contains helper functions and types which are useful for
|
||||
// setting up nebula configs, processes, and deployments.
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/slackhq/nebula/cert"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
// ErrInvalidSignature is returned from functions when a signature validation
|
||||
// fails.
|
||||
var ErrInvalidSignature = errors.New("invalid signature")
|
||||
|
||||
// HostCredentials contains the certificate and private key files which will
|
||||
// need to be present on a particular host. Each file is PEM encoded.
|
||||
type HostCredentials struct {
|
||||
CACertPEM string `yaml:"ca_cert_pem"`
|
||||
HostKeyPEM string `yaml:"host_key_pem"`
|
||||
HostCertPEM string `yaml:"host_cert_pem"`
|
||||
}
|
||||
|
||||
// CACredentials contains the certificate and private files which can be used to
|
||||
// create and validate HostCredentials. Each file is PEM encoded.
|
||||
type CACredentials struct {
|
||||
CACertPEM string `yaml:"ca_cert_pem"`
|
||||
CAKeyPEM string `yaml:"ca_key_pem"`
|
||||
}
|
||||
|
||||
// NewHostCredentials generates a new key/cert for a nebula host using the CA
|
||||
// key which will be found in the adminFS.
|
||||
func NewHostCredentials(
|
||||
caCreds CACredentials, hostName string, ip net.IP,
|
||||
) (
|
||||
HostCredentials, 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(caCreds.CAKeyPEM))
|
||||
if err != nil {
|
||||
return HostCredentials{}, fmt.Errorf("unmarshaling ca.key: %w", err)
|
||||
}
|
||||
|
||||
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.CACertPEM))
|
||||
if err != nil {
|
||||
return HostCredentials{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
|
||||
}
|
||||
|
||||
issuer, err := caCert.Sha256Sum()
|
||||
if err != nil {
|
||||
return HostCredentials{}, fmt.Errorf("getting ca.crt issuer: %w", err)
|
||||
}
|
||||
|
||||
expireAt := caCert.Details.NotAfter.Add(-1 * time.Second)
|
||||
|
||||
subnet := caCert.Details.Subnets[0]
|
||||
if !subnet.Contains(ip) {
|
||||
return HostCredentials{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet)
|
||||
}
|
||||
|
||||
var hostPub, hostKey []byte
|
||||
{
|
||||
var pubkey, privkey [32]byte
|
||||
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
|
||||
return HostCredentials{}, fmt.Errorf("reading random bytes to form private key: %w", err)
|
||||
}
|
||||
curve25519.ScalarBaseMult(&pubkey, &privkey)
|
||||
hostPub, hostKey = pubkey[:], privkey[:]
|
||||
}
|
||||
|
||||
hostCert := cert.NebulaCertificate{
|
||||
Details: cert.NebulaCertificateDetails{
|
||||
Name: hostName,
|
||||
Ips: []*net.IPNet{{
|
||||
IP: ip,
|
||||
Mask: subnet.Mask,
|
||||
}},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: expireAt,
|
||||
PublicKey: hostPub,
|
||||
IsCA: false,
|
||||
Issuer: issuer,
|
||||
},
|
||||
}
|
||||
|
||||
if err := hostCert.CheckRootConstrains(caCert); err != nil {
|
||||
return HostCredentials{}, fmt.Errorf("validating certificate constraints: %w", err)
|
||||
}
|
||||
|
||||
if err := hostCert.Sign(caKey); err != nil {
|
||||
return HostCredentials{}, fmt.Errorf("signing host cert with ca.key: %w", err)
|
||||
}
|
||||
|
||||
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
|
||||
|
||||
hostCertPEM, err := hostCert.MarshalToPEM()
|
||||
if err != nil {
|
||||
return HostCredentials{}, fmt.Errorf("marshalling host.crt: %w", err)
|
||||
}
|
||||
|
||||
return HostCredentials{
|
||||
CACertPEM: caCreds.CACertPEM,
|
||||
HostKeyPEM: string(hostKeyPEM),
|
||||
HostCertPEM: string(hostCertPEM),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCACredentials generates a CACredentials. The domain should be the network's root domain,
|
||||
// and is included in the signing certificate's Name field.
|
||||
func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, 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)
|
||||
|
||||
caCert := cert.NebulaCertificate{
|
||||
Details: cert.NebulaCertificateDetails{
|
||||
Name: fmt.Sprintf("%s cryptic-net root cert", domain),
|
||||
Subnets: []*net.IPNet{subnet},
|
||||
NotBefore: now,
|
||||
NotAfter: expireAt,
|
||||
PublicKey: pubKey,
|
||||
IsCA: true,
|
||||
},
|
||||
}
|
||||
|
||||
if err := caCert.Sign(privKey); err != nil {
|
||||
return CACredentials{}, fmt.Errorf("signing caCert: %w", err)
|
||||
}
|
||||
|
||||
caKeyPEM := cert.MarshalEd25519PrivateKey(privKey)
|
||||
|
||||
caCertPEM, err := caCert.MarshalToPEM()
|
||||
if err != nil {
|
||||
return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err)
|
||||
}
|
||||
|
||||
return CACredentials{
|
||||
CACertPEM: string(caCertPEM),
|
||||
CAKeyPEM: string(caKeyPEM),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateHostCertPEM checks if the given host certificate was signed by the
|
||||
// given CA certificate, and returns ErrInvalidSignature if validation fails.
|
||||
func ValidateHostCertPEM(caCertPEM, hostCertPEM string) error {
|
||||
|
||||
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCertPEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshaling CA certificate as PEM: %w", err)
|
||||
}
|
||||
|
||||
hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(hostCertPEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshaling host certificate as PEM: %w", err)
|
||||
}
|
||||
|
||||
caPubKey := ed25519.PublicKey(caCert.Details.PublicKey)
|
||||
|
||||
if !hostCert.CheckSignature(caPubKey) {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPFromHostCertPEM is a convenience function for parsing the IP of a host out
|
||||
// of its nebula cert.
|
||||
func IPFromHostCertPEM(hostCertPEM string) (net.IP, error) {
|
||||
|
||||
hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(hostCertPEM))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling host certificate as PEM: %w", err)
|
||||
}
|
||||
|
||||
ips := hostCert.Details.Ips
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("malformed nebula host cert: no IPs")
|
||||
}
|
||||
|
||||
return ips[0].IP, nil
|
||||
}
|
||||
|
||||
// SignAndWrap signs the given bytes using the keyPEM, and writes an
|
||||
// encoded, versioned structure containing the signature and the given bytes.
|
||||
func SignAndWrap(into io.Writer, keyPEM string, b []byte) error {
|
||||
|
||||
key, _, err := cert.UnmarshalEd25519PrivateKey([]byte(keyPEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshaling private key: %w", err)
|
||||
}
|
||||
|
||||
sig, err := key.Sign(rand.Reader, b, crypto.Hash(0))
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating signature: %w", err)
|
||||
}
|
||||
|
||||
if _, err := into.Write([]byte("0")); err != nil {
|
||||
return fmt.Errorf("writing version byte: %w", err)
|
||||
}
|
||||
|
||||
err = pem.Encode(into, &pem.Block{
|
||||
Type: "SIGNATURE",
|
||||
Bytes: sig,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing PEM encoding of signature: %w", err)
|
||||
}
|
||||
|
||||
if _, err := into.Write(b); err != nil {
|
||||
return fmt.Errorf("writing input bytes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unwrap reads a stream of bytes which was produced by SignAndWrap, and returns
|
||||
// the original inpute to SignAndWrap as well as the signature which was
|
||||
// created. ValidateSignature can be used to validate the signature.
|
||||
func Unwrap(from io.Reader) (b, sig []byte, err error) {
|
||||
|
||||
full, err := io.ReadAll(from)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("reading full input: %w", err)
|
||||
} else if len(full) < 3 {
|
||||
return nil, nil, fmt.Errorf("input too small")
|
||||
} else if full[0] != '0' {
|
||||
return nil, nil, fmt.Errorf("unexpected version byte: %d", full[0])
|
||||
}
|
||||
|
||||
full = full[1:]
|
||||
|
||||
pemBlock, rest := pem.Decode(full)
|
||||
if pemBlock == nil {
|
||||
return nil, nil, fmt.Errorf("PEM-encoded signature could not be decoded")
|
||||
}
|
||||
|
||||
return rest, pemBlock.Bytes, nil
|
||||
}
|
||||
|
||||
// ValidateSignature can be used to validate a signature produced by Unwrap.
|
||||
func ValidateSignature(certPEM string, b, sig []byte) error {
|
||||
|
||||
cert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(certPEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshaling certificate as PEM: %w", err)
|
||||
}
|
||||
|
||||
pubKey := ed25519.PublicKey(cert.Details.PublicKey)
|
||||
|
||||
if !ed25519.Verify(pubKey, b, sig) {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
ip net.IP
|
||||
ipNet *net.IPNet
|
||||
caCredsA, caCredsB CACredentials
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
ip, ipNet, err = net.ParseCIDR("192.168.0.1/24")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
caCredsA, err = NewCACredentials("a.example.com", ipNet)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
caCredsB, err = NewCACredentials("b.example.com", ipNet)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateHostCredentials(t *testing.T) {
|
||||
|
||||
hostCreds, err := NewHostCredentials(caCredsA, "foo", ip)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ValidateHostCertPEM(hostCreds.CACertPEM, hostCreds.HostCertPEM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ValidateHostCertPEM(caCredsB.CACertPEM, hostCreds.HostCertPEM)
|
||||
if !errors.Is(err, ErrInvalidSignature) {
|
||||
t.Fatalf("expected ErrInvalidSignature, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignAndWrap(t *testing.T) {
|
||||
|
||||
b := []byte("foo bar baz")
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if err := SignAndWrap(buf, caCredsA.CAKeyPEM, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotB, gotSig, err := Unwrap(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
} else if !bytes.Equal(b, gotB) {
|
||||
t.Fatalf("got %q but expected %q", gotB, b)
|
||||
}
|
||||
|
||||
if err := ValidateSignature(caCredsA.CACertPEM, b, gotSig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ValidateSignature(caCredsB.CACertPEM, b, gotSig); !errors.Is(err, ErrInvalidSignature) {
|
||||
t.Fatalf("expected ErrInvalidSignature but got %v", err)
|
||||
}
|
||||
}
|
@ -24,8 +24,9 @@ in rec {
|
||||
env = buildEnv {
|
||||
name = "cryptic-net-garage";
|
||||
paths = [
|
||||
garage.pkgs.amd64.release
|
||||
garage
|
||||
minioClient
|
||||
./src
|
||||
];
|
||||
};
|
||||
|
24
garage/src/bin/garage-apply-layout-diff
Normal file
24
garage/src/bin/garage-apply-layout-diff
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
set -e -o pipefail
|
||||
|
||||
tmp="$(mktemp -d -t cryptic-net-garage-apply-layout-diff-XXX)"
|
||||
|
||||
( trap "rm -rf '$tmp'" EXIT
|
||||
|
||||
tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./hosts
|
||||
|
||||
thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname)
|
||||
thisHostIP=$(cat "$tmp"/hosts/"$thisHostName".yml | yq '.nebula.ip')
|
||||
|
||||
firstRPCPort=$(cat "$_DAEMON_YML_PATH" | yq '.storage.allocations[0].rpc_port')
|
||||
|
||||
firstPeerID=$(cryptic-net-main garage-peer-keygen -danger -ip "$thisHostIP" -port "$firstRPCPort")
|
||||
|
||||
export GARAGE_RPC_HOST="$firstPeerID"@"$thisHostIP":"$firstRPCPort"
|
||||
export GARAGE_RPC_SECRET=$(tar -xzf "$_BOOTSTRAP_PATH" --to-stdout "./garage/rpc-secret.txt")
|
||||
|
||||
garage layout show | cryptic-net-main garage-layout-diff | while read diffLine; do
|
||||
echo "> $diffLine"
|
||||
$diffLine
|
||||
done
|
||||
)
|
9
go-workspace/README.md
Normal file
9
go-workspace/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# go-workspace
|
||||
|
||||
This module is used for building all custom go binaries within the cryptic-net
|
||||
project.
|
||||
|
||||
The reason binaries are contained here, and not under the sub-directory for the
|
||||
sub-process the correspond to like most other code in this project, is that nix
|
||||
makes it difficult to compose multiple modules defined locally. If nix ever
|
||||
fixes this we should split this out.
|
18
go-workspace/default.nix
Normal file
18
go-workspace/default.nix
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
buildGoModule,
|
||||
}: let
|
||||
|
||||
build = subPackage: buildGoModule {
|
||||
|
||||
pname = "cryptic-net-" + (builtins.baseNameOf subPackage);
|
||||
version = "unstable";
|
||||
src = ./src;
|
||||
vendorSha256 = "sha256-UqMxbu/v/zDR4yJgUYiLs7HuHkvsZc/MJiWgw/8g+xk=";
|
||||
subPackages = [
|
||||
subPackage
|
||||
];
|
||||
};
|
||||
|
||||
in {
|
||||
crypticNetMain = build "cmd/cryptic-net-main";
|
||||
}
|
122
go-workspace/src/admin/admin.go
Normal file
122
go-workspace/src/admin/admin.go
Normal file
@ -0,0 +1,122 @@
|
||||
// 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()
|
||||
}
|
194
go-workspace/src/bootstrap/bootstrap.go
Normal file
194
go-workspace/src/bootstrap/bootstrap.go
Normal file
@ -0,0 +1,194 @@
|
||||
// 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
|
||||
GarageAdminToken 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},
|
||||
{&b.GarageAdminToken, garageAdminTokenPath},
|
||||
}
|
||||
|
||||
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},
|
||||
{b.GarageAdminToken, garageAdminTokenPath},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
@ -2,12 +2,20 @@ package bootstrap
|
||||
|
||||
import (
|
||||
"cryptic-net/garage"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Paths within the bootstrap FS related to garage.
|
||||
const (
|
||||
garageRPCSecretPath = "garage/rpc-secret.txt"
|
||||
garageAdminTokenPath = "garage/admin-token.txt"
|
||||
garageGlobalBucketKeyYmlPath = "garage/cryptic-net-global-bucket-key.yml"
|
||||
)
|
||||
|
||||
// GaragePeers returns a Peer for each known garage instance in the network.
|
||||
func (b Bootstrap) GaragePeers() []garage.RemotePeer {
|
||||
func (b Bootstrap) GaragePeers() []garage.Peer {
|
||||
|
||||
var peers []garage.RemotePeer
|
||||
var peers []garage.Peer
|
||||
|
||||
for _, host := range b.Hosts {
|
||||
|
||||
@ -17,9 +25,8 @@ func (b Bootstrap) GaragePeers() []garage.RemotePeer {
|
||||
|
||||
for _, instance := range host.Garage.Instances {
|
||||
|
||||
peer := garage.RemotePeer{
|
||||
ID: instance.ID,
|
||||
IP: host.IP().String(),
|
||||
peer := garage.Peer{
|
||||
IP: host.Nebula.IP,
|
||||
RPCPort: instance.RPCPort,
|
||||
S3APIPort: instance.S3APIPort,
|
||||
}
|
||||
@ -36,8 +43,7 @@ func (b Bootstrap) GaragePeers() []garage.RemotePeer {
|
||||
func (b Bootstrap) GarageRPCPeerAddrs() []string {
|
||||
var addrs []string
|
||||
for _, peer := range b.GaragePeers() {
|
||||
addr := peer.RPCPeerAddr()
|
||||
addrs = append(addrs, addr)
|
||||
addrs = append(addrs, peer.RPCPeerAddr())
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
@ -45,16 +51,14 @@ func (b Bootstrap) GarageRPCPeerAddrs() []string {
|
||||
// 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.RemotePeer {
|
||||
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.RemotePeer{
|
||||
ID: inst.ID,
|
||||
IP: thisHost.IP().String(),
|
||||
return garage.Peer{
|
||||
IP: thisHost.Nebula.IP,
|
||||
RPCPort: inst.RPCPort,
|
||||
S3APIPort: inst.S3APIPort,
|
||||
}
|
||||
@ -69,8 +73,15 @@ func (b Bootstrap) ChooseGaragePeer() garage.RemotePeer {
|
||||
|
||||
// GlobalBucketS3APIClient returns an S3 client pre-configured with access to
|
||||
// the global bucket.
|
||||
func (b Bootstrap) GlobalBucketS3APIClient() garage.S3APIClient {
|
||||
func (b Bootstrap) GlobalBucketS3APIClient() (garage.S3APIClient, error) {
|
||||
|
||||
addr := b.ChooseGaragePeer().S3APIAddr()
|
||||
creds := b.Garage.GlobalBucketS3APICredentials
|
||||
return garage.NewS3APIClient(addr, creds)
|
||||
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
|
||||
}
|
111
go-workspace/src/bootstrap/garage_global_bucket.go
Normal file
111
go-workspace/src/bootstrap/garage_global_bucket.go
Normal file
@ -0,0 +1,111 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"cryptic-net/garage"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Paths within garage's global bucket
|
||||
const (
|
||||
garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts"
|
||||
)
|
||||
|
||||
// PutGarageBoostrapHost places the <hostname>.yml file for the given host into
|
||||
// garage so that other hosts are able to see relevant configuration for it.
|
||||
//
|
||||
// The given client should be for the global bucket.
|
||||
func PutGarageBoostrapHost(
|
||||
ctx context.Context, client garage.S3APIClient, host Host,
|
||||
) error {
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if err := yaml.NewEncoder(buf).Encode(host); err != nil {
|
||||
log.Fatalf("yaml encoding host data: %v", err)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, host.Name+".yml")
|
||||
|
||||
_, err := client.PutObject(
|
||||
ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()),
|
||||
minio.PutObjectOptions{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing to %q in global bucket: %w", filePath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveGarageBootstrapHost removes the <hostname>.yml for the given host from
|
||||
// garage.
|
||||
//
|
||||
// The given client should be for the global bucket.
|
||||
func RemoveGarageBootstrapHost(
|
||||
ctx context.Context, client garage.S3APIClient, hostName string,
|
||||
) error {
|
||||
|
||||
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, hostName+".yml")
|
||||
|
||||
return client.RemoveObject(
|
||||
ctx, garage.GlobalBucket, filePath,
|
||||
minio.RemoveObjectOptions{},
|
||||
)
|
||||
}
|
||||
|
||||
// GetGarageBootstrapHosts loads the <hostname>.yml file for all hosts stored in
|
||||
// garage.
|
||||
//
|
||||
// The given client should be for the global bucket.
|
||||
func GetGarageBootstrapHosts(
|
||||
ctx context.Context, client garage.S3APIClient,
|
||||
) (
|
||||
map[string]Host, error,
|
||||
) {
|
||||
|
||||
hosts := map[string]Host{}
|
||||
|
||||
objInfoCh := client.ListObjects(
|
||||
ctx, garage.GlobalBucket,
|
||||
minio.ListObjectsOptions{
|
||||
Prefix: garageGlobalBucketBootstrapHostsDirPath,
|
||||
Recursive: true,
|
||||
},
|
||||
)
|
||||
|
||||
for objInfo := range objInfoCh {
|
||||
|
||||
if objInfo.Err != nil {
|
||||
return nil, fmt.Errorf("listing objects: %w", objInfo.Err)
|
||||
}
|
||||
|
||||
obj, err := client.GetObject(
|
||||
ctx, garage.GlobalBucket, objInfo.Key, minio.GetObjectOptions{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
|
||||
}
|
||||
|
||||
var host Host
|
||||
|
||||
err = yaml.NewDecoder(obj).Decode(&host)
|
||||
obj.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("yaml decoding object %q: %w", objInfo.Key, err)
|
||||
}
|
||||
|
||||
hosts[host.Name] = host
|
||||
}
|
||||
|
||||
return hosts, nil
|
||||
}
|
81
go-workspace/src/bootstrap/hosts.go
Normal file
81
go-workspace/src/bootstrap/hosts.go
Normal file
@ -0,0 +1,81 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
8
go-workspace/src/bootstrap/nebula.go
Normal file
8
go-workspace/src/bootstrap/nebula.go
Normal file
@ -0,0 +1,8 @@
|
||||
package bootstrap
|
||||
|
||||
// Paths within the bootstrap FS related to nebula.
|
||||
const (
|
||||
nebulaCertsCACertPath = "nebula/certs/ca.crt"
|
||||
nebulaCertsHostCertPath = "nebula/certs/host.crt"
|
||||
nebulaCertsHostKeyPath = "nebula/certs/host.key"
|
||||
)
|
79
go-workspace/src/cmd/cryptic-net-main/main.go
Normal file
79
go-workspace/src/cmd/cryptic-net-main/main.go
Normal file
@ -0,0 +1,79 @@
|
||||
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_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-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()
|
||||
}
|
123
go-workspace/src/cmd/entrypoint/admin.go
Normal file
123
go-workspace/src/cmd/entrypoint/admin.go
Normal file
@ -0,0 +1,123 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"cryptic-net/admin"
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/nebula"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func randStr(l int) string {
|
||||
b := make([]byte, l)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
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 subCmdAdminMakeBootstrap = subCmd{
|
||||
name: "make-bootstrap",
|
||||
descr: "Creates a new bootstrap.tgz file for a particular host and writes it to stdout",
|
||||
checkLock: true,
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
|
||||
flags := subCmdCtx.flagSet(false)
|
||||
|
||||
name := flags.StringP(
|
||||
"name", "n", "",
|
||||
"Name of the host to generate bootstrap.tgz for",
|
||||
)
|
||||
|
||||
adminPath := flags.StringP(
|
||||
"admin-path", "a", "",
|
||||
`Path to admin.tgz file. If the given path is "-" then stdin is used.`,
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if *name == "" || *adminPath == "" {
|
||||
return errors.New("--name and --admin-path are required")
|
||||
}
|
||||
|
||||
env := subCmdCtx.env
|
||||
|
||||
adm, err := readAdmin(*adminPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
|
||||
}
|
||||
|
||||
client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||
}
|
||||
|
||||
// NOTE this isn't _technically_ required, but if the `hosts add`
|
||||
// command for this host has been run recently then it might not have
|
||||
// made it into the bootstrap file yet, and so won't be in
|
||||
// `env.Bootstrap`.
|
||||
hosts, err := bootstrap.GetGarageBootstrapHosts(env.Context, client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving host info from garage: %w", err)
|
||||
}
|
||||
|
||||
host, ok := hosts[*name]
|
||||
if !ok {
|
||||
return fmt.Errorf("couldn't find host into for %q in garage, has `cryptic-net hosts add` been run yet?", *name)
|
||||
}
|
||||
|
||||
nebulaHostCert, err := nebula.NewHostCert(adm.NebulaCACert, host.Name, host.Nebula.IP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating new nebula host key/cert: %w", err)
|
||||
}
|
||||
|
||||
newBootstrap := bootstrap.Bootstrap{
|
||||
Hosts: hosts,
|
||||
HostName: *name,
|
||||
|
||||
NebulaHostCert: nebulaHostCert,
|
||||
|
||||
GarageRPCSecret: adm.GarageRPCSecret,
|
||||
GarageAdminToken: randStr(32),
|
||||
GarageGlobalBucketS3APICredentials: adm.GarageGlobalBucketS3APICredentials,
|
||||
}
|
||||
|
||||
return newBootstrap.WriteTo(os.Stdout)
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdAdmin = subCmd{
|
||||
name: "admin",
|
||||
descr: "Sub-commands which only admins can run",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
subCmdAdminMakeBootstrap,
|
||||
)
|
||||
},
|
||||
}
|
274
go-workspace/src/cmd/entrypoint/daemon.go
Normal file
274
go-workspace/src/cmd/entrypoint/daemon.go
Normal file
@ -0,0 +1,274 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
crypticnet "cryptic-net"
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/garage"
|
||||
|
||||
"github.com/cryptic-io/pmux/pmuxlib"
|
||||
)
|
||||
|
||||
// The daemon sub-command deals with starting an actual cryptic-net daemon
|
||||
// process, which is required to be running for most other cryptic-net
|
||||
// functionality. The sub-command does the following:
|
||||
//
|
||||
// * Creates and locks the runtime directory.
|
||||
//
|
||||
// * Creates the data directory and copies the appdir bootstrap file into there,
|
||||
// if it's not already there.
|
||||
//
|
||||
// * Merges the user-provided daemon.yml file with the default, and writes the
|
||||
// result to the runtime dir.
|
||||
//
|
||||
// * Merges daemon.yml configuration into the bootstrap configuration, and
|
||||
// rewrites the bootstrap file.
|
||||
//
|
||||
// * Sets up environment variables that all other sub-processes then use, based
|
||||
// on the runtime dir.
|
||||
//
|
||||
// * Dynamically creates the root pmux config and runs pmux.
|
||||
//
|
||||
// * (On exit) cleans up the runtime directory.
|
||||
|
||||
// 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{
|
||||
nebulaEntrypointPmuxProcConfig(),
|
||||
{
|
||||
Name: "dnsmasq",
|
||||
Cmd: "bash",
|
||||
Args: waitForNebulaArgs(env, "dnsmasq-entrypoint"),
|
||||
},
|
||||
}
|
||||
|
||||
if len(thisDaemon.Storage.Allocations) > 0 {
|
||||
|
||||
garageChildrenPmuxProcConfigs, err := garageChildrenPmuxProcConfigs(env)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating garage children configs: %w", err)
|
||||
}
|
||||
|
||||
pmuxProcConfigs = append(pmuxProcConfigs, garageChildrenPmuxProcConfigs...)
|
||||
pmuxProcConfigs = append(pmuxProcConfigs, garageApplyLayoutDiffPmuxProcConfig(env))
|
||||
}
|
||||
|
||||
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
|
||||
Name: "update-global-bucket",
|
||||
Cmd: "bash",
|
||||
Args: waitForGarageArgs(env, "update-global-bucket"),
|
||||
NoRestartOn: []int{0},
|
||||
})
|
||||
|
||||
pmuxConfig := pmuxlib.Config{Processes: pmuxProcConfigs}
|
||||
|
||||
doneCh := env.Context.Done()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
ctx, cancel := context.WithCancel(env.Context)
|
||||
defer cancel()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
pmuxlib.Run(ctx, pmuxConfig)
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(3 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
case <-doneCh:
|
||||
return env.Context.Err()
|
||||
|
||||
case <-ticker.C:
|
||||
|
||||
fmt.Fprintln(os.Stderr, "checking for changes to bootstrap")
|
||||
|
||||
if changed, err := reloadBootstrap(env, s3Client); err != nil {
|
||||
return fmt.Errorf("reloading bootstrap: %w", err)
|
||||
|
||||
} else if changed {
|
||||
fmt.Fprintln(os.Stderr, "bootstrap info has changed, restarting all processes")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var subCmdDaemon = subCmd{
|
||||
name: "daemon",
|
||||
descr: "Runs the cryptic-net daemon (Default if no sub-command given)",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
|
||||
flags := subCmdCtx.flagSet(false)
|
||||
|
||||
daemonYmlPath := flags.StringP(
|
||||
"config-path", "c", "",
|
||||
"Optional path to a daemon.yml file to load configuration from.",
|
||||
)
|
||||
|
||||
dumpConfig := flags.Bool(
|
||||
"dump-config", false,
|
||||
"Write the default configuration file to stdout and exit.",
|
||||
)
|
||||
|
||||
bootstrapPath := flags.StringP(
|
||||
"bootstrap-path", "b", "",
|
||||
`Path to a bootstrap.tgz file. This only needs to be provided the first time the daemon is started, after that it is ignored. If the cryptic-net binary has a bootstrap built into it then this argument is always optional.`,
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
env := subCmdCtx.env
|
||||
|
||||
if *dumpConfig {
|
||||
return writeBuiltinDaemonYml(env, os.Stdout)
|
||||
}
|
||||
|
||||
runtimeDirPath := env.RuntimeDirPath
|
||||
|
||||
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath)
|
||||
|
||||
if err := os.MkdirAll(runtimeDirPath, 0700); err != nil {
|
||||
return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err)
|
||||
|
||||
} else if err := crypticnet.NewProcLock(runtimeDirPath).WriteLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// do not defer the cleaning of the runtime directory until the lock has
|
||||
// been obtained, otherwise we might delete the directory out from under
|
||||
// the feet of an already running daemon
|
||||
defer func() {
|
||||
fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", runtimeDirPath)
|
||||
if err := os.RemoveAll(runtimeDirPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", runtimeDirPath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
// If the bootstrap file is not being stored in the data dir, move it
|
||||
// there and reload the bootstrap info
|
||||
if env.BootstrapPath != env.DataDirBootstrapPath() {
|
||||
|
||||
path := env.BootstrapPath
|
||||
|
||||
// If there's no BootstrapPath then no bootstrap file could be
|
||||
// found. In this case we require the user to provide one on the
|
||||
// command-line.
|
||||
if path == "" {
|
||||
|
||||
if *bootstrapPath == "" {
|
||||
return errors.New("No bootstrap.tgz file could be found, and one is not provided with --bootstrap-path")
|
||||
}
|
||||
|
||||
path = *bootstrapPath
|
||||
}
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening file %q: %w", env.BootstrapPath, err)
|
||||
}
|
||||
|
||||
err = copyBootstrapToDataDir(env, f)
|
||||
f.Close()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("copying bootstrap file from %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil {
|
||||
return fmt.Errorf("merging and writing daemon.yml file: %w", err)
|
||||
}
|
||||
|
||||
// we update this Host's data using whatever configuration has been
|
||||
// provided by daemon.yml. This way the daemon has the most
|
||||
// up-to-date possible bootstrap. This updated bootstrap will later
|
||||
// get updated in garage using update-global-bucket, so other hosts
|
||||
// will see it as well.
|
||||
if err := mergeDaemonIntoBootstrap(env); err != nil {
|
||||
return fmt.Errorf("merging daemon.yml into bootstrap data: %w", err)
|
||||
}
|
||||
|
||||
for key, val := range env.ToMap() {
|
||||
if err := os.Setenv(key, val); err != nil {
|
||||
return fmt.Errorf("failed to set %q to %q: %w", key, val, err)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
// create s3Client anew on every loop, in case the topology has
|
||||
// changed and we should be connecting to a different garage
|
||||
// endpoint.
|
||||
s3Client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||
}
|
||||
|
||||
if err := runDaemonPmuxOnce(env, s3Client); errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("running pmux for daemon: %w", err)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
205
go-workspace/src/cmd/entrypoint/daemon_util.go
Normal file
205
go-workspace/src/cmd/entrypoint/daemon_util.go
Normal file
@ -0,0 +1,205 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
crypticnet "cryptic-net"
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/garage"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/cryptic-io/pmux/pmuxlib"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func mergeDaemonIntoBootstrap(env *crypticnet.Env) error {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForNebulaArgs(env *crypticnet.Env, args ...string) []string {
|
||||
thisHost := env.Bootstrap.ThisHost()
|
||||
return append([]string{"wait-for-ip", thisHost.Nebula.IP}, args...)
|
||||
}
|
||||
|
||||
func waitForGarageArgs(env *crypticnet.Env, args ...string) []string {
|
||||
|
||||
thisHost := env.Bootstrap.ThisHost()
|
||||
allocs := env.ThisDaemon().Storage.Allocations
|
||||
|
||||
if len(allocs) == 0 {
|
||||
return waitForNebulaArgs(env, args...)
|
||||
}
|
||||
|
||||
var preArgs []string
|
||||
|
||||
for _, alloc := range allocs {
|
||||
preArgs = append(
|
||||
preArgs,
|
||||
"wait-for",
|
||||
net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
|
||||
"--",
|
||||
)
|
||||
}
|
||||
|
||||
return append(preArgs, args...)
|
||||
}
|
||||
|
||||
func nebulaEntrypointPmuxProcConfig() pmuxlib.ProcessConfig {
|
||||
return pmuxlib.ProcessConfig{
|
||||
Name: "nebula",
|
||||
Cmd: "cryptic-net-main",
|
||||
Args: []string{
|
||||
"nebula-entrypoint",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func garageWriteChildConf(
|
||||
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,
|
||||
AdminToken: env.Bootstrap.GarageAdminToken,
|
||||
|
||||
RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
|
||||
APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)),
|
||||
AdminAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.AdminPort)),
|
||||
|
||||
BootstrapPeers: env.Bootstrap.GarageRPCPeerAddrs(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("creating garage.toml file at %q: %w", garageTomlPath, err)
|
||||
}
|
||||
|
||||
return garageTomlPath, nil
|
||||
}
|
||||
|
||||
func garageChildrenPmuxProcConfigs(env *crypticnet.Env) ([]pmuxlib.ProcessConfig, error) {
|
||||
|
||||
var pmuxProcConfigs []pmuxlib.ProcessConfig
|
||||
|
||||
for _, alloc := range env.ThisDaemon().Storage.Allocations {
|
||||
|
||||
childConfPath, err := garageWriteChildConf(env, alloc)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
|
||||
}
|
||||
|
||||
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
|
||||
Name: fmt.Sprintf("garage-%d", alloc.RPCPort),
|
||||
Cmd: "garage",
|
||||
Args: []string{"-c", childConfPath, "server"},
|
||||
SigKillWait: 1 * time.Minute,
|
||||
})
|
||||
}
|
||||
|
||||
return pmuxProcConfigs, nil
|
||||
}
|
||||
|
||||
func garageApplyLayoutDiffPmuxProcConfig(env *crypticnet.Env) pmuxlib.ProcessConfig {
|
||||
return pmuxlib.ProcessConfig{
|
||||
Name: "garage-apply-layout-diff",
|
||||
Cmd: "bash",
|
||||
Args: waitForGarageArgs(env, "bash", "garage-apply-layout-diff"),
|
||||
NoRestartOn: []int{0},
|
||||
}
|
||||
}
|
72
go-workspace/src/cmd/entrypoint/daemon_yml.go
Normal file
72
go-workspace/src/cmd/entrypoint/daemon_yml.go
Normal file
@ -0,0 +1,72 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
crypticnet "cryptic-net"
|
||||
"cryptic-net/yamlutil"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func builtinDaemonYmlPath(env *crypticnet.Env) string {
|
||||
return filepath.Join(env.AppDirPath, "etc", "daemon.yml")
|
||||
}
|
||||
|
||||
func writeBuiltinDaemonYml(env *crypticnet.Env, w io.Writer) error {
|
||||
|
||||
builtinDaemonYmlPath := builtinDaemonYmlPath(env)
|
||||
|
||||
builtinDaemonYml, err := os.ReadFile(builtinDaemonYmlPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading default daemon.yml at %q: %w", builtinDaemonYmlPath, err)
|
||||
}
|
||||
|
||||
if _, err := w.Write(builtinDaemonYml); err != nil {
|
||||
return fmt.Errorf("writing default daemon.yml: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeMergedDaemonYml(env *crypticnet.Env, userDaemonYmlPath string) error {
|
||||
|
||||
builtinDaemonYmlPath := builtinDaemonYmlPath(env)
|
||||
|
||||
var fullDaemonYml map[string]interface{}
|
||||
|
||||
if err := yamlutil.LoadYamlFile(&fullDaemonYml, builtinDaemonYmlPath); err != nil {
|
||||
return fmt.Errorf("parsing builtin daemon.yml file: %w", err)
|
||||
}
|
||||
|
||||
if userDaemonYmlPath != "" {
|
||||
|
||||
var daemonYml map[string]interface{}
|
||||
if err := yamlutil.LoadYamlFile(&daemonYml, userDaemonYmlPath); err != nil {
|
||||
return fmt.Errorf("parsing %q: %w", userDaemonYmlPath, err)
|
||||
}
|
||||
|
||||
err := mergo.Merge(&fullDaemonYml, daemonYml, mergo.WithOverride)
|
||||
if err != nil {
|
||||
return fmt.Errorf("merging contents of file %q: %w", userDaemonYmlPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
fullDaemonYmlB, err := yaml.Marshal(fullDaemonYml)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("yaml marshaling daemon config: %w", err)
|
||||
}
|
||||
|
||||
daemonYmlPath := filepath.Join(env.RuntimeDirPath, "daemon.yml")
|
||||
|
||||
if err := ioutil.WriteFile(daemonYmlPath, fullDaemonYmlB, 0400); err != nil {
|
||||
return fmt.Errorf("writing daemon.yml file to %q: %w", daemonYmlPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -28,21 +28,18 @@ var subCmdGarageMC = subCmd{
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
hostBootstrap, err := loadHostBootstrap()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||
}
|
||||
env := subCmdCtx.env
|
||||
|
||||
s3APIAddr := hostBootstrap.ChooseGaragePeer().S3APIAddr()
|
||||
s3APIAddr := env.Bootstrap.ChooseGaragePeer().S3APIAddr()
|
||||
|
||||
if *keyID == "" || *keySecret == "" {
|
||||
|
||||
if *keyID == "" {
|
||||
*keyID = hostBootstrap.Garage.GlobalBucketS3APICredentials.ID
|
||||
*keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.ID
|
||||
}
|
||||
|
||||
if *keySecret == "" {
|
||||
*keyID = hostBootstrap.Garage.GlobalBucketS3APICredentials.Secret
|
||||
*keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.Secret
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +52,7 @@ var subCmdGarageMC = subCmd{
|
||||
args = append([]string{"mc"}, args...)
|
||||
|
||||
var (
|
||||
binPath = "mc"
|
||||
binPath = env.BinPath("mc")
|
||||
cliEnv = append(
|
||||
os.Environ(),
|
||||
fmt.Sprintf(
|
||||
@ -86,18 +83,15 @@ var subCmdGarageCLI = subCmd{
|
||||
checkLock: true,
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
|
||||
hostBootstrap, err := loadHostBootstrap()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||
}
|
||||
env := subCmdCtx.env
|
||||
|
||||
var (
|
||||
binPath = "garage"
|
||||
binPath = env.BinPath("garage")
|
||||
args = append([]string{"garage"}, subCmdCtx.args...)
|
||||
cliEnv = append(
|
||||
os.Environ(),
|
||||
"GARAGE_RPC_HOST="+hostBootstrap.ChooseGaragePeer().RPCAddr(),
|
||||
"GARAGE_RPC_SECRET="+hostBootstrap.Garage.RPCSecret,
|
||||
"GARAGE_RPC_HOST="+env.Bootstrap.ChooseGaragePeer().RPCAddr(),
|
||||
"GARAGE_RPC_SECRET="+env.Bootstrap.GarageRPCSecret,
|
||||
)
|
||||
)
|
||||
|
@ -1,9 +1,10 @@
|
||||
package main
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"cryptic-net/bootstrap"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
@ -22,18 +23,74 @@ func validateHostName(name string) error {
|
||||
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 {
|
||||
|
||||
hostBootstrap, err := loadHostBootstrap()
|
||||
env := subCmdCtx.env
|
||||
|
||||
client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||
}
|
||||
|
||||
hostsMap, err := hostBootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx)
|
||||
hostsMap, err := bootstrap.GetGarageBootstrapHosts(env.Context, client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving hosts from garage: %w", err)
|
||||
}
|
||||
@ -70,14 +127,14 @@ var subCmdHostsDelete = subCmd{
|
||||
return errors.New("--name is required")
|
||||
}
|
||||
|
||||
hostBootstrap, err := loadHostBootstrap()
|
||||
env := subCmdCtx.env
|
||||
|
||||
client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||
return fmt.Errorf("creating client for global bucket: %w", err)
|
||||
}
|
||||
|
||||
client := hostBootstrap.GlobalBucketS3APIClient()
|
||||
|
||||
return bootstrap.RemoveGarageBootstrapHost(subCmdCtx.ctx, client, *name)
|
||||
return bootstrap.RemoveGarageBootstrapHost(env.Context, client, *name)
|
||||
},
|
||||
}
|
||||
|
||||
@ -86,6 +143,7 @@ var subCmdHosts = subCmd{
|
||||
descr: "Sub-commands having to do with configuration of hosts in the network",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
subCmdHostsAdd,
|
||||
subCmdHostsDelete,
|
||||
subCmdHostsList,
|
||||
)
|
37
go-workspace/src/cmd/entrypoint/main.go
Normal file
37
go-workspace/src/cmd/entrypoint/main.go
Normal file
@ -0,0 +1,37 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
crypticnet "cryptic-net"
|
||||
)
|
||||
|
||||
// The purpose of this binary is to act as the entrypoint of the cryptic-net
|
||||
// process. It processes the command-line arguments which are passed in, and
|
||||
// then passes execution along to an appropriate binary housed in AppDir/bin
|
||||
// (usually a bash script, which is more versatile than a go program).
|
||||
|
||||
func Main() {
|
||||
|
||||
env, err := crypticnet.NewEnv(true)
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("loading environment: %v", err))
|
||||
}
|
||||
|
||||
err = subCmdCtx{
|
||||
args: os.Args[1:],
|
||||
env: env,
|
||||
}.doSubCmd(
|
||||
subCmdAdmin,
|
||||
subCmdDaemon,
|
||||
subCmdGarage,
|
||||
subCmdHosts,
|
||||
subCmdVersion,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
package main
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
crypticnet "cryptic-net"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
@ -14,8 +14,7 @@ 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
|
||||
|
||||
ctx context.Context
|
||||
env *crypticnet.Env
|
||||
}
|
||||
|
||||
type subCmd struct {
|
||||
@ -100,7 +99,9 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
|
||||
|
||||
if subCmd.checkLock {
|
||||
|
||||
if err := assertLock(); err != nil {
|
||||
err := crypticnet.NewProcLock(ctx.env.RuntimeDirPath).AssertLock()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking lock file: %w", err)
|
||||
}
|
||||
}
|
||||
@ -109,7 +110,7 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
|
||||
subCmd: subCmd,
|
||||
args: args,
|
||||
subCmdNames: append(ctx.subCmdNames, subCmdName),
|
||||
ctx: ctx.ctx,
|
||||
env: ctx.env,
|
||||
})
|
||||
|
||||
if err != nil {
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -11,7 +11,7 @@ var subCmdVersion = subCmd{
|
||||
descr: "Dumps version and build info to stdout",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
|
||||
versionPath := filepath.Join(envAppDirPath, "share/version")
|
||||
versionPath := filepath.Join(subCmdCtx.env.AppDirPath, "share/version")
|
||||
|
||||
version, err := os.ReadFile(versionPath)
|
||||
|
256
go-workspace/src/cmd/garage-layout-diff/main.go
Normal file
256
go-workspace/src/cmd/garage-layout-diff/main.go
Normal file
@ -0,0 +1,256 @@
|
||||
package garage_layout_diff
|
||||
|
||||
// This binary accepts the output of `garage layout show` into its stdout, and
|
||||
// it outputs a newline-delimited set of `garage layout $cmd` strings on
|
||||
// stdout. The layout commands which are output will, if run, bring the current
|
||||
// node's layout on the cluster up-to-date with what's in daemon.yml.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
crypticnet "cryptic-net"
|
||||
"cryptic-net/garage"
|
||||
)
|
||||
|
||||
type clusterNode struct {
|
||||
ID string
|
||||
Zone string
|
||||
Capacity int
|
||||
}
|
||||
|
||||
type clusterNodes []clusterNode
|
||||
|
||||
func (n clusterNodes) get(id string) (clusterNode, bool) {
|
||||
|
||||
var ok bool
|
||||
|
||||
for _, node := range n {
|
||||
|
||||
if len(node.ID) > len(id) {
|
||||
ok = strings.HasPrefix(node.ID, id)
|
||||
} else {
|
||||
ok = strings.HasPrefix(id, node.ID)
|
||||
}
|
||||
|
||||
if ok {
|
||||
return node, true
|
||||
}
|
||||
}
|
||||
|
||||
return clusterNode{}, false
|
||||
}
|
||||
|
||||
var currClusterLayoutVersionB = []byte("Current cluster layout version:")
|
||||
|
||||
func readCurrNodes(r io.Reader) (clusterNodes, int, error) {
|
||||
|
||||
input, err := io.ReadAll(r)
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("reading stdin: %w", err)
|
||||
}
|
||||
|
||||
// NOTE I'm not sure if this check should be turned on or not. It simplifies
|
||||
// things to turn it off and just say that no one should ever be manually
|
||||
// messing with the layout, but on the other hand maybe someone might?
|
||||
//
|
||||
//if i := bytes.Index(input, []byte("==== STAGED ROLE CHANGES ====")); i >= 0 {
|
||||
// return nil, 0, errors.New("cluster layout has staged changes already, won't modify")
|
||||
//}
|
||||
|
||||
/* The first section of input will always be something like this:
|
||||
|
||||
```
|
||||
==== CURRENT CLUSTER LAYOUT ====
|
||||
ID Tags Zone Capacity
|
||||
AAA… ZZZ 1
|
||||
BBB… ZZZ 1
|
||||
CCC… ZZZ 1
|
||||
|
||||
Current cluster layout version: N
|
||||
```
|
||||
|
||||
There may be more, depending on if the cluster already has changes staged,
|
||||
but this will definitely be first. */
|
||||
|
||||
i := bytes.Index(input, currClusterLayoutVersionB)
|
||||
|
||||
if i < 0 {
|
||||
return nil, 0, errors.New("no current cluster layout found in input")
|
||||
}
|
||||
|
||||
input, tail := input[:i], input[i:]
|
||||
|
||||
var currNodes clusterNodes
|
||||
|
||||
for inputBuf := bufio.NewReader(bytes.NewBuffer(input)); ; {
|
||||
|
||||
line, err := inputBuf.ReadString('\n')
|
||||
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, 0, fmt.Errorf("reading input line by line from buffer: %w", err)
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
id := fields[0]
|
||||
|
||||
// The ID will always be given ending in this fucked up ellipses
|
||||
if trimmedID := strings.TrimSuffix(id, "…"); id == trimmedID {
|
||||
continue
|
||||
} else {
|
||||
id = trimmedID
|
||||
}
|
||||
|
||||
zone := fields[1]
|
||||
|
||||
capacity, err := strconv.Atoi(fields[2])
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("parsing capacity %q: %w", fields[2], err)
|
||||
}
|
||||
|
||||
currNodes = append(currNodes, clusterNode{
|
||||
ID: id,
|
||||
Zone: zone,
|
||||
Capacity: capacity,
|
||||
})
|
||||
}
|
||||
|
||||
// parse current cluster version from tail
|
||||
tail = bytes.TrimPrefix(tail, currClusterLayoutVersionB)
|
||||
|
||||
if i := bytes.Index(tail, []byte("\n")); i > 0 {
|
||||
tail = tail[:i]
|
||||
}
|
||||
|
||||
tail = bytes.TrimSpace(tail)
|
||||
|
||||
version, err := strconv.Atoi(string(tail))
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("parsing version string from %q: %w", tail, err)
|
||||
}
|
||||
|
||||
return currNodes, version, nil
|
||||
}
|
||||
|
||||
func readExpNodes(env *crypticnet.Env) clusterNodes {
|
||||
|
||||
thisHost := env.Bootstrap.ThisHost()
|
||||
|
||||
var expNodes clusterNodes
|
||||
|
||||
for _, alloc := range env.ThisDaemon().Storage.Allocations {
|
||||
|
||||
peer := garage.Peer{
|
||||
IP: thisHost.Nebula.IP,
|
||||
RPCPort: alloc.RPCPort,
|
||||
S3APIPort: alloc.S3APIPort,
|
||||
}
|
||||
|
||||
id := peer.RPCPeerID()
|
||||
|
||||
expNodes = append(expNodes, clusterNode{
|
||||
ID: id,
|
||||
Zone: env.Bootstrap.HostName,
|
||||
Capacity: alloc.Capacity / 100,
|
||||
})
|
||||
}
|
||||
|
||||
return expNodes
|
||||
}
|
||||
|
||||
// NOTE: The id formatting for currNodes and expNodes is different; expNodes has
|
||||
// fully expanded ids, currNodes are abbreviated.
|
||||
|
||||
func diff(currNodes, expNodes clusterNodes) []string {
|
||||
|
||||
var lines []string
|
||||
|
||||
for _, node := range currNodes {
|
||||
|
||||
if _, ok := expNodes.get(node.ID); !ok {
|
||||
lines = append(
|
||||
lines,
|
||||
fmt.Sprintf("garage layout remove %s", node.ID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for _, expNode := range expNodes {
|
||||
|
||||
currNode, ok := currNodes.get(expNode.ID)
|
||||
|
||||
currNode.ID = expNode.ID // so that equality checking works
|
||||
|
||||
if ok && currNode == expNode {
|
||||
continue
|
||||
}
|
||||
|
||||
lines = append(
|
||||
lines,
|
||||
fmt.Sprintf(
|
||||
"garage layout assign %s -z %s -c %d",
|
||||
expNode.ID,
|
||||
expNode.Zone,
|
||||
expNode.Capacity,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func Main() {
|
||||
|
||||
env, err := crypticnet.ReadEnv()
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("reading environment: %w", err))
|
||||
}
|
||||
|
||||
currNodes, currVersion, err := readCurrNodes(os.Stdin)
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("reading current layout from stdin: %w", err))
|
||||
}
|
||||
|
||||
thisCurrNodes := make(clusterNodes, 0, len(currNodes))
|
||||
|
||||
for _, node := range currNodes {
|
||||
|
||||
if env.Bootstrap.HostName != node.Zone {
|
||||
continue
|
||||
}
|
||||
|
||||
thisCurrNodes = append(thisCurrNodes, node)
|
||||
}
|
||||
|
||||
expNodes := readExpNodes(env)
|
||||
|
||||
lines := diff(thisCurrNodes, expNodes)
|
||||
|
||||
if len(lines) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
fmt.Println(line)
|
||||
}
|
||||
|
||||
fmt.Printf("garage layout apply --version %d\n", currVersion+1)
|
||||
}
|
137
go-workspace/src/cmd/garage-layout-diff/main_test.go
Normal file
137
go-workspace/src/cmd/garage-layout-diff/main_test.go
Normal file
@ -0,0 +1,137 @@
|
||||
package garage_layout_diff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadCurrNodes(t *testing.T) {
|
||||
|
||||
expNodes := clusterNodes{
|
||||
{
|
||||
ID: "AAA",
|
||||
Zone: "XXX",
|
||||
Capacity: 1,
|
||||
},
|
||||
{
|
||||
ID: "BBB",
|
||||
Zone: "YYY",
|
||||
Capacity: 2,
|
||||
},
|
||||
{
|
||||
ID: "CCC",
|
||||
Zone: "ZZZ",
|
||||
Capacity: 3,
|
||||
},
|
||||
}
|
||||
|
||||
expVersion := 666
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expNodes clusterNodes
|
||||
expVersion int
|
||||
}{
|
||||
{
|
||||
input: `
|
||||
==== CURRENT CLUSTER LAYOUT ====
|
||||
ID Tags Zone Capacity
|
||||
AAA… XXX 1
|
||||
BBB… YYY 2
|
||||
CCC… ZZZ 3
|
||||
|
||||
Current cluster layout version: 666
|
||||
`,
|
||||
expNodes: expNodes,
|
||||
expVersion: expVersion,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
|
||||
gotNodes, gotVersion, err := readCurrNodes(
|
||||
bytes.NewBufferString(test.input),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if gotVersion != test.expVersion {
|
||||
t.Fatalf(
|
||||
"expected version %d, got %d",
|
||||
test.expVersion,
|
||||
gotVersion,
|
||||
)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(gotNodes, test.expNodes) {
|
||||
t.Fatalf(
|
||||
"expected nodes: %#v,\ngot nodes: %#v",
|
||||
gotNodes,
|
||||
test.expNodes,
|
||||
)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
|
||||
currNodes := clusterNodes{
|
||||
{
|
||||
ID: "1",
|
||||
Zone: "zone",
|
||||
Capacity: 1,
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Zone: "zone",
|
||||
Capacity: 1,
|
||||
},
|
||||
{
|
||||
ID: "3",
|
||||
Zone: "zone",
|
||||
Capacity: 1,
|
||||
},
|
||||
{
|
||||
ID: "4",
|
||||
Zone: "zone",
|
||||
Capacity: 1,
|
||||
},
|
||||
}
|
||||
|
||||
expNodes := clusterNodes{
|
||||
{
|
||||
ID: "111",
|
||||
Zone: "zone",
|
||||
Capacity: 1,
|
||||
},
|
||||
{
|
||||
ID: "222",
|
||||
Zone: "zone2",
|
||||
Capacity: 1,
|
||||
},
|
||||
{
|
||||
ID: "333",
|
||||
Zone: "zone",
|
||||
Capacity: 10,
|
||||
},
|
||||
}
|
||||
|
||||
expLines := []string{
|
||||
`garage layout remove 4`,
|
||||
`garage layout assign 222 -z zone2 -c 1`,
|
||||
`garage layout assign 333 -z zone -c 10`,
|
||||
}
|
||||
|
||||
gotLines := diff(currNodes, expNodes)
|
||||
|
||||
if !reflect.DeepEqual(gotLines, expLines) {
|
||||
t.Fatalf("expected lines: %#v,\ngot lines: %#v", expLines, gotLines)
|
||||
}
|
||||
}
|
65
go-workspace/src/cmd/garage-peer-keygen/main.go
Normal file
65
go-workspace/src/cmd/garage-peer-keygen/main.go
Normal file
@ -0,0 +1,65 @@
|
||||
package garage_peer_keygen
|
||||
|
||||
/*
|
||||
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! !!
|
||||
!! DANGER !!
|
||||
!! !!
|
||||
!! This script will deterministically produce public/private keys given some !!
|
||||
!! arbitrary input. This is NEVER what you want. It's only being used in !!
|
||||
!! cryptic-net for a very specific purpose for which I think it's ok and is !!
|
||||
!! very necessary, and people are probably _still_ going to yell at me. !!
|
||||
!! !!
|
||||
!! DONT USE THIS. !!
|
||||
!! !!
|
||||
!! - Brian !!
|
||||
!! !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"cryptic-net/garage"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
|
||||
ip := flag.String("ip", "", "Internal IP address of the node to generate a key for")
|
||||
port := flag.Int("port", 0, "RPC port number for the garage instance to generate a key for")
|
||||
outPriv := flag.String("out-priv", "", "The path to the private key which should be created, if given.")
|
||||
outPub := flag.String("out-pub", "", "The path to the public key which should be created, if given.")
|
||||
danger := flag.Bool("danger", false, "Set this flag to indicate you understand WHY this binary should NEVER be used (see source code).")
|
||||
flag.Parse()
|
||||
|
||||
if len(*ip) == 0 || *port == 0 || !*danger {
|
||||
panic("The arguments -ip, -port, and -danger are required")
|
||||
}
|
||||
|
||||
peer := garage.Peer{
|
||||
IP: *ip,
|
||||
RPCPort: *port,
|
||||
}
|
||||
|
||||
pubKey, privKey := peer.RPCPeerKey()
|
||||
|
||||
fmt.Fprintln(os.Stdout, hex.EncodeToString(pubKey))
|
||||
|
||||
if *outPub != "" {
|
||||
if err := ioutil.WriteFile(*outPub, pubKey, 0444); err != nil {
|
||||
panic(fmt.Errorf("writing public key to %q: %w", *outPub, err))
|
||||
}
|
||||
}
|
||||
|
||||
if *outPriv != "" {
|
||||
if err := ioutil.WriteFile(*outPriv, privKey, 0400); err != nil {
|
||||
panic(fmt.Errorf("writing private key to %q: %w", *outPriv, err))
|
||||
}
|
||||
}
|
||||
}
|
129
go-workspace/src/cmd/nebula-entrypoint/main.go
Normal file
129
go-workspace/src/cmd/nebula-entrypoint/main.go
Normal file
@ -0,0 +1,129 @@
|
||||
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",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
30
go-workspace/src/cmd/update-global-bucket/main.go
Normal file
30
go-workspace/src/cmd/update-global-bucket/main.go
Normal file
@ -0,0 +1,30 @@
|
||||
package update_global_bucket
|
||||
|
||||
import (
|
||||
crypticnet "cryptic-net"
|
||||
"cryptic-net/bootstrap"
|
||||
"log"
|
||||
)
|
||||
|
||||
func Main() {
|
||||
|
||||
env, err := crypticnet.ReadEnv()
|
||||
if err != nil {
|
||||
log.Fatalf("reading envvars: %v", err)
|
||||
}
|
||||
|
||||
client, err := env.Bootstrap.GlobalBucketS3APIClient()
|
||||
if err != nil {
|
||||
log.Fatalf("creating client for global bucket: %v", err)
|
||||
}
|
||||
|
||||
err = bootstrap.PutGarageBoostrapHost(
|
||||
env.Context,
|
||||
client,
|
||||
env.Bootstrap.ThisHost(),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
package daemon
|
||||
|
||||
import "strconv"
|
||||
package crypticnet
|
||||
|
||||
type ConfigFirewall struct {
|
||||
Conntrack ConfigConntrack `yaml:"conntrack"`
|
||||
@ -27,9 +25,9 @@ type ConfigFirewallRule struct {
|
||||
CAName string `yaml:"ca_name,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigStorageAllocation describes the structure of each storage allocation
|
||||
// within the daemon config file.
|
||||
type ConfigStorageAllocation struct {
|
||||
// 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"`
|
||||
@ -38,8 +36,8 @@ type ConfigStorageAllocation struct {
|
||||
AdminPort int `yaml:"admin_port"`
|
||||
}
|
||||
|
||||
// Config describes the structure of the daemon config file.
|
||||
type Config struct {
|
||||
// DaemonYml describes the structure of the daemon.yml file.
|
||||
type DaemonYml struct {
|
||||
DNS struct {
|
||||
Resolvers []string `yaml:"resolvers"`
|
||||
} `yaml:"dns"`
|
||||
@ -48,47 +46,6 @@ type Config struct {
|
||||
Firewall ConfigFirewall `yaml:"firewall"`
|
||||
} `yaml:"vpn"`
|
||||
Storage struct {
|
||||
Allocations []ConfigStorageAllocation
|
||||
Allocations []DaemonYmlStorageAllocation
|
||||
} `yaml:"storage"`
|
||||
}
|
||||
|
||||
func (c *Config) fillDefaults() {
|
||||
|
||||
var firewallGarageInbound []ConfigFirewallRule
|
||||
|
||||
for i := range c.Storage.Allocations {
|
||||
|
||||
if c.Storage.Allocations[i].RPCPort == 0 {
|
||||
c.Storage.Allocations[i].RPCPort = 3900 + (i * 10)
|
||||
}
|
||||
|
||||
if c.Storage.Allocations[i].S3APIPort == 0 {
|
||||
c.Storage.Allocations[i].S3APIPort = 3901 + (i * 10)
|
||||
}
|
||||
|
||||
if c.Storage.Allocations[i].AdminPort == 0 {
|
||||
c.Storage.Allocations[i].AdminPort = 3902 + (i * 10)
|
||||
}
|
||||
|
||||
alloc := c.Storage.Allocations[i]
|
||||
|
||||
firewallGarageInbound = append(
|
||||
firewallGarageInbound,
|
||||
ConfigFirewallRule{
|
||||
Port: strconv.Itoa(alloc.S3APIPort),
|
||||
Proto: "tcp",
|
||||
Host: "any",
|
||||
},
|
||||
ConfigFirewallRule{
|
||||
Port: strconv.Itoa(alloc.RPCPort),
|
||||
Proto: "tcp",
|
||||
Host: "any",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
c.VPN.Firewall.Inbound = append(
|
||||
c.VPN.Firewall.Inbound,
|
||||
firewallGarageInbound...,
|
||||
)
|
||||
}
|
3
go-workspace/src/doc.go
Normal file
3
go-workspace/src/doc.go
Normal file
@ -0,0 +1,3 @@
|
||||
// Package globals defines global constants and variables which are valid
|
||||
// across all cryptic-net processes and sub-processes.
|
||||
package crypticnet
|
226
go-workspace/src/env.go
Normal file
226
go-workspace/src/env.go
Normal file
@ -0,0 +1,226 @@
|
||||
package crypticnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/yamlutil"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
)
|
||||
|
||||
// Names of various environment variables which get set by the entrypoint.
|
||||
const (
|
||||
DaemonYmlPathEnvVar = "_DAEMON_YML_PATH"
|
||||
BootstrapPathEnvVar = "_BOOTSTRAP_PATH"
|
||||
RuntimeDirPathEnvVar = "_RUNTIME_DIR_PATH"
|
||||
DataDirPathEnvVar = "_DATA_DIR_PATH"
|
||||
)
|
||||
|
||||
// Env contains the values of environment variables, as well as other entities
|
||||
// which are useful across all processes.
|
||||
type Env struct {
|
||||
Context context.Context
|
||||
|
||||
AppDirPath string
|
||||
DaemonYmlPath string
|
||||
RuntimeDirPath string
|
||||
DataDirPath string
|
||||
|
||||
// If NewEnv is called with bootstrapOptional, and a bootstrap file is not
|
||||
// found, then these fields will not be set.
|
||||
BootstrapPath string
|
||||
Bootstrap bootstrap.Bootstrap
|
||||
|
||||
thisDaemon DaemonYml
|
||||
thisDaemonOnce sync.Once
|
||||
}
|
||||
|
||||
func getAppDirPath() string {
|
||||
appDirPath := os.Getenv("APPDIR")
|
||||
if appDirPath == "" {
|
||||
appDirPath = "."
|
||||
}
|
||||
return appDirPath
|
||||
}
|
||||
|
||||
// NewEnv calculates an Env instance based on the APPDIR and XDG envvars.
|
||||
//
|
||||
// If bootstrapOptional is true then NewEnv will first check if a bootstrap file
|
||||
// can be found in the expected places, and if not then it will not populate
|
||||
// BootstrapFS or any other fields based on it.
|
||||
func NewEnv(bootstrapOptional bool) (*Env, error) {
|
||||
|
||||
runtimeDirPath := filepath.Join(xdg.RuntimeDir, "cryptic-net")
|
||||
appDirPath := getAppDirPath()
|
||||
|
||||
env := &Env{
|
||||
AppDirPath: appDirPath,
|
||||
DaemonYmlPath: filepath.Join(runtimeDirPath, "daemon.yml"),
|
||||
RuntimeDirPath: runtimeDirPath,
|
||||
DataDirPath: filepath.Join(xdg.DataHome, "cryptic-net"),
|
||||
}
|
||||
|
||||
return env, env.init(bootstrapOptional)
|
||||
}
|
||||
|
||||
// ReadEnv reads an Env from the process's environment variables, rather than
|
||||
// calculating like NewEnv does.
|
||||
func ReadEnv() (*Env, error) {
|
||||
|
||||
var err error
|
||||
|
||||
readEnv := func(key string) string {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
val := os.Getenv(key)
|
||||
|
||||
if val == "" {
|
||||
err = fmt.Errorf("envvar %q not set", key)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
env := &Env{
|
||||
AppDirPath: getAppDirPath(),
|
||||
DaemonYmlPath: readEnv(DaemonYmlPathEnvVar),
|
||||
RuntimeDirPath: readEnv(RuntimeDirPathEnvVar),
|
||||
DataDirPath: readEnv(DataDirPathEnvVar),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return env, env.init(false)
|
||||
}
|
||||
|
||||
// DataDirBootstrapPath returns the path to the bootstrap file within the user's
|
||||
// data dir. If the file does not exist there it will be found in the AppDirPath
|
||||
// by ReloadBootstrap.
|
||||
func (e *Env) DataDirBootstrapPath() string {
|
||||
return filepath.Join(e.DataDirPath, "bootstrap.tgz")
|
||||
}
|
||||
|
||||
// LoadBootstrap sets BootstrapPath to the given value, and loads BootstrapFS
|
||||
// and all derived fields based on that.
|
||||
func (e *Env) LoadBootstrap(path string) error {
|
||||
|
||||
var err error
|
||||
|
||||
if e.Bootstrap, err = bootstrap.FromFile(path); err != nil {
|
||||
return fmt.Errorf("parsing bootstrap.tgz at %q: %w", path, err)
|
||||
}
|
||||
|
||||
e.BootstrapPath = path
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Env) initBootstrap(bootstrapOptional bool) error {
|
||||
|
||||
exists := func(path string) (bool, error) {
|
||||
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
||||
return false, nil
|
||||
} else if err != nil {
|
||||
return false, fmt.Errorf("stat'ing %q: %w", path, err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// start by checking if a bootstrap can be found in the user's data
|
||||
// directory. This will only not be the case if daemon has never been
|
||||
// successfully started.
|
||||
{
|
||||
bootstrapPath := e.DataDirBootstrapPath()
|
||||
|
||||
if exists, err := exists(bootstrapPath); err != nil {
|
||||
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
|
||||
|
||||
} else if exists {
|
||||
return e.LoadBootstrap(bootstrapPath)
|
||||
}
|
||||
}
|
||||
|
||||
// fallback to checking within the AppDir for a bootstrap which has been
|
||||
// embedded into the binary.
|
||||
{
|
||||
bootstrapPath := filepath.Join(e.AppDirPath, "share/bootstrap.tgz")
|
||||
|
||||
if exists, err := exists(bootstrapPath); err != nil {
|
||||
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
|
||||
|
||||
} else if !exists && !bootstrapOptional {
|
||||
return fmt.Errorf("boostrap file not found at %q", bootstrapPath)
|
||||
|
||||
} else if exists {
|
||||
return e.LoadBootstrap(bootstrapPath)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Env) init(bootstrapOptional bool) error {
|
||||
|
||||
var cancel context.CancelFunc
|
||||
e.Context, cancel = context.WithCancel(context.Background())
|
||||
|
||||
signalCh := make(chan os.Signal, 2)
|
||||
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
go func() {
|
||||
sig := <-signalCh
|
||||
cancel()
|
||||
fmt.Fprintf(os.Stderr, "got signal %v, will exit gracefully\n", sig)
|
||||
|
||||
sig = <-signalCh
|
||||
fmt.Fprintf(os.Stderr, "second interrupt signal %v received, force quitting, there may be zombie children left behind, good luck!\n", sig)
|
||||
|
||||
os.Stderr.Sync()
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
if err := e.initBootstrap(bootstrapOptional); err != nil {
|
||||
return fmt.Errorf("initializing bootstrap data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToMap returns the Env as a map of key/value strings. If this map is set into
|
||||
// a process's environment, then that process can read it back using ReadEnv.
|
||||
func (e *Env) ToMap() map[string]string {
|
||||
return map[string]string{
|
||||
DaemonYmlPathEnvVar: e.DaemonYmlPath,
|
||||
BootstrapPathEnvVar: e.BootstrapPath,
|
||||
RuntimeDirPathEnvVar: e.RuntimeDirPath,
|
||||
DataDirPathEnvVar: e.DataDirPath,
|
||||
}
|
||||
}
|
||||
|
||||
// ThisDaemon returns the DaemonYml (loaded from DaemonYmlPath) for the
|
||||
// currently running process.
|
||||
func (e *Env) ThisDaemon() DaemonYml {
|
||||
e.thisDaemonOnce.Do(func() {
|
||||
if err := yamlutil.LoadYamlFile(&e.thisDaemon, e.DaemonYmlPath); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
return e.thisDaemon
|
||||
}
|
||||
|
||||
// BinPath returns the absolute path to a binary in the AppDir.
|
||||
func (e *Env) BinPath(name string) string {
|
||||
return filepath.Join(e.AppDirPath, "bin", name)
|
||||
}
|
@ -1,23 +1,12 @@
|
||||
package garage
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
func randStr(l int) string {
|
||||
b := make([]byte, l)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// IsKeyNotFound returns true if the given error is the result of a key not
|
||||
// being found in a bucket.
|
||||
func IsKeyNotFound(err error) bool {
|
||||
@ -35,26 +24,11 @@ type S3APICredentials struct {
|
||||
Secret string `yaml:"secret"`
|
||||
}
|
||||
|
||||
// NewS3APICredentials returns a new usable instance of S3APICredentials.
|
||||
func NewS3APICredentials() S3APICredentials {
|
||||
return S3APICredentials{
|
||||
ID: randStr(8),
|
||||
Secret: randStr(32),
|
||||
}
|
||||
}
|
||||
|
||||
// NewS3APIClient returns a minio client configured to use the given garage S3 API
|
||||
// endpoint.
|
||||
func NewS3APIClient(addr string, creds S3APICredentials) S3APIClient {
|
||||
|
||||
client, err := minio.New(addr, &minio.Options{
|
||||
func NewS3APIClient(addr string, creds S3APICredentials) (S3APIClient, error) {
|
||||
return minio.New(addr, &minio.Options{
|
||||
Creds: credentials.NewStaticV4(creds.ID, creds.Secret, ""),
|
||||
Region: Region,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("initializing minio client at addr %q and with creds %+v", addr, creds))
|
||||
}
|
||||
|
||||
return client
|
||||
}
|
13
go-workspace/src/garage/garage.go
Normal file
13
go-workspace/src/garage/garage.go
Normal file
@ -0,0 +1,13 @@
|
||||
// Package garage contains helper functions and types which are useful for
|
||||
// setting up garage configs, processes, and deployments.
|
||||
package garage
|
||||
|
||||
const (
|
||||
|
||||
// Region is the region which garage is configured with.
|
||||
Region = "garage"
|
||||
|
||||
// GlobalBucket is the name of the global garage bucket which is
|
||||
// accessible to all hosts in the network.
|
||||
GlobalBucket = "cryptic-net-global"
|
||||
)
|
41
go-workspace/src/garage/infinite_reader.go
Normal file
41
go-workspace/src/garage/infinite_reader.go
Normal file
@ -0,0 +1,41 @@
|
||||
package garage
|
||||
|
||||
import "io"
|
||||
|
||||
type infiniteReader struct {
|
||||
b []byte
|
||||
i int
|
||||
}
|
||||
|
||||
// NewInfiniteReader returns a reader which will produce the given bytes in
|
||||
// repetition. len(b) must be greater than 0.
|
||||
func NewInfiniteReader(b []byte) io.Reader {
|
||||
|
||||
if len(b) == 0 {
|
||||
panic("len(b) must be greater than 0")
|
||||
}
|
||||
|
||||
return &infiniteReader{b: b}
|
||||
}
|
||||
|
||||
func (r *infiniteReader) Read(b []byte) (int, error) {
|
||||
|
||||
// here, have a puzzle
|
||||
|
||||
var n int
|
||||
|
||||
for {
|
||||
|
||||
n += copy(b[n:], r.b[r.i:])
|
||||
|
||||
if r.i > 0 {
|
||||
n += copy(b[n:], r.b[:r.i])
|
||||
}
|
||||
|
||||
r.i = (r.i + n) % len(r.b)
|
||||
|
||||
if n >= len(b) {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
}
|
101
go-workspace/src/garage/infinite_reader_test.go
Normal file
101
go-workspace/src/garage/infinite_reader_test.go
Normal file
@ -0,0 +1,101 @@
|
||||
package garage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInfiniteReader(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
in []byte
|
||||
size int
|
||||
exp []string
|
||||
}{
|
||||
{
|
||||
in: []byte("a"),
|
||||
size: 1,
|
||||
exp: []string{"a"},
|
||||
},
|
||||
{
|
||||
in: []byte("ab"),
|
||||
size: 1,
|
||||
exp: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
in: []byte("ab"),
|
||||
size: 2,
|
||||
exp: []string{"ab"},
|
||||
},
|
||||
{
|
||||
in: []byte("ab"),
|
||||
size: 3,
|
||||
exp: []string{"aba", "bab"},
|
||||
},
|
||||
{
|
||||
in: []byte("ab"),
|
||||
size: 4,
|
||||
exp: []string{"abab"},
|
||||
},
|
||||
{
|
||||
in: []byte("ab"),
|
||||
size: 5,
|
||||
exp: []string{"ababa", "babab"},
|
||||
},
|
||||
{
|
||||
in: []byte("abc"),
|
||||
size: 1,
|
||||
exp: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
in: []byte("abc"),
|
||||
size: 2,
|
||||
exp: []string{"ab", "ca", "bc"},
|
||||
},
|
||||
{
|
||||
in: []byte("abc"),
|
||||
size: 3,
|
||||
exp: []string{"abc"},
|
||||
},
|
||||
{
|
||||
in: []byte("abc"),
|
||||
size: 4,
|
||||
exp: []string{"abca", "bcab", "cabc"},
|
||||
},
|
||||
{
|
||||
in: []byte("abc"),
|
||||
size: 5,
|
||||
exp: []string{"abcab", "cabca", "bcabc"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
|
||||
r := NewInfiniteReader(test.in)
|
||||
buf := make([]byte, test.size)
|
||||
|
||||
assertRead := func(expBuf []byte) {
|
||||
|
||||
n, err := r.Read(buf)
|
||||
|
||||
if !bytes.Equal(buf, expBuf) {
|
||||
t.Fatalf("expected bytes %q, got %q", expBuf, buf)
|
||||
|
||||
} else if n != len(buf) {
|
||||
t.Fatalf("expected n %d, got %d", len(buf), n)
|
||||
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
for _, expStr := range test.exp {
|
||||
assertRead([]byte(expStr))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
66
go-workspace/src/garage/peer.go
Normal file
66
go-workspace/src/garage/peer.go
Normal file
@ -0,0 +1,66 @@
|
||||
package garage
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Peer describes all information necessary to connect to a given garage node.
|
||||
type Peer struct {
|
||||
IP string
|
||||
RPCPort int
|
||||
S3APIPort int
|
||||
}
|
||||
|
||||
// RPCPeerKey deterministically generates a public/private keys which can
|
||||
// be used as a garage node key.
|
||||
//
|
||||
// DANGER: This function will deterministically produce public/private keys
|
||||
// given some arbitrary input. This is NEVER what you want. It's only being used
|
||||
// in cryptic-net for a very specific purpose for which I think it's ok and is
|
||||
// very necessary, and people are probably _still_ going to yell at me.
|
||||
//
|
||||
func (p Peer) RPCPeerKey() (pubKey, privKey []byte) {
|
||||
input := []byte(net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort)))
|
||||
|
||||
// Append the length of the input to the input, so that the input "foo"
|
||||
// doesn't generate the same key as the input "foofoo".
|
||||
input = strconv.AppendInt(input, int64(len(input)), 10)
|
||||
|
||||
pubKey, privKey, err := ed25519.GenerateKey(NewInfiniteReader(input))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return pubKey, privKey
|
||||
}
|
||||
|
||||
// RPCPeerID returns the peer ID of the garage node for use in communicating
|
||||
// over RPC.
|
||||
//
|
||||
// DANGER: See warning on RPCPeerKey.
|
||||
func (p Peer) RPCPeerID() string {
|
||||
pubKey, _ := p.RPCPeerKey()
|
||||
return hex.EncodeToString(pubKey)
|
||||
}
|
||||
|
||||
// RPCAddr returns the address of the peer's RPC port.
|
||||
func (p Peer) RPCAddr() string {
|
||||
return net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort))
|
||||
}
|
||||
|
||||
// RPCPeerAddr returns the full peer address (e.g. "id@ip:port") of the garage
|
||||
// node for use in communicating over RPC.
|
||||
//
|
||||
// DANGER: See warning on RPCPeerKey.
|
||||
func (p Peer) RPCPeerAddr() string {
|
||||
return fmt.Sprintf("%s@%s", p.RPCPeerID(), p.RPCAddr())
|
||||
}
|
||||
|
||||
// S3APIAddr returns the address of the peer's S3 API port.
|
||||
func (p Peer) S3APIAddr() string {
|
||||
return net.JoinHostPort(p.IP, strconv.Itoa(p.S3APIPort))
|
||||
}
|
@ -17,7 +17,7 @@ type GarageTomlData struct {
|
||||
AdminToken string
|
||||
|
||||
RPCAddr string
|
||||
S3APIAddr string
|
||||
APIAddr string
|
||||
AdminAddr string
|
||||
|
||||
BootstrapPeers []string
|
||||
@ -39,7 +39,7 @@ bootstrap_peers = [{{- range .BootstrapPeers }}
|
||||
{{ end -}}]
|
||||
|
||||
[s3_api]
|
||||
api_bind_addr = "{{ .S3APIAddr }}"
|
||||
api_bind_addr = "{{ .APIAddr }}"
|
||||
s3_region = "garage"
|
||||
|
||||
[admin]
|
||||
@ -67,7 +67,9 @@ func WriteGarageTomlFile(path string, data GarageTomlData) error {
|
||||
|
||||
defer file.Close()
|
||||
|
||||
if err := garageTomlTpl.Execute(file, data); err != nil {
|
||||
err = RenderGarageToml(file, data)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("rendering template to file: %w", err)
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ module cryptic-net
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
code.betamike.com/cryptic-io/pmux v0.0.0-20221025185405-29241f144a2d
|
||||
github.com/adrg/xdg v0.4.0
|
||||
github.com/cryptic-io/pmux v0.0.0-20220630194257-a451ee620c83
|
||||
github.com/imdario/mergo v0.3.12
|
||||
github.com/minio/minio-go/v7 v7.0.28
|
||||
github.com/nlepage/go-tarfs v1.1.0
|
||||
@ -12,7 +12,7 @@ require (
|
||||
github.com/slackhq/nebula v1.6.1
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
require (
|
@ -1,9 +1,7 @@
|
||||
code.betamike.com/cryptic-io/pmux v0.0.0-20221020185531-7a7868003822 h1:c7Eu2h8gXOpOfhC1LvSYLNfiSsWTyvdI1XVpUuqMFHE=
|
||||
code.betamike.com/cryptic-io/pmux v0.0.0-20221020185531-7a7868003822/go.mod h1:cBuEN/rkaM/GH24uQroX/++qDmte+mLudDUqMt6XJWs=
|
||||
code.betamike.com/cryptic-io/pmux v0.0.0-20221025185405-29241f144a2d h1:s6nDTg23o9ujZZnl8ohZBDoG4SqPUyFfvod9DQjwmNU=
|
||||
code.betamike.com/cryptic-io/pmux v0.0.0-20221025185405-29241f144a2d/go.mod h1:cBuEN/rkaM/GH24uQroX/++qDmte+mLudDUqMt6XJWs=
|
||||
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||
github.com/cryptic-io/pmux v0.0.0-20220630194257-a451ee620c83 h1:0W4j4Rg0hMPyUpuvZuj1u9Gmgah9SSsfAUeNvTxo2BA=
|
||||
github.com/cryptic-io/pmux v0.0.0-20220630194257-a451ee620c83/go.mod h1:fg/CCfMpcbqO7/iqLjoYskopsVO3JqdD3Oij83+YXSI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@ -102,6 +100,7 @@ gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
160
go-workspace/src/nebula/nebula.go
Normal file
160
go-workspace/src/nebula/nebula.go
Normal file
@ -0,0 +1,160 @@
|
||||
// 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,4 +1,4 @@
|
||||
package main
|
||||
package crypticnet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -12,13 +12,33 @@ import (
|
||||
|
||||
var errDaemonNotRunning = errors.New("no cryptic-net daemon process running")
|
||||
|
||||
func lockFilePath() string {
|
||||
return filepath.Join(envRuntimeDirPath, "lock")
|
||||
// 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
|
||||
}
|
||||
|
||||
func writeLock() error {
|
||||
type procLock struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
lockFilePath := lockFilePath()
|
||||
// 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,
|
||||
@ -43,31 +63,11 @@ func writeLock() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// returns a cleanup function which will clean up the created runtime directory.
|
||||
func setupAndLockRuntimeDir() (func(), error) {
|
||||
|
||||
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", envRuntimeDirPath)
|
||||
|
||||
if err := os.MkdirAll(envRuntimeDirPath, 0700); err != nil {
|
||||
return nil, fmt.Errorf("creating directory %q: %w", envRuntimeDirPath, err)
|
||||
|
||||
} else if err := writeLock(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func() {
|
||||
fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", envRuntimeDirPath)
|
||||
if err := os.RemoveAll(envRuntimeDirPath); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", envRuntimeDirPath, err)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// checks that the lock file exists and that the process which created it also
|
||||
// still exists.
|
||||
func assertLock() error {
|
||||
func (pl *procLock) AssertLock() error {
|
||||
|
||||
lockFilePath := lockFilePath()
|
||||
lockFilePath := pl.path()
|
||||
|
||||
lockFile, err := os.Open(lockFilePath)
|
||||
|
24
go-workspace/src/tarutil/tarutil.go
Normal file
24
go-workspace/src/tarutil/tarutil.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Package tarutil implements utilities which are useful for interacting with
|
||||
// tar and tgz files.
|
||||
package tarutil
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
|
||||
"github.com/nlepage/go-tarfs"
|
||||
)
|
||||
|
||||
// FSFromReader returns a FS instance which will read the contents of a tgz
|
||||
// file from the given Reader.
|
||||
func FSFromReader(r io.Reader) (fs.FS, error) {
|
||||
gf, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("un-gziping: %w", err)
|
||||
}
|
||||
defer gf.Close()
|
||||
|
||||
return tarfs.New(gf)
|
||||
}
|
112
go-workspace/src/tarutil/tgz_writer.go
Normal file
112
go-workspace/src/tarutil/tgz_writer.go
Normal file
@ -0,0 +1,112 @@
|
||||
package tarutil
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TGZWriter is a utility for writing tgz files. If an internal error is
|
||||
// encountered by any method then all subsequent methods will be no-ops, and
|
||||
// Close() will return that error (after closing out resources).
|
||||
type TGZWriter struct {
|
||||
gzipW *gzip.Writer
|
||||
tarW *tar.Writer
|
||||
err error
|
||||
|
||||
dirsWritten map[string]bool
|
||||
}
|
||||
|
||||
// NewTGZWriter initializes and returns a new instance of TGZWriter which will
|
||||
// write all data to the given io.Writer.
|
||||
func NewTGZWriter(w io.Writer) *TGZWriter {
|
||||
gzipW := gzip.NewWriter(w)
|
||||
tarW := tar.NewWriter(gzipW)
|
||||
return &TGZWriter{
|
||||
gzipW: gzipW,
|
||||
tarW: tarW,
|
||||
dirsWritten: map[string]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
// Close cleans up all open resources being held by TGZWriter, and returns the
|
||||
// first internal error which was encountered during its operation (if any).
|
||||
func (w *TGZWriter) Close() error {
|
||||
w.tarW.Close()
|
||||
w.gzipW.Close()
|
||||
return w.err
|
||||
}
|
||||
|
||||
func (w *TGZWriter) writeDir(path string) {
|
||||
|
||||
if w.err != nil {
|
||||
return
|
||||
|
||||
} else if path != "." {
|
||||
w.writeDir(filepath.Dir(path))
|
||||
}
|
||||
|
||||
if path == "." {
|
||||
path = "./"
|
||||
} else {
|
||||
path = "./" + strings.TrimPrefix(path, "./")
|
||||
path = path + "/"
|
||||
}
|
||||
|
||||
if w.dirsWritten[path] {
|
||||
return
|
||||
}
|
||||
|
||||
err := w.tarW.WriteHeader(&tar.Header{
|
||||
Name: path,
|
||||
Mode: 0700,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
w.err = fmt.Errorf("writing header for directory %q: %w", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.dirsWritten[path] = true
|
||||
}
|
||||
|
||||
// WriteFile writes a file to the tgz archive. The file will automatically be
|
||||
// rooted to the "." directory, and any sub-directories the file exists in
|
||||
// should have already been created.
|
||||
func (w *TGZWriter) WriteFile(path string, size int64, body io.Reader) {
|
||||
|
||||
if w.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
path = "./" + strings.TrimPrefix(path, "./")
|
||||
|
||||
w.writeDir(filepath.Dir(path))
|
||||
|
||||
err := w.tarW.WriteHeader(&tar.Header{
|
||||
Name: path,
|
||||
Size: size,
|
||||
Mode: 0400,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
w.err = fmt.Errorf("writing header for file %q: %w", path, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := io.Copy(w.tarW, body); err != nil {
|
||||
w.err = fmt.Errorf("writing file body of file %q: %w", path, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// WriteFileBytes is a shortcut for calling WriteFile with the given byte slice
|
||||
// being used as the file body.
|
||||
func (w *TGZWriter) WriteFileBytes(path string, body []byte) {
|
||||
bodyR := bytes.NewReader(body)
|
||||
w.WriteFile(path, bodyR.Size(), bodyR)
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
{
|
||||
|
||||
stdenv,
|
||||
glibcStatic,
|
||||
|
||||
}: 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"
|
||||
];
|
||||
}
|
25
nix/pkgs.nix
25
nix/pkgs.nix
@ -2,8 +2,7 @@ rec {
|
||||
|
||||
overlays = [
|
||||
|
||||
# Make buildGoModules use static compilation by default, and use go 1.18
|
||||
# everywhere.
|
||||
# Make both buildGoModules use static compilation by default.
|
||||
(final: prev:
|
||||
|
||||
let
|
||||
@ -17,22 +16,26 @@ rec {
|
||||
|
||||
in {
|
||||
|
||||
go = prev.go_1_18;
|
||||
buildGoModule = args: prev.buildGo118Module (buildArgs // args);
|
||||
buildGoModule = args: prev.buildGoModule (buildArgs // args);
|
||||
buildGo118Module = args: prev.buildGo118Module (buildArgs // args);
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
(final: prev: { rebase = prev.callPackage ./rebase.nix {}; })
|
||||
(final: prev: { yq-go = prev.callPackage ./yq-go.nix {}; })
|
||||
|
||||
(final: prev: { nebula = prev.callPackage ./nebula.nix {
|
||||
buildGoModule = prev.buildGo118Module;
|
||||
}; })
|
||||
|
||||
];
|
||||
|
||||
version = "22-05";
|
||||
rev = "2aec372cdcd4d73b94863611fea70e0884270fdc";
|
||||
|
||||
src = fetchTarball {
|
||||
name = "nixpkgs-${version}";
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz";
|
||||
stableSrc = fetchTarball {
|
||||
name = "nixpkgs-22-05";
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/2aec372cdcd4d73b94863611fea70e0884270fdc.tar.gz";
|
||||
sha256 = "1pbfhlh4v8l70p44gspsci3i6w0wk70vaisiawg3jhka2nxb8367";
|
||||
};
|
||||
|
||||
pkgs = import src { inherit overlays; };
|
||||
stable = import stableSrc { inherit overlays; };
|
||||
}
|
||||
|
18
nix/rebase.nix
Normal file
18
nix/rebase.nix
Normal file
@ -0,0 +1,18 @@
|
||||
# rebase is a helper which takes all files/dirs under oldroot, and
|
||||
# creates a new derivation with those files/dirs copied under newroot
|
||||
# (where newroot is a relative path to the root of the derivation).
|
||||
|
||||
{
|
||||
|
||||
stdenv,
|
||||
|
||||
}: name: oldroot: newroot: stdenv.mkDerivation {
|
||||
|
||||
inherit name oldroot newroot;
|
||||
|
||||
builder = builtins.toFile "builder.sh" ''
|
||||
source $stdenv/setup
|
||||
mkdir -p "$out"/"$newroot"
|
||||
cp -rL "$oldroot"/* "$out"/"$newroot"
|
||||
'';
|
||||
}
|
21
nix/wait-for.nix
Normal file
21
nix/wait-for.nix
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
fetchFromGitHub,
|
||||
stdenv,
|
||||
}: stdenv.mkDerivation rec {
|
||||
|
||||
pname = "cryptic-net-wait-for";
|
||||
version = "2.2.2";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "eficode";
|
||||
repo = "wait-for";
|
||||
rev = "v${version}";
|
||||
sha256 = "sha256-qYeBOF63/+8bbFHiR6HT2mMQDFKCVkLNzIGLeEZJ4sk=";
|
||||
};
|
||||
|
||||
builder = builtins.toFile "builder.sh" ''
|
||||
source $stdenv/setup
|
||||
mkdir -p "$out"/bin
|
||||
cp "$src"/wait-for "$out"/bin/
|
||||
'';
|
||||
}
|
19
nix/yq-go.nix
Normal file
19
nix/yq-go.nix
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
|
||||
buildGoModule,
|
||||
fetchFromGitHub,
|
||||
|
||||
}: buildGoModule rec {
|
||||
|
||||
pname = "yq-go";
|
||||
version = "4.21.1";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "mikefarah";
|
||||
repo = "yq";
|
||||
rev = "v${version}";
|
||||
sha256 = "sha256-283xe7FVHYSsRl4cZD7WDzIW1gqNAFsNrWYJkthZheU=";
|
||||
};
|
||||
|
||||
vendorSha256 = "sha256-F11FnDYJ59aKrdRXDPpKlhX52yQXdaN1sblSkVI2j9w=";
|
||||
}
|
Loading…
Reference in New Issue
Block a user