Compare commits

..

No commits in common. "5c8c24e73e665ae119cf92f197feddabdce7a67a" and "f720d7accdc2abda9bb6c6fcb5b83174a60a22bc" have entirely different histories.

85 changed files with 3262 additions and 2859 deletions

View File

@ -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
View File

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

View File

@ -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

View File

@ -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)

View File

@ -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 []);
};

View 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
View File

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

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

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

View File

@ -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.

View File

@ -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/

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,6 +1,6 @@
{
pkgs ? (import ../nix/pkgs.nix).pkgs,
pkgs ? (import ../nix/pkgs.nix).stable,
}: pkgs.mkShell {
name = "cryptic-net-build-docs";

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -1,14 +0,0 @@
{
buildGoModule,
}: buildGoModule {
pname = "cryptic-net-entrypoint";
version = "unstable";
src = ./src;
vendorSha256 = "sha256-1mHD0tmITlGjeo6F+Dvd2TdEPzxWtndy/J+uGHWKen4=";
subPackages = [
"cmd/entrypoint"
];
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
)
},
}

View File

@ -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)
}

View File

@ -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)
}
}
},
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -1,3 +0,0 @@
// Package dnsmasq contains helper functions and types which are useful for
// setting up dnsmasq configs, processes, and deployments.
package dnsmasq

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -24,8 +24,9 @@ in rec {
env = buildEnv {
name = "cryptic-net-garage";
paths = [
garage.pkgs.amd64.release
garage
minioClient
./src
];
};

View File

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

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

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

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

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

View File

@ -0,0 +1,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()
}

View 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
}

View File

@ -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
}

View File

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

View File

@ -0,0 +1,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
}

View File

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

View File

@ -0,0 +1,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()
}

View 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,
)
},
}

View 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)
}
}
},
}

View 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},
}
}

View File

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

View File

@ -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,
)
)

View File

@ -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,
)

View File

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

View File

@ -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 {

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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)
}
}

View File

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

View File

@ -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
View File

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

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

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

View File

@ -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
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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)
}

View File

@ -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 (

View File

@ -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=

View 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
}

View File

@ -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)

View File

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

View File

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

View File

@ -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"
];
}

View File

@ -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
View 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
View 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
View 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=";
}