commit b35a3d657465287d88b82dbfd2a6a0d8be604dcc Author: Brian Picciano Date: Tue Apr 20 15:31:37 2021 -0600 First public commit There has been over 1 year of commit history leading up to this point, but almost all of that has had some kind network configuration or secrets built into the code. As of today all of that has been removed, and the codebase can finally be published! I am keeping a private copy of the previous commit history, though it's unclear if it will ever be able to be published. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..faf7e4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*-bin +*-admin.tgz* +*-bootstrap.tgz +result diff --git a/AppDir/AppRun b/AppDir/AppRun new file mode 100755 index 0000000..6ef0c8e --- /dev/null +++ b/AppDir/AppRun @@ -0,0 +1,4 @@ +#!/bin/sh + +export PATH=$APPDIR/bin +exec cryptic-net-main entrypoint "$@" diff --git a/AppDir/bin/wait-for-ip b/AppDir/bin/wait-for-ip new file mode 100644 index 0000000..988e20b --- /dev/null +++ b/AppDir/bin/wait-for-ip @@ -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 "$@" diff --git a/AppDir/cryptic-logo.svg b/AppDir/cryptic-logo.svg new file mode 100644 index 0000000..7d3a2b1 --- /dev/null +++ b/AppDir/cryptic-logo.svg @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/AppDir/cryptic-net.desktop b/AppDir/cryptic-net.desktop new file mode 100644 index 0000000..08d7219 --- /dev/null +++ b/AppDir/cryptic-net.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Cryptic Net +Name[en]=Cryptic Net +Exec=AppRun + +Icon=cryptic-logo +Type=Application +Categories=Network; diff --git a/AppDir/etc/daemon.yml b/AppDir/etc/daemon.yml new file mode 100644 index 0000000..4252123 --- /dev/null +++ b/AppDir/etc/daemon.yml @@ -0,0 +1,76 @@ + +# +# This file defines all configuration directives which can be modified for +# the cryptic-net daemon at runtime. All values specified here are the +# default values. +# +################################################################################ + +# A DNS service runs as part of every cryptic-net process. +dns: + + # list of IPs that the DNS service will use to resolve non-cryptic.io + # hostnames. + resolvers: + - 1.1.1.1 + - 8.8.8.8 + +# A VPN service runs as part of every cryptic-net process. +vpn: + + # Enable this field if the vpn will be made to be publicly accessible at a + # particular IP or hostname. At least one host must have a publicly accessible + # VPN process at any given moment. + #public_addr: "host:port" + + # Firewall directives, as described here: + # https://github.com/slackhq/nebula/blob/v1.4.0/examples/config.yml#L216 + firewall: + + conntrack: + tcp_timeout: 12m + udp_timeout: 3m + default_timeout: 10m + max_connections: 100000 + + outbound: + + # Allow all outbound traffic from this node. + - port: any + proto: any + host: any + + inbound: + + # If any storage allocations are declared below, the ports used will be + # allowed here automatically. + + # Allow ICMP between hosts. + - port: any + proto: icmp + host: any + + # That's it. + +storage: + + # Allocations defined here are used to store data in the distributed storage + # network. If no allocations are defined then no data is replicated to this + # node. + # + # The data directory of each allocation should be on a different drive, while + # the meta directories can be anywhere (ideally on an SSD). + # + # Capacity declares how many gigabytes can be stored in each allocation, and + # is required. It must be a multiple of 100. + # + # The various ports are all required and must all be unique within and across + # allocations. + allocations: + + #- data_path: /foo/bar/data + # meta_path: /foo/bar/meta + # capacity: 1200 + # api_port: 3900 + # rpc_port: 3901 + # web_port: 3902 diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a00369 --- /dev/null +++ b/README.md @@ -0,0 +1,107 @@ + +**_This project is currently in early-access deep-alpha testing phase. Do not +rely on it for anything._** + +----- + +# cryptic-net + +The cryptic-net project provides the foundation for an **autonomous community +cloud infrastructure**. + +The core components of cryptic-net, currently, are: + +* A VPN which enables direct peer-to-peer communication, while transparently + handling NAT punching. + +* An S3-compatible network database which replicates and shards its dataset + amongst all hosts providing storage. Each user can provide as much storage as + they care to, if any. + +These components are wrapped into a single binary, with all manual setup being +automated away by glue code. cryptic-net takes "just works" very seriously. + +Participants are able to build upon these foundations to host services for +themselves and others. They can be assured that their communications are private +and their storage is reliable, all with zero administrative overhead and zero +third parties involved. + +[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 +know which documents they need to care about. + +### User Docs + +Users are participants who use cryptic-net resources, but do not provide any +network or storage resources themselves. Users may be accessing the network from +a laptop, and so are not expected to be online at any particular moment. + +Documentation for users: + +* [Getting Started](docs/user/getting-started.md) +* [Creating a daemon.yml File](docs/user/creating-a-daemonyml-file.md) +* [Using DNS](docs/user/using-dns.md) (advanced) +* Restic example (TODO) + +### Operator Docs + +Operators are participants who own a dedicated host which they can expect to be +always-online (to the extent that's possible in a residential environment). +Operator hosts will need at least one of the following to be useful: + +* A static public IP, or a dynamic public IP with [dDNS][ddns] set up. + +* At least 100GB of unused storage which can be reserved for the network. + +Operators are expected to be familiar with server administration, and to not be +afraid of a terminal. + +Documentation for operators: + +* [Contributing Storage](docs/operator/contributing-storage.md) +* [Contributing a Lighthouse](docs/operator/contributing-a-lighthouse.md) +* [Managing garage](docs/operator/managing-garage.md) + +[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/ + +### Admin Docs + +Admins are participants who control membership within the network. They are +likely operators as well. + +Documentation for admins: + +* [Adding a Host to the Network](docs/admin/adding-a-host-to-the-network.md) +* Removing a Host From the Network (TODO) + +### Dev Docs + +Dev may or may not be participants in any particular cryptic-net. They instead +are those who work on the actual code for cryptic-net. + +Documentation for devs: + +* [Design Principles](docs/dev/design-principles.md) +* [`cryptic-net daemon` process tree](docs/dev/daemon-process-tree.svg): Diagram + describing the [pmux](https://github.com/cryptic-io/pmux) process tree created + by `cryptic-net daemon` at runtime. +* [Rebuilding Documentation](docs/dev/rebuilding-documentation.md) + +## Misc + +Besides documentation, there are a few other pages which might be useful: + +* [Roadmap][roadmap] + +[roadmap]: docs/roadmap.md diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..8afdf10 --- /dev/null +++ b/default.nix @@ -0,0 +1,116 @@ +{ + + pkgs ? (import ./nix/pkgs.nix).stable, + bootstrap ? null, + +}: rec { + + rootedBootstrap = pkgs.stdenv.mkDerivation { + name = "cryptic-net-rooted-bootstrap"; + + src = bootstrap; + + builder = builtins.toFile "builder.sh" '' + source $stdenv/setup + mkdir -p "$out"/share + cp "$src" "$out"/share/bootstrap.tgz + ''; + }; + + version = pkgs.stdenv.mkDerivation { + name = "cryptic-net-version"; + + buildInputs = [ pkgs.git pkgs.go ]; + src = ./.; + inherit bootstrap; + + builder = builtins.toFile "builder.sh" '' + source $stdenv/setup + + versionFile=version + + if [ "$bootstrap" != "" ]; then + hostName=$(tar -xzf "$bootstrap" --to-stdout ./hostname) + echo "Built for host: $hostName" >> "$versionFile" + fi + + echo "Build date: $(date)" >> "$versionFile" + echo "Git status: $(cd "$src" && git describe --always --long --dirty=' (dirty)')" >> "$versionFile" + echo "Go version: $(go version)" >> "$versionFile" + echo "Build host info: $(uname -srvm)" >> "$versionFile" + + mkdir -p "$out"/share + cp "$versionFile" "$out"/share + ''; + }; + + goWorkspace = pkgs.callPackage ./go-workspace {}; + + dnsmasq = (pkgs.callPackage ./dnsmasq { + glibcStatic = pkgs.glibc.static; + }).env; + + garage = (pkgs.callPackage ./garage {}).env; + + waitFor = pkgs.callPackage ./nix/wait-for.nix {}; + + appDir = pkgs.buildEnv { + name = "cryptic-net-AppDir"; + paths = [ + + pkgs.pkgsStatic.bash + pkgs.pkgsStatic.coreutils + pkgs.pkgsStatic.unixtools.ping + pkgs.pkgsStatic.netcat # required by waitFor + pkgs.pkgsStatic.gnutar + pkgs.pkgsStatic.gzip + + # custom packages from ./pkgs.nix + pkgs.yq-go + pkgs.nebula + + ./AppDir + version + dnsmasq + garage + waitFor + goWorkspace.crypticNetMain + + ] ++ (if bootstrap != null then [ rootedBootstrap ] else []); + }; + + appimagetool = pkgs.callPackage ./nix/appimagetool.nix {}; + + appImage = pkgs.stdenv.mkDerivation { + name = "cryptic-net-AppImage"; + src = appDir; + + buildInputs = [ appimagetool ]; + + ARCH = "x86_64"; + + builder = builtins.toFile "build.sh" '' + source $stdenv/setup + cp -rL "$src" cryptic-net + chmod +w cryptic-net -R + mkdir $out + appimagetool cryptic-net "$out/cryptic-net" + ''; + }; + + service = pkgs.writeText "cryptic-service" '' + [Unit] + Description=cryptic nebula + Requires=network.target + After=network.target + + [Service] + Restart=always + RestartSec=1s + User=root + ExecStart=${appImage}/cryptic-net + + [Install] + WantedBy=multi-user.target + ''; +} diff --git a/dnsmasq/bin/dnsmasq-entrypoint b/dnsmasq/bin/dnsmasq-entrypoint new file mode 100644 index 0000000..2b989bb --- /dev/null +++ b/dnsmasq/bin/dnsmasq-entrypoint @@ -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" ./nebula/hosts + + thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname) + thisHostIP=$(cat "$tmp"/nebula/hosts/"$thisHostName".yml | yq '.ip') + + echo "listen-address=$thisHostIP" >> "$conf_path" + + ls -1 "$tmp"/nebula/hosts | while read hostYml; do + + hostName=$(echo "$hostYml" | cut -d. -f1) + hostIP=$(cat "$tmp"/nebula/hosts/"$hostYml" | yq '.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" diff --git a/dnsmasq/default.nix b/dnsmasq/default.nix new file mode 100644 index 0000000..7202339 --- /dev/null +++ b/dnsmasq/default.nix @@ -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 + ]; + }; + +} diff --git a/dnsmasq/etc/base.conf b/dnsmasq/etc/base.conf new file mode 100644 index 0000000..58501ff --- /dev/null +++ b/dnsmasq/etc/base.conf @@ -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 +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + diff --git a/docs/admin/adding-a-host-to-the-network.md b/docs/admin/adding-a-host-to-the-network.md new file mode 100644 index 0000000..a44f2e0 --- /dev/null +++ b/docs/admin/adding-a-host-to-the-network.md @@ -0,0 +1,94 @@ +# Adding a Host to the Network + +This document guides an admin through adding a single host to the network. Keep +in mind that the steps described here must be done for _each_ host the user +wishes to add. + +There are two ways for a user to add a host to the cryptic-net network. + +- If the user is savy enough to obtain their own `cryptic-net` binary, they can + do so. The admin can then generate a `bootstrap.tgz` file for their host, + give that to the user, and the user can run `cryptic-net daemon` using that + bootstrap file. + +- If the user is not so savy, the admin can generate a custom `cryptic-net` + binary with the `bootstrap.tgz` embedded into it. The user can be given this + binary and run `cryptic-net daemon` without any configuration on their end. + +From the admin's perspective the only difference between these cases is one +extra step. + +## Step 1: Choose Hostname + +The user will need to provide you with a name for their host. The name should +conform to the following rules: + +* It should only contain lowercase letters, numbers, and hyphens. + +* It should begin with a letter. + +* It should end with a letter or number. + +## 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 the VPN's +set of allowed IPs. + +The admin should perform the following command from their own host: + +``` +cryptic-net hosts add --name --ip +``` + +## Step 3: Create a `bootstrap.tgz` File + +Access to an `admin.tgz` file is required for this step. + +To create a `bootstrap.tgz` file for the new host, the admin should perform the +following command from their own host: + +``` +cryptic-net hosts make-bootstrap \ + --name \ + --admin-path \ + > bootstrap.tgz +``` + +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.tgz` file can be given to them as-is, and they can proceed with +running their host's `cryptic-net daemon`. + +### Encrypted `admin.tgz` + +If `admin.tgz` is kept in an encrypted format on disk (it should be!) then the +decrypted form can be piped into `make-bootstrap` over stdin. For example, if +GPG is being used to secure `admin.tgz` then the following could be used to +generate a `bootstrap.tgz`: + +``` +gpg -d | cryptic-net hosts make-boostrap \ + --name \ + --admin-path - \ + > bootstrap.tgz +``` + +Note that the value of `--admin-path` is `-`, indicating that `admin.tgz` should +be read from stdin. + +## Step 4: Optionally, Build Binary + +If you wish to embed the `bootstrap.tgz` into a custom binary for the user (to +make installation _extremely_ easy for them) then you can run the following: + +``` +nix-build --arg bootstrap -A appImage +``` + +The resulting binary can be found in the `result` directory which is created. +Note that this binary should be treated like a `bootstrap.tgz` in terms of its +uniqueness and sensitivity. diff --git a/docs/dev/daemon-process-tree.plantuml b/docs/dev/daemon-process-tree.plantuml new file mode 100644 index 0000000..5de49a6 --- /dev/null +++ b/docs/dev/daemon-process-tree.plantuml @@ -0,0 +1,69 @@ +@startuml +hide empty description + +state "./cryptic-net daemon -c ./daemon.yml" as init + +state AppDir { + + note "All relative paths are relative to the root of the AppDir" as N1 + + state "./AppRun" as AppRun { + AppRun : * Set PATH to APPDIR/bin + } + + state "./bin/entrypoint" as entrypoint { + entrypoint : * Merge given and default daemon.yml files + entrypoint : * Create runtime dir at $_RUNTIME_DIR_PATH + entrypoint : * Lock runtime dir + entrypoint : * Run child processes + } + + init --> AppRun : exec + AppRun --> entrypoint : exec + + state "./bin/dnsmasq-entrypoint" as dnsmasqEntrypoint { + dnsmasqEntrypoint : * Create $_RUNTIME_DIR_PATH/dnsmasq.conf + } + + state "./bin/dnsmasq -d -C $_RUNTIME_DIR_PATH/dnsmasq.conf" as dnsmasq + + entrypoint --> dnsmasqEntrypoint : child + dnsmasqEntrypoint --> dnsmasq : exec + + state "./bin/nebula-entrypoint" as nebulaEntrypoint { + nebulaEntrypoint : * Create $_RUNTIME_DIR_PATH/nebula.yml + } + + state "./bin/nebula -config $_RUNTIME_DIR_PATH/nebula.yml" as nebula + state "./bin/nebula-update-global-bucket" as nebulaUpdateGlobalBucket { + nebulaUpdateGlobalBucket : * Runs once then exits + nebulaUpdateGlobalBucket : * Updates network topo data in garage global bucket (used for bootstrapping) + } + + entrypoint --> nebulaEntrypoint : child + nebulaEntrypoint --> nebula : exec + nebulaEntrypoint --> nebulaUpdateGlobalBucket : child + + state "./bin/garage-entrypoint" as garageEntrypoint { + garageEntrypoint : * Create $_RUNTIME_DIR_PATH/garage-N.toml\n (one per storage allocation) + garageEntrypoint : * Run child processes + } + + state "./bin/garage -c $_RUNTIME_DIR_PATH/garage-N.toml server" as garage + state "./bin/garage-apply-layout-diff" as garageApplyLayoutDiff { + garageApplyLayoutDiff : * Runs once then exits + garageApplyLayoutDiff : * Updates cluster topo + } + state "./bin/garage-update-global-bucket" as garageUpdateGlobalBucket { + garageUpdateGlobalBucket : * Runs once then exits + garageUpdateGlobalBucket : * Updates cluster topo data in garage global bucket (used for bootstrapping) + } + + entrypoint --> garageEntrypoint : child (only if >1 storage allocation defined in daemon.yml) + garageEntrypoint --> garage : child (one per storage allocation) + garageEntrypoint --> garageApplyLayoutDiff : child + garageEntrypoint --> garageUpdateGlobalBucket : child +} + +@enduml + diff --git a/docs/dev/daemon-process-tree.svg b/docs/dev/daemon-process-tree.svg new file mode 100644 index 0000000..47d5ac7 --- /dev/null +++ b/docs/dev/daemon-process-tree.svg @@ -0,0 +1,90 @@ +AppDirAll relative paths are relative to the root of the AppDir./bin/dnsmasq -d -C $_RUNTIME_DIR_PATH/dnsmasq.conf./bin/nebula -config $_RUNTIME_DIR_PATH/nebula.yml./bin/garage -c $_RUNTIME_DIR_PATH/garage-N.toml server./AppRunSet PATH to APPDIR/bin./bin/entrypointMerge given and default daemon.yml filesCreate runtime dir at $_RUNTIME_DIR_PATHLock runtime dirRun child processes./bin/dnsmasq-entrypointCreate $_RUNTIME_DIR_PATH/dnsmasq.conf./bin/nebula-entrypointCreate $_RUNTIME_DIR_PATH/nebula.yml./bin/nebula-update-global-bucketRuns once then exitsUpdates network topo data in garage global bucket (used for bootstrapping)./bin/garage-entrypointCreate $_RUNTIME_DIR_PATH/garage-N.toml(one per storage allocation)Run child processes./bin/garage-apply-layout-diffRuns once then exitsUpdates cluster topo./bin/garage-update-global-bucketRuns once then exitsUpdates cluster topo data in garage global bucket (used for bootstrapping)./cryptic-net daemon -c ./daemon.ymlexecexecchildexecchildexecchildchild (only if >1 storage allocation defined in daemon.yml)child (one per storage allocation)childchild \ No newline at end of file diff --git a/docs/dev/design-principles.md b/docs/dev/design-principles.md new file mode 100644 index 0000000..228fb29 --- /dev/null +++ b/docs/dev/design-principles.md @@ -0,0 +1,21 @@ +# Design Principles + +The following points form the basis for all design decisions made within the +cryptic-net project. + +* The UX is aggressively optimized to eliminate manual intervention by users. + All other concerns are secondary. The concept of "UX" extends beyond GUI + interfaces, and encompasses all interactions of any sort with a cryptic-net + process. + +* All resources within a cryptic-net are expected to be hosted on hardware owned + by community members, for example home media servers or gaming rigs. Thus, a + cryptic-net is fully autonomous. + +* Hardware resources are expected to be heterogenous and geographically + dispersed. + +* It is expected that a single host might be a part of multiple, independent + cryptic-net networks. These should not conflict with each other, nor share + resources. + diff --git a/docs/dev/rebuilding-documentation.md b/docs/dev/rebuilding-documentation.md new file mode 100644 index 0000000..691fd3e --- /dev/null +++ b/docs/dev/rebuilding-documentation.md @@ -0,0 +1,14 @@ +# Rebuilding Documentation + +Most documentation for cryptic-net takes the form of markdown (`.md`) files, +which do not require any build step. There are a few other kinds of files, such +as `.plantuml` files, which do require a build step. If these are changed then +their artifacts should be rebuilt by doing: + +``` +cd docs +nix-shell +``` + +The resulting artifact changes should be committed to the repository alongside +the source changes. diff --git a/docs/operator/contributing-a-lighthouse.md b/docs/operator/contributing-a-lighthouse.md new file mode 100644 index 0000000..def4f2f --- /dev/null +++ b/docs/operator/contributing-a-lighthouse.md @@ -0,0 +1,46 @@ +# Contributing a Lighthouse + +The [nebula][nebula] project provides the VPN component which is used by +cryptic-net. Every nebula network requires at least one (but preferably more) +publicly accessible hosts. These hosts are called lighthouses. + +Lighthouses do _not_ route traffic between hosts on the VPN. Rather, they +coordinate VPN hosts to talk directly to each other, and handle the details of +NAT punching through any NATs that hosts might be behind. As such, they are very +lightweight to run, and require no storage resources at all. + +If your host machine has a public static IP, or a dynamic public IP with +[dDNS][ddns] set up, then it can contribute a lighthouse for cryptic-net. + +[nebula]: https://github.com/slackhq/nebula +[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/ + +## Setup network + +The first step is to pick a UDP port you will expose the lighthouse on. It +doesn't really matter which port you pick, but a number over 1024 is +recommended. + +If your host is behind a NAT, ensure that the gateway is setup to forward UDP +traffic on that port to your host. + +Configure your host's firewall to allow all UDP traffic on that port. + +## Create daemon.yml + +First, if you haven't already, [create a `daemon.yml` +file](../user/creating-a-daemonyml-file.md). This will be used to +configure your `cryptic-net daemon` process with the public address that other +hosts can find your daemon on. + +## Edit daemon.yml + +Open your `daemon.yml` file in a text editor, and find the `vpn.public_addr` +field. Update that field to reflect your host's IP/DNS name and your chosen UDP +port. + +## Restart the Daemon + +With the `daemon.yml` configured, you should restart your `cryptic-net daemon` +process. On startup the daemon will add its public address to the global +configuration, which other hosts will pick up on and begin using. diff --git a/docs/operator/contributing-storage.md b/docs/operator/contributing-storage.md new file mode 100644 index 0000000..ca13377 --- /dev/null +++ b/docs/operator/contributing-storage.md @@ -0,0 +1,67 @@ +# Contributing Storage + +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 + +First, if you haven't already, [create a `daemon.yml` +file](../user/creating-a-daemonyml-file.md). This will be used to +configure your `cryptic-net daemon` process with the storage locations and +capacities you want to contribute. + +## Edit daemon.yml + +Open your `daemon.yml` file in a text editor, and find the +`storage.allocations` section. + +Each allocation in the allocations list describes the space being contributed +from a single physical drive. If you only have one drive then you will only need +one allocation listed. + +The comments in the file should be self-explanatory, but ask your admin if you +need any clarification. + +Here are an example set of allocations for a host which is contributing space +from two separate drives: + +``` +storage: + allocations: + + # 1.2 TB are being shared from drive1 + - data_path: /mnt/drive1/cryptic-net/data + meta_path: /mnt/drive1/cryptic-net/meta + capacity: 1200 + api_port: 3900 + rpc_port: 3901 + web_port: 3902 + + # 100 GB (the minimum) are being shared from drive2 + - data_path: /mnt/drive2/cryptic-net/data + meta_path: /mnt/drive2/cryptic-net/meta + capacity: 100 + api_port: 3910 + rpc_port: 3911 + web_port: 3912 +``` + +## 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. + +## Further Reading + +cryptic-net uses the [garage][garage] project for its storage system. See the +[Managing Garage](managing-garage.md) document for more +information on how to interact directly with the garage instance being run by +cryptic-net. + +[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/ diff --git a/docs/operator/managing-garage.md b/docs/operator/managing-garage.md new file mode 100644 index 0000000..dc8cd52 --- /dev/null +++ b/docs/operator/managing-garage.md @@ -0,0 +1,65 @@ +# Managing Garage + +The garage project provides the network storage component for +cryptic-net. If you're reading this document then you would likely benefit +greatly from reading the [garage documentation][garage] on their website. It's +extremely well written and concise. + +Note that the `cryptic-net daemon` process will handle all setup steps described +in that documentation, but it's still good to have an understanding of how +garage works and what it can do. + +[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/ + +## Garage Runtime Note + +There is an important thing to note regarding how cryptic-net runs garage. As +described in the [Contributing Storage](contributing-storage.md) document, a +single `cryptic-net daemon` process can be configured to provide any number of +storage allocations. + +For each allocation which is configured, `cryptic-net daemon` will configure and +run a separate `garage server` instance as a sub-process. Each garage will use +the host's name as its zone in the garage cluster layout, which means that the +cluster will prefer to not replicate the same data within the same host, but may +do so if necessary. + +## Garage CLI + +Every `cryptic-net` binary contains a full `garage` binary embedded into it. +This binary can be accessed directly like so: + +``` +sudo cryptic-net garage cli +``` + +Before handing off execution to the `garage` binary, the `cryptic-net` process +will automatically set up the RPC host and secret environment variables. + +If the host which is running the command has more than one allocation +configured, then the `garage server` process for the first allocation will be +connected to by this invocation of `garage`. If no allocations are configured, +then the `garage server` process of some other host in the network will be +connected to. + +## Examples + +To display the current layout of the garage cluster: + +``` +sudo cryptic-net garage cli layout show +``` + +**(DO NOT CHANGE THE CLUSTER LAYOUT UNLESS YOU KNOW WHAT YOU'RE DOING!)** + +To create a new bucket: + +``` +sudo cryptic-net garage cli bucket create new-bucket +``` + +To list existing buckets: + +``` +sudo cryptic-net garage cli bucket list +``` diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..5626032 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,87 @@ +# Roadmap + +The following are rough outlines of upcoming work on the roadmap, roughly in the +order they will be implemented. + +## Main quest + +These items are listed more or less in the order they need to be completed, as +they generally depend on the items previous to them. + +### Cross Compilation + +Currently the only supported OS/CPU is Linux/amd64. This can be expanded +_theoretically_ quite easily, using nix's cross compilation tools. First target +should be OSX/arm64, but windows would also be quite the get. + +### Bootstrap + +This will be difficult. There's currently no way to bootstrap a new cryptic-net +network from scratch, only the currently existing one is supported. Support for +IPv6 internal CIDR should also be a part of this effort. + +### Testing + +Once bootstrap is generalized, we'll be able to write some automated tests. + +## Side quests + +These items aren't necessarily required by the main quest, and aren't dependent +on any other items being completed. They are nice-to-haves that we do want to +eventually complete, but aren't the main focus. + +### DNS resolver settings + +The daemon should update the resolver settings of the host, so that it +automatically serves DNS queries, unless set to not do so in `daemon.yml`. + +### Install sub-command + +It would be great to have a `cryptic-net install` sub-command which would +auto-detect the installed operating system and install the daemon automatically. + +### Web server + interface + +One idea is to have every `cryptic-net daemon` run a webserver as one of its +sub-processes. This server could serve multiple functions: + +- Possible public gateway, if the host is configured to be public, into internal + cryptic-net services. This will require some sort of service discovery + mechanism that other hosts in the network can easily hook into via their + `daemon.yml`s. Ideally this mechanism would involve little beyond updating + entries in garage, which the public gateways then pick up on. + + One wrinkle here will be TLS certificates. Ideally internal hosts could host + websites publicly via the gateways on their network, using arbitrary domains + that the users own. The gateways will need some way of obtaining certs for + these domains automatically (or as automatically as possible). + +- Local interface for the `cryptic-net daemon` process. For example, status and + connectivity information for the local host could be provided via a simple web + interface, which the user can open in their browser. This saves us the effort + of needing to develop UIs for individual OSs. This could also make remotely + debugging hosts easier for admins. + +### Mobile app + +To start with a simple mobile app which provided connectivity to the network +would be great. Such a mobile app could be based on the existing +[mobile_nebula](https://github.com/DefinedNet/mobile_nebula). The main changes +needed would be: + +- Allow importing a `bootstrap.tgz` file, rather than requiring manual setup by + users. + +- Set device's DNS settings. There is an [open + PR](https://github.com/DefinedNet/mobile_nebula/pull/18) for android to do + this upstream. + +- Rebranding and possibly submitting to Apple app store (bleh). + +### Plugins + +It would not be difficult to spec out a plugin system using nix commands. +Existing components could be rigged to use this plugin system, and we could then +use the system to add future components which might prove useful. Once the +project is public such a system would be much appreciated I think, as it would +let other groups rig their binaries with all sorts of new functionality. diff --git a/docs/shell.nix b/docs/shell.nix new file mode 100644 index 0000000..aec6302 --- /dev/null +++ b/docs/shell.nix @@ -0,0 +1,14 @@ +{ + + pkgs ? (import ../nix/pkgs.nix).stable, + +}: pkgs.mkShell { + name = "cryptic-net-build-docs"; + buildInputs = [ pkgs.plantuml ]; + + shellHook = '' + set -e + plantuml -tsvg ./dev/*.plantuml + exit 0 + ''; +} diff --git a/docs/user/creating-a-daemonyml-file.md b/docs/user/creating-a-daemonyml-file.md new file mode 100644 index 0000000..15252d4 --- /dev/null +++ b/docs/user/creating-a-daemonyml-file.md @@ -0,0 +1,32 @@ +# Creating a daemon.yml File + +The `cryptic-net daemon` process has generally sane defaults and does not need +to be configured for most users. However, in some cases it does, so this +document describes how to use the `daemon.yml` file to handle those cases. + +## Create daemon.yml + +First, create a `daemon.yml` file. You can create a new `daemon.yml` with +default values filled in by doing: + +``` +cryptic-net daemon --dump-config > /path/to/daemon.yml +``` + +If you open that file in a text editor you can view all default values that +`cryptic-net daemon` ships with, as well as documentation for all configurable +parameters. Feel free to edit this file as needed. + +## Using daemon.yml + +With the `daemon.yml` created and configured, you can configure your daemon +process to use it by passing it as the `-c` argument: + +``` +sudo cryptic-net daemon -c /path/to/daemon.yml +``` + +If you are an operator then your host should be running its `cryptic-net daemon` +process in systemd (see [Getting Started](getting-started.md) if +not), and you will need to modify the `cryptic-net.service` accordingly. + diff --git a/docs/user/getting-started.md b/docs/user/getting-started.md new file mode 100644 index 0000000..da279a8 --- /dev/null +++ b/docs/user/getting-started.md @@ -0,0 +1,108 @@ +# Getting Started + +This document will guide you through the process of obtaining a cryptic-net +binary and joining the network. + +NOTE currently only linux machines with amd64/x86_64 processors are supported. +More OSs and architectures coming soon! + +## Obtaining a cryptic-net Binary + +Every host can have a binary built for it which has all configuration for that +host embedded directly into it. Such binaries require no extra configuration by +the user to use, and have no dependencies on anything else in the user's system. + +The process of obtaining a custom binary for your host is quite simple: ask an +admin of the network you'd like to join to give you one! + +Note that if you'd like to join the network on multiple devices, each device +will needs its own binary, so be sure to tell your admin how many you want to +add and their names. + +### Obtaining a cryptic-net Binary, the Hard Way + +Alternatively, you can build your own binary by running the following from the +project's root: + +``` +nix-build -A appImage +``` + +The resulting binary can be found in the `result` directory which is created. + +In this case you will need an admin to provide you with a `bootstrap.tgz` for +your host, rather than a custom binary. When running the daemon in the following +steps you will need to provide the `--bootstrap-path` CLI argument to the daemon +process. + +## Running the Daemon + +Once you have a binary, you will need to run the `daemon` sub-command as the +root user. This can most easily be done using the `sudo` command, in a terminal: + +``` +sudo /path/to/cryptic-net daemon +``` + +This will start the daemon process, which will keep running until you kill it +with `ctrl-c`. + +You can double check that the daemon is running properly by pinging a private IP +from the network in a separate terminal: + +``` +ping 10.10.0.1 +``` + +If the pings are successful then your daemon is working! + +## Installing the Daemon as a Systemd Service + +NOTE in the future we will introduce an `install` sub-command which will +automate most of this section. + +Rather than running the daemon manually, you can install it as a systemd +service. This way your daemon will automatically start in the background on +startup, and will be restarted if it has any issues. + +To do so, create a file at `/etc/systemd/system/cryptic-net.service` with the +following contents: + +``` +[Unit] +Description=cryptic-net +Requires=network.target +After=network.target + +[Service] +Restart=always +RestartSec=1s +User=root +ExecStart=/path/to/cryptic-net daemon + +[Install] +WantedBy=multi-user.target +``` + +Remember to change the `/path/to/cryptic-net` part to the actual absolute path +to your binary! + +Once created, perform the following commands in a terminal to enable the +service: + +``` +sudo systemctl daemon-reload +sudo systemctl enable --now cryptic-net +``` + +You can check the service's status by doing: + +``` +sudo systemctl status cryptic-net +``` + +and you can view its full logs by doing: + +``` +sudo journalctl -lu cryptic-net +``` diff --git a/docs/user/using-dns.md b/docs/user/using-dns.md new file mode 100644 index 0000000..b73a4d2 --- /dev/null +++ b/docs/user/using-dns.md @@ -0,0 +1,37 @@ +# Using DNS + +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 `.hosts.cryptic.io` hostnames, +where `` 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 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 +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`. In order to configure the host to use the +cryptic-net DNS server for all DNS requests, I could do something like this: + +``` +sudo su +echo "nameserver 10.10.1.1" > /etc/resolv.conf +``` + +From that point, all DNS requests on my host would hit the cryptic-net DNS +server. If I request `my-host.cryptic.io`, it would respond with the appropriate +private IP. + +NOTE that configuration of dns resolvers is very OS-specific, even amongst Linux +distributions, so ensure you know how your resolver configuration works before +doing this. diff --git a/garage/default.nix b/garage/default.nix new file mode 100644 index 0000000..351efcf --- /dev/null +++ b/garage/default.nix @@ -0,0 +1,32 @@ +{ + + fetchgit, + buildEnv, + minio-client, + +}: let + + src = fetchgit { + name = "garage-v0.6.0-unstable"; + url = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git"; + rev = "84613e66a286536dff9828d8aca2625d2c6c6bf2"; + sha256 = "sha256-ZVf+PPNL/DkJW4asJwW6/xpXVzZWIvLhsqaKh65eATM="; + }; + +in rec { + + garage = (import "${src}/default.nix") { release = true; }; + + minioClient = minio-client; + + env = buildEnv { + name = "cryptic-net-garage"; + paths = [ + garage + minioClient + ./src + ]; + }; + +} + diff --git a/garage/src/bin/garage-apply-layout-diff b/garage/src/bin/garage-apply-layout-diff new file mode 100644 index 0000000..3a718ad --- /dev/null +++ b/garage/src/bin/garage-apply-layout-diff @@ -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" ./nebula/hosts + + thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname) + thisHostIP=$(cat "$tmp"/nebula/hosts/"$thisHostName".yml | yq '.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 +) diff --git a/go-workspace/README.md b/go-workspace/README.md new file mode 100644 index 0000000..459014a --- /dev/null +++ b/go-workspace/README.md @@ -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. diff --git a/go-workspace/default.nix b/go-workspace/default.nix new file mode 100644 index 0000000..6860e07 --- /dev/null +++ b/go-workspace/default.nix @@ -0,0 +1,18 @@ +{ + buildGoModule, +}: let + + build = subPackage: buildGoModule { + + pname = "cryptic-net-" + (builtins.baseNameOf subPackage); + version = "unstable"; + src = ./src; + vendorSha256 = "sha256-d3Lpzb1CYVy+z9HCHEiqLG0v678d9+B14VTb3FV3AZQ="; + subPackages = [ + subPackage + ]; + }; + +in { + crypticNetMain = build "cmd/cryptic-net-main"; +} diff --git a/go-workspace/src/bootstrap/bootstrap.go b/go-workspace/src/bootstrap/bootstrap.go new file mode 100644 index 0000000..b32e8bc --- /dev/null +++ b/go-workspace/src/bootstrap/bootstrap.go @@ -0,0 +1,34 @@ +// Package bootstrap deals with the creation of bootstrap files +package bootstrap + +import ( + "cryptic-net/tarutil" + "fmt" + "io" + "io/fs" +) + +// GetHashFromFS returns the hash of the contents of the given bootstrap file. +// It may return nil if the bootstrap file doesn't have a hash. +func GetHashFromFS(bootstrapFS fs.FS) ([]byte, error) { + + b, err := fs.ReadFile(bootstrapFS, tarutil.HashBinPath) + + if err != nil { + return nil, fmt.Errorf("reading file %q from bootstrap fs: %w", tarutil.HashBinPath, err) + } + + return b, nil +} + +// GetHashFromReader reads the given tgz file as an fs.FS, and passes that to +// GetHashFromFS. +func GetHashFromReader(r io.Reader) ([]byte, error) { + + bootstrapFS, err := tarutil.FSFromReader(r) + if err != nil { + return nil, fmt.Errorf("reading tar fs from reader: %w", err) + } + + return GetHashFromFS(bootstrapFS) +} diff --git a/go-workspace/src/bootstrap/new_for_host.go b/go-workspace/src/bootstrap/new_for_host.go new file mode 100644 index 0000000..659c224 --- /dev/null +++ b/go-workspace/src/bootstrap/new_for_host.go @@ -0,0 +1,161 @@ +package bootstrap + +import ( + crypticnet "cryptic-net" + "cryptic-net/garage" + "cryptic-net/tarutil" + "crypto/rand" + "fmt" + "io" + "io/fs" + "net" + "time" + + "github.com/slackhq/nebula/cert" + "golang.org/x/crypto/curve25519" +) + +var ipCIDRMask = func() net.IPMask { + _, ipNet, err := net.ParseCIDR("10.10.0.0/16") + if err != nil { + panic(err) + } + return ipNet.Mask +}() + +// Generates a new key/cert for a nebula host, writing their encoded forms into +// the given TGZWriter. It will also write the ca.crt file to the TGZWriter. +// +// The logic here is largely based on +// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go +func writeNewNebulaCert( + w *tarutil.TGZWriter, adminFS fs.FS, host crypticnet.NebulaHost, +) error { + + caKeyPEM, err := fs.ReadFile(adminFS, "nebula/certs/ca.key") + if err != nil { + return fmt.Errorf("reading ca.key from admin fs: %w", err) + } + + caKey, _, err := cert.UnmarshalEd25519PrivateKey(caKeyPEM) + if err != nil { + return fmt.Errorf("unmarshaling ca.key: %w", err) + } + + caCrtPEM, err := fs.ReadFile(adminFS, "nebula/certs/ca.crt") + if err != nil { + return fmt.Errorf("reading ca.crt from admin fs: %w", err) + } + + caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM(caCrtPEM) + if err != nil { + return fmt.Errorf("unmarshaling ca.crt: %w", err) + } + + issuer, err := caCrt.Sha256Sum() + if err != nil { + return fmt.Errorf("getting ca.crt issuer: %w", err) + } + + expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second) + + ip := net.ParseIP(host.IP) + if ip == nil { + return fmt.Errorf("invalid host ip %q", host.IP) + } + + 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 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: host.Name, + Ips: []*net.IPNet{ipNet}, + NotBefore: time.Now(), + NotAfter: expireAt, + PublicKey: hostPub, + IsCA: false, + Issuer: issuer, + }, + } + + if err := hostCrt.CheckRootConstrains(caCrt); err != nil { + return fmt.Errorf("validating certificate constraints: %w", err) + } + + if err := hostCrt.Sign(caKey); err != nil { + return fmt.Errorf("signing host cert with ca.key: %w", err) + } + + hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey) + + hostCrtPEM, err := hostCrt.MarshalToPEM() + if err != nil { + return fmt.Errorf("marshalling host.crt: %w", err) + } + + w.WriteFileBytes("nebula/certs/ca.crt", caCrtPEM) + w.WriteFileBytes("nebula/certs/host.key", hostKeyPEM) + w.WriteFileBytes("nebula/certs/host.crt", hostCrtPEM) + + return nil +} + +// NewForHost generates a new bootstrap file for an arbitrary host, based on the +// given admin file's FS and data in garage. +func NewForHost(env *crypticnet.Env, adminFS fs.FS, name string, into io.Writer) error { + + host, ok := env.Hosts[name] + if !ok { + return fmt.Errorf("unknown host %q, make sure host entry has been created", name) + } + + client, err := garage.GlobalBucketAPIClient(env) + if err != nil { + return fmt.Errorf("creating client for global bucket: %w", err) + } + + w := tarutil.NewTGZWriter(into) + + w.WriteFileBytes("hostname", []byte(name)) + + if err := writeNewNebulaCert(w, adminFS, host.Nebula); err != nil { + return fmt.Errorf("creating/adding host's nebula certs: %w", err) + } + + fsFilesToCopy := []string{ + "garage/rpc-secret.txt", + "garage/cryptic-net-global-bucket-key.yml", + } + + for _, filePath := range fsFilesToCopy { + if err := copyFSFile(w, adminFS, filePath); err != nil { + return fmt.Errorf("copying %q from bootstrap fs: %w", filePath, err) + } + } + + garageDirsToCopy := []string{ + "nebula/hosts", + "garage/hosts", + } + + for _, dirPath := range garageDirsToCopy { + if err := copyGarageDir(env.Context, client, w, dirPath); err != nil { + return fmt.Errorf("copying %q from garage: %w", dirPath, err) + } + } + + return w.Close() +} diff --git a/go-workspace/src/bootstrap/new_for_this_host.go b/go-workspace/src/bootstrap/new_for_this_host.go new file mode 100644 index 0000000..6acd8dc --- /dev/null +++ b/go-workspace/src/bootstrap/new_for_this_host.go @@ -0,0 +1,49 @@ +package bootstrap + +import ( + crypticnet "cryptic-net" + "cryptic-net/garage" + "cryptic-net/tarutil" + "fmt" + "io" +) + +// NewForThisHost generates a new bootstrap file for the current host, based on +// the existing environment as well as data in garage. +func NewForThisHost(env *crypticnet.Env, into io.Writer) error { + + client, err := garage.GlobalBucketAPIClient(env) + if err != nil { + return fmt.Errorf("creating client for global bucket: %w", err) + } + + w := tarutil.NewTGZWriter(into) + + fsFilesToCopy := []string{ + "hostname", + "nebula/certs/ca.crt", + "nebula/certs/host.crt", + "nebula/certs/host.key", + "garage/rpc-secret.txt", + "garage/cryptic-net-global-bucket-key.yml", + } + + for _, filePath := range fsFilesToCopy { + if err := copyFSFile(w, env.BootstrapFS, filePath); err != nil { + return fmt.Errorf("copying %q from bootstrap fs: %w", filePath, err) + } + } + + garageDirsToCopy := []string{ + "nebula/hosts", + "garage/hosts", + } + + for _, dirPath := range garageDirsToCopy { + if err := copyGarageDir(env.Context, client, w, dirPath); err != nil { + return fmt.Errorf("copying %q from garage: %w", dirPath, err) + } + } + + return w.Close() +} diff --git a/go-workspace/src/bootstrap/util.go b/go-workspace/src/bootstrap/util.go new file mode 100644 index 0000000..c51d497 --- /dev/null +++ b/go-workspace/src/bootstrap/util.go @@ -0,0 +1,74 @@ +package bootstrap + +import ( + "context" + "cryptic-net/garage" + "cryptic-net/tarutil" + "fmt" + "io/fs" + + "github.com/minio/minio-go/v7" +) + +func copyFSFile(w *tarutil.TGZWriter, srcFS fs.FS, path string) error { + + f, err := srcFS.Open(path) + if err != nil { + return fmt.Errorf("opening %q in bootstrap fs: %w", path, err) + } + defer f.Close() + + fStat, err := f.Stat() + if err != nil { + return fmt.Errorf("stating %q from bootstrap fs: %w", path, err) + } + + w.WriteFile(path, fStat.Size(), f) + return nil +} + +func copyGarageDir( + ctx context.Context, client *minio.Client, + w *tarutil.TGZWriter, path string, +) error { + + objInfoCh := client.ListObjects( + ctx, garage.GlobalBucket, + minio.ListObjectsOptions{ + Prefix: path, + Recursive: true, + }, + ) + + for objInfo := range objInfoCh { + + if objInfo.Err != nil { + return fmt.Errorf("listing objects: %w", objInfo.Err) + } + + obj, err := client.GetObject( + ctx, garage.GlobalBucket, objInfo.Key, minio.GetObjectOptions{}, + ) + + if err != nil { + return fmt.Errorf( + "retrieving object %q from global bucket: %w", + objInfo.Key, err, + ) + } + + objStat, err := obj.Stat() + if err != nil { + obj.Close() + return fmt.Errorf( + "stating object %q from global bucket: %w", + objInfo.Key, err, + ) + } + + w.WriteFile(objInfo.Key, objStat.Size, obj) + obj.Close() + } + + return nil +} diff --git a/go-workspace/src/cmd/cryptic-net-main/main.go b/go-workspace/src/cmd/cryptic-net-main/main.go new file mode 100644 index 0000000..f9b6905 --- /dev/null +++ b/go-workspace/src/cmd/cryptic-net-main/main.go @@ -0,0 +1,83 @@ +package main + +// +// This binary acts as a wrapper around other programs which would otherwise +// form their own binaries. We do this for two reasons: +// +// * Nix makes it difficult to determine which individuals binaries need to be +// rebuilt upon changes, so it rebuilds all of them no matter what changed. This +// makes development slow. By wrapping everything in a sinble binary we only +// ever have to build that binary. +// +// * If we have N binaries total, then we have N copies of the go runtime in our +// final AppImage. By bundling the binaries into a single one we can reduce the +// number go runtime copies to 1. +// + +import ( + "cryptic-net/cmd/entrypoint" + garage_entrypoint "cryptic-net/cmd/garage-entrypoint" + garage_layout_diff "cryptic-net/cmd/garage-layout-diff" + garage_peer_keygen "cryptic-net/cmd/garage-peer-keygen" + garage_update_global_bucket "cryptic-net/cmd/garage-update-global-bucket" + nebula_entrypoint "cryptic-net/cmd/nebula-entrypoint" + nebula_update_global_bucket "cryptic-net/cmd/nebula-update-global-bucket" + "fmt" + "os" +) + +type mainFn struct { + name string + fn func() +} + +var mainFns = []mainFn{ + {"entrypoint", entrypoint.Main}, + {"garage-entrypoint", garage_entrypoint.Main}, + {"garage-layout-diff", garage_layout_diff.Main}, + {"garage-peer-keygen", garage_peer_keygen.Main}, + {"garage-update-global-bucket", garage_update_global_bucket.Main}, + {"nebula-entrypoint", nebula_entrypoint.Main}, + {"nebula-update-global-bucket", nebula_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 \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() +} diff --git a/go-workspace/src/cmd/entrypoint/daemon.go b/go-workspace/src/cmd/entrypoint/daemon.go new file mode 100644 index 0000000..0a7d4b8 --- /dev/null +++ b/go-workspace/src/cmd/entrypoint/daemon.go @@ -0,0 +1,341 @@ +package entrypoint + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "sync" + "time" + + crypticnet "cryptic-net" + "cryptic-net/bootstrap" + "cryptic-net/yamlutil" + + "github.com/cryptic-io/pmux/pmuxlib" + "github.com/imdario/mergo" + "gopkg.in/yaml.v3" +) + +// The daemon sub-command deals with starting an actual cryptic-net daemon +// process, which is required to be running for most other cryptic-net +// functionality. The sub-command does the following: +// +// * Creates and locks the runtime directory. +// +// * Creates the data directory and copies the appdir bootstrap file into there, +// if it's not already there. +// +// * Merges the user-provided daemon.yml file with the default, and writes the +// result to the runtime dir. +// +// * Sets up environment variables that all other sub-processes then use, based +// on the runtime dir. +// +// * Dynamically creates the root pmux config and runs pmux. +// +// * (On exit) cleans up the runtime directory. + +func writeDaemonYml(userDaemonYmlPath, builtinDaemonYmlPath, runtimeDirPath string) error { + + var fullDaemonYml map[string]interface{} + + if err := yamlutil.LoadYamlFile(&fullDaemonYml, builtinDaemonYmlPath); err != nil { + return fmt.Errorf("parsing builtin daemon.yml file: %w", err) + } + + if userDaemonYmlPath != "" { + + var daemonYml map[string]interface{} + if err := yamlutil.LoadYamlFile(&daemonYml, userDaemonYmlPath); err != nil { + return fmt.Errorf("parsing %q: %w", userDaemonYmlPath, err) + } + + err := mergo.Merge(&fullDaemonYml, daemonYml, mergo.WithOverride) + if err != nil { + return fmt.Errorf("merging contents of file %q: %w", userDaemonYmlPath, err) + } + } + + fullDaemonYmlB, err := yaml.Marshal(fullDaemonYml) + + if err != nil { + return fmt.Errorf("yaml marshaling daemon config: %w", err) + } + + daemonYmlPath := filepath.Join(runtimeDirPath, "daemon.yml") + + if err := ioutil.WriteFile(daemonYmlPath, fullDaemonYmlB, 0400); err != nil { + return fmt.Errorf("writing daemon.yml file to %q: %w", daemonYmlPath, err) + } + + return nil +} + +func writeBootstrapToDataDir(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("writing new bootstrap file to %q: %w", path, err) + } + + if err := env.LoadBootstrap(path); err != nil { + return fmt.Errorf("loading bootstrap from %q: %w", path, err) + } + + return nil +} + +// creates a new bootstrap file using available information from the network. If +// the new bootstrap file is different than the existing one, the existing one +// is overwritten, ReloadBootstrap is called on env, true is returned. +func reloadBootstrap(env *crypticnet.Env) (bool, error) { + + buf := new(bytes.Buffer) + + if err := bootstrap.NewForThisHost(env, buf); err != nil { + return false, fmt.Errorf("generating new bootstrap from env: %w", err) + } + + newHash, err := bootstrap.GetHashFromReader(bytes.NewReader(buf.Bytes())) + if err != nil { + return false, fmt.Errorf("reading hash from new bootstrap file: %w", err) + } + + currHash, err := bootstrap.GetHashFromFS(env.BootstrapFS) + if err != nil { + return false, fmt.Errorf("reading hash from existing bootstrap fs: %w", err) + } + + if bytes.Equal(newHash, currHash) { + return false, nil + } + + if err := writeBootstrapToDataDir(env, buf); err != nil { + return false, fmt.Errorf("writing new bootstrap file: %w", err) + } + + return true, nil +} + +// runs a single pmux process for 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) error { + + thisHost := env.ThisHost() + fmt.Fprintf(os.Stderr, "host name is %q, ip is %q\n", thisHost.Name, thisHost.Nebula.IP) + + pmuxProcConfigs := []pmuxlib.ProcessConfig{ + { + Name: "nebula", + Cmd: "cryptic-net-main", + Args: []string{ + "nebula-entrypoint", + }, + }, + { + Name: "dnsmasq", + Cmd: "bash", + Args: []string{ + "wait-for-ip", + env.ThisHost().Nebula.IP, + "bash", + "dnsmasq-entrypoint", + }, + }, + } + + if len(env.ThisDaemon().Storage.Allocations) > 0 { + pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{ + Name: "garage", + Cmd: "bash", + Args: []string{ + "wait-for-ip", + env.ThisHost().Nebula.IP, + "cryptic-net-main", "garage-entrypoint", + }, + + // garage can take a while to clean up + SigKillWait: (1 * time.Minute) + (10 * time.Second), + }) + } + + pmuxConfig := pmuxlib.Config{Processes: pmuxProcConfigs} + + doneCh := env.Context.Done() + + var wg sync.WaitGroup + defer wg.Wait() + + ctx, cancel := context.WithCancel(env.Context) + defer cancel() + + wg.Add(1) + go func() { + defer wg.Done() + pmuxlib.Run(ctx, pmuxConfig) + }() + + ticker := time.NewTicker(3 * time.Minute) + defer ticker.Stop() + + for { + select { + + case <-doneCh: + return env.Context.Err() + + case <-ticker.C: + + fmt.Fprintln(os.Stderr, "checking for changes to bootstrap") + + if changed, err := reloadBootstrap(env); 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 + + appDirPath := env.AppDirPath + + builtinDaemonYmlPath := filepath.Join(appDirPath, "etc", "daemon.yml") + + if *dumpConfig { + + builtinDaemonYml, err := os.ReadFile(builtinDaemonYmlPath) + + if err != nil { + return fmt.Errorf("reading default daemon.yml at %q: %w", builtinDaemonYmlPath, err) + } + + if _, err := os.Stdout.Write(builtinDaemonYml); err != nil { + return fmt.Errorf("writing default daemon.yml to stdout: %w", err) + } + + return nil + } + + runtimeDirPath := env.RuntimeDirPath + + fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath) + + if err := os.MkdirAll(runtimeDirPath, 0700); err != nil { + return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err) + + } else if err := crypticnet.NewProcLock(runtimeDirPath).WriteLock(); err != nil { + return err + } + + // do not defer the cleaning of the runtime directory until the lock has + // been obtained, otherwise we might delete the directory out from under + // the feet of an already running daemon + defer func() { + fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", runtimeDirPath) + if err := os.RemoveAll(runtimeDirPath); err != nil { + fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", runtimeDirPath, err) + } + }() + + // If the bootstrap file is not being stored in the data dir, move it + // there and reload the bootstrap info + if env.BootstrapPath != env.DataDirBootstrapPath() { + + path := env.BootstrapPath + + // If there's no BootstrapPath then no bootstrap file could be + // found. In this case we require the user to provide one on the + // command-line. + if path == "" { + + if *bootstrapPath == "" { + return errors.New("No bootstrap.tgz file could be found, and one is not provided with --bootstrap-path") + } + + path = *bootstrapPath + } + + f, err := os.Open(path) + if err != nil { + return fmt.Errorf("opening file %q: %w", env.BootstrapPath, err) + } + + err = writeBootstrapToDataDir(env, f) + f.Close() + + if err != nil { + return fmt.Errorf("copying bootstrap file from %q: %w", path, err) + } + } + + if err := writeDaemonYml(*daemonYmlPath, builtinDaemonYmlPath, runtimeDirPath); err != nil { + return fmt.Errorf("generating daemon.yml file: %w", err) + } + + for key, val := range env.ToMap() { + if err := os.Setenv(key, val); err != nil { + return fmt.Errorf("failed to set %q to %q: %w", key, val, err) + } + } + + for { + + if err := runDaemonPmuxOnce(env); errors.Is(err, context.Canceled) { + return nil + + } else if err != nil { + return fmt.Errorf("running pmux for daemon: %w", err) + } + } + }, +} diff --git a/go-workspace/src/cmd/entrypoint/garage.go b/go-workspace/src/cmd/entrypoint/garage.go new file mode 100644 index 0000000..4084dbd --- /dev/null +++ b/go-workspace/src/cmd/entrypoint/garage.go @@ -0,0 +1,158 @@ +package entrypoint + +import ( + "fmt" + "io/fs" + "log" + "os" + "strings" + "syscall" + + crypticnet "cryptic-net" + "cryptic-net/garage" +) + +func getGaragePeer(env *crypticnet.Env) (string, error) { + + if allocs := env.ThisDaemon().Storage.Allocations; len(allocs) > 0 { + return garage.GeneratePeerAddr(env.ThisHost().Nebula.IP, allocs[0].RPCPort) + } + + bootstrapPeers, err := garage.BootstrapPeerAddrs(env.Hosts) + + if err != nil { + return "", err + } + + return bootstrapPeers[0], nil +} + +var subCmdGarageMC = subCmd{ + name: "mc", + descr: "Runs the mc (minio-client) binary. The cryptic-net garage can be accessed under the `garage` alias", + checkLock: true, + do: func(subCmdCtx subCmdCtx) error { + + flags := subCmdCtx.flagSet(true) + + keyID := flags.StringP( + "key-id", "i", "", + "Optional key ID to use, defaults to that of the shared cryptic-net-global key", + ) + + keySecret := flags.StringP( + "key-secret", "s", "", + "Optional key secret to use, defaults to that of the shared cryptic-net-global key", + ) + + if err := flags.Parse(subCmdCtx.args); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + + env := subCmdCtx.env + + apiAddr := garage.APIAddr(env) + + if *keyID == "" || *keySecret == "" { + + globalBucketCreds, err := garage.GlobalBucketAPICredentials(env) + if err != nil { + return fmt.Errorf("loading global bucket credentials: %w", err) + } + + if *keyID == "" { + *keyID = globalBucketCreds.ID + } + + if *keySecret == "" { + *keySecret = globalBucketCreds.Secret + } + } + + args := flags.Args() + + if i := flags.ArgsLenAtDash(); i >= 0 { + args = args[i:] + } + + args = append([]string{"mc"}, args...) + + var ( + binPath = env.BinPath("mc") + cliEnv = append( + os.Environ(), + fmt.Sprintf( + "MC_HOST_garage=http://%s:%s@%s", + *keyID, *keySecret, apiAddr, + ), + + // The garage docs say this is necessary, though nothing bad + // seems to happen if we leave it out *shrug* + "MC_REGION=garage", + ) + ) + + if err := syscall.Exec(binPath, args, cliEnv); err != nil { + return fmt.Errorf( + "calling exec(%q, %#v, %#v): %w", + binPath, args, cliEnv, err, + ) + } + + return nil + }, +} + +var subCmdGarageCLI = subCmd{ + name: "cli", + descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running cryptic-net daemon", + checkLock: true, + do: func(subCmdCtx subCmdCtx) error { + + env := subCmdCtx.env + + peerAddr, err := getGaragePeer(env) + + if err != nil { + return fmt.Errorf("picking peer to communicate with: %w", err) + } + + rpcSecretB, err := fs.ReadFile(env.BootstrapFS, "garage/rpc-secret.txt") + + if err != nil { + log.Fatalf("reading garage rpc secret bootstrap fs: %v", err) + } + + rpcSecret := strings.TrimSpace(string(rpcSecretB)) + + var ( + binPath = env.BinPath("garage") + args = append([]string{"garage"}, subCmdCtx.args...) + cliEnv = append( + os.Environ(), + "GARAGE_RPC_HOST="+peerAddr, + "GARAGE_RPC_SECRET="+rpcSecret, + ) + ) + + if err := syscall.Exec(binPath, args, cliEnv); err != nil { + return fmt.Errorf( + "calling exec(%q, %#v, %#v): %w", + binPath, args, cliEnv, err, + ) + } + + return nil + }, +} + +var subCmdGarage = subCmd{ + name: "garage", + descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running cryptic-net daemon", + do: func(subCmdCtx subCmdCtx) error { + return subCmdCtx.doSubCmd( + subCmdGarageCLI, + subCmdGarageMC, + ) + }, +} diff --git a/go-workspace/src/cmd/entrypoint/hosts.go b/go-workspace/src/cmd/entrypoint/hosts.go new file mode 100644 index 0000000..45ab1db --- /dev/null +++ b/go-workspace/src/cmd/entrypoint/hosts.go @@ -0,0 +1,281 @@ +package entrypoint + +import ( + "bytes" + crypticnet "cryptic-net" + "cryptic-net/bootstrap" + "cryptic-net/garage" + "cryptic-net/tarutil" + "errors" + "fmt" + "io/fs" + "net" + "os" + "path/filepath" + "regexp" + + "github.com/minio/minio-go/v7" + "gopkg.in/yaml.v3" +) + +const nebulaHostPathPrefix = "nebula/hosts/" + +func nebulaHostPath(name string) string { + return filepath.Join(nebulaHostPathPrefix, name+".yml") +} + +var hostNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`) + +func validateHostName(name string) error { + + if !hostNameRegexp.MatchString(name) { + return errors.New("a host's name must start with a letter and only contain letters, numbers, and dashes") + } + + return nil +} + +var subCmdHostsAdd = subCmd{ + name: "add", + descr: "Adds a host to the network", + checkLock: true, + do: func(subCmdCtx subCmdCtx) error { + + flags := subCmdCtx.flagSet(false) + + name := flags.StringP( + "name", "n", "", + "Name of the new host", + ) + + ip := flags.StringP( + "ip", "i", "", + "IP of the new host", + ) + + if err := flags.Parse(subCmdCtx.args); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + + if *name == "" || *ip == "" { + return errors.New("--name and --ip are required") + } + + if err := validateHostName(*name); err != nil { + return fmt.Errorf("invalid hostname %q: %w", *name, err) + } + + if net.ParseIP(*ip) == nil { + return fmt.Errorf("invalid ip %q", *ip) + } + + // TODO validate that the IP is in the correct CIDR + + env := subCmdCtx.env + + client, err := garage.GlobalBucketAPIClient(env) + if err != nil { + return fmt.Errorf("creating client for global bucket: %w", err) + } + + nebulaHost := crypticnet.NebulaHost{ + Name: *name, + IP: *ip, + } + + bodyBuf := new(bytes.Buffer) + + if err := yaml.NewEncoder(bodyBuf).Encode(nebulaHost); err != nil { + return fmt.Errorf("marshaling nebula host to yaml: %w", err) + } + + filePath := nebulaHostPath(*name) + + _, err = client.PutObject( + env.Context, garage.GlobalBucket, filePath, + bodyBuf, int64(bodyBuf.Len()), + minio.PutObjectOptions{}, + ) + + if err != nil { + return fmt.Errorf("writing to %q in global bucket: %w", filePath, err) + } + + return nil + }, +} + +var subCmdHostsList = subCmd{ + name: "list", + descr: "Lists all hosts in the network, and their IPs", + checkLock: true, + do: func(subCmdCtx subCmdCtx) error { + + env := subCmdCtx.env + + client, err := garage.GlobalBucketAPIClient(env) + if err != nil { + return fmt.Errorf("creating client for global bucket: %w", err) + } + + objInfoCh := client.ListObjects( + env.Context, garage.GlobalBucket, + minio.ListObjectsOptions{ + Prefix: nebulaHostPathPrefix, + }, + ) + + for { + select { + case <-env.Context.Done(): + return env.Context.Err() + + case objInfo, ok := <-objInfoCh: + + if !ok { + return nil + } else if objInfo.Err != nil { + return objInfo.Err + } + + obj, err := client.GetObject( + env.Context, garage.GlobalBucket, objInfo.Key, + minio.GetObjectOptions{}, + ) + + if err != nil { + return fmt.Errorf("retrieving object %q from global bucket: %w", objInfo.Key, err) + } + + var nebulaHost crypticnet.NebulaHost + + err = yaml.NewDecoder(obj).Decode(&nebulaHost) + obj.Close() + + if err != nil { + return fmt.Errorf("yaml decoding %q from global bucket: %w", objInfo.Key, err) + } + + fmt.Fprintf( + os.Stdout, "%s\t%s\n", + nebulaHost.Name, nebulaHost.IP, + ) + } + } + + }, +} + +var subCmdHostsDelete = subCmd{ + name: "delete", + descr: "Deletes a host from the network", + checkLock: true, + do: func(subCmdCtx subCmdCtx) error { + + flags := subCmdCtx.flagSet(false) + + name := flags.StringP( + "name", "n", "", + "Name of the new host", + ) + + if err := flags.Parse(subCmdCtx.args); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + + if *name == "" { + return errors.New("--name is required") + } + + env := subCmdCtx.env + + filePath := nebulaHostPath(*name) + + client, err := garage.GlobalBucketAPIClient(env) + if err != nil { + return fmt.Errorf("creating client for global bucket: %w", err) + } + + err = client.RemoveObject( + env.Context, garage.GlobalBucket, filePath, + minio.RemoveObjectOptions{}, + ) + + if garage.IsKeyNotFound(err) { + return fmt.Errorf("host %q not found", *name) + } else if err != nil { + return fmt.Errorf("removing object %q from global bucket: %w", filePath, err) + } + + return nil + }, +} + +func readAdminFS(path string) (fs.FS, error) { + + if path == "-" { + + outFS, err := tarutil.FSFromReader(os.Stdin) + if err != nil { + return nil, fmt.Errorf("reading admin.tgz from stdin: %w", err) + } + + return outFS, nil + } + + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("opening file: %w", err) + } + defer f.Close() + + return tarutil.FSFromReader(f) +} + +var subCmdHostsMakeBootstrap = subCmd{ + name: "make-bootstrap", + descr: "Creates a new bootstrap.tgz file for a particular host and writes it to stdout", + checkLock: true, + do: func(subCmdCtx subCmdCtx) error { + + flags := subCmdCtx.flagSet(false) + + name := flags.StringP( + "name", "n", "", + "Name of the host to generate bootstrap.tgz for", + ) + + adminPath := flags.StringP( + "admin-path", "a", "", + `Path to admin.tgz file. If the given path is "-" then stdin is used.`, + ) + + if err := flags.Parse(subCmdCtx.args); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + + if *name == "" || *adminPath == "" { + return errors.New("--name and --admin-path are required") + } + + adminFS, err := readAdminFS(*adminPath) + if err != nil { + return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err) + } + + return bootstrap.NewForHost(subCmdCtx.env, adminFS, *name, os.Stdout) + }, +} + +var subCmdHosts = subCmd{ + name: "hosts", + descr: "Sub-commands having to do with configuration of hosts in the network", + do: func(subCmdCtx subCmdCtx) error { + return subCmdCtx.doSubCmd( + subCmdHostsAdd, + subCmdHostsList, + subCmdHostsDelete, + subCmdHostsMakeBootstrap, + ) + }, +} diff --git a/go-workspace/src/cmd/entrypoint/main.go b/go-workspace/src/cmd/entrypoint/main.go new file mode 100644 index 0000000..c502280 --- /dev/null +++ b/go-workspace/src/cmd/entrypoint/main.go @@ -0,0 +1,36 @@ +package entrypoint + +import ( + "fmt" + "os" + + crypticnet "cryptic-net" +) + +// The purpose of this binary is to act as the entrypoint of the cryptic-net +// process. It processes the command-line arguments which are passed in, and +// then passes execution along to an appropriate binary housed in AppDir/bin +// (usually a bash script, which is more versatile than a go program). + +func Main() { + + env, err := crypticnet.NewEnv(true) + + if err != nil { + panic(fmt.Sprintf("loading environment: %v", err)) + } + + err = subCmdCtx{ + args: os.Args[1:], + env: env, + }.doSubCmd( + subCmdDaemon, + subCmdHosts, + subCmdGarage, + subCmdVersion, + ) + + if err != nil { + panic(err) + } +} diff --git a/go-workspace/src/cmd/entrypoint/sub_cmd.go b/go-workspace/src/cmd/entrypoint/sub_cmd.go new file mode 100644 index 0000000..a8e4e94 --- /dev/null +++ b/go-workspace/src/cmd/entrypoint/sub_cmd.go @@ -0,0 +1,121 @@ +package entrypoint + +import ( + crypticnet "cryptic-net" + "fmt" + "os" + "strings" + + "github.com/spf13/pflag" +) + +// subCmdCtx contains all information available to a subCmd's do method. +type subCmdCtx struct { + subCmd subCmd // the subCmd itself + args []string // command-line arguments, excluding the subCmd itself. + subCmdNames []string // names of subCmds so far, including this one + env *crypticnet.Env +} + +type subCmd struct { + name string + descr string + checkLock bool + do func(subCmdCtx) error +} + +func (ctx subCmdCtx) usagePrefix() string { + + subCmdNamesStr := strings.Join(ctx.subCmdNames, " ") + if subCmdNamesStr != "" { + subCmdNamesStr += " " + } + + return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr) +} + +func (ctx subCmdCtx) flagSet(withPassthrough bool) *pflag.FlagSet { + flags := pflag.NewFlagSet(ctx.subCmd.name, pflag.ExitOnError) + flags.Usage = func() { + + var passthroughStr string + if withPassthrough { + passthroughStr = " [--] [args...]" + } + + fmt.Fprintf( + os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n", + ctx.usagePrefix(), ctx.subCmd.name, passthroughStr, + ) + fmt.Fprintf(os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(ctx.subCmd.name)) + fmt.Fprintln(os.Stderr, flags.FlagUsages()) + + os.Stderr.Sync() + os.Exit(2) + } + return flags +} + +func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error { + + printUsageExit := func(subCmdName string) { + + fmt.Fprintf(os.Stderr, "unknown sub-command %q\n", subCmdName) + + fmt.Fprintf( + os.Stderr, + "%s [-h|--help] [sub-command flags...]\n", + ctx.usagePrefix(), + ) + + fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n") + + for _, subCmd := range subCmds { + fmt.Fprintf(os.Stderr, " %s\t%s\n", subCmd.name, subCmd.descr) + } + + fmt.Fprintf(os.Stderr, "\n") + os.Stderr.Sync() + os.Exit(2) + } + + args := ctx.args + + if len(args) == 0 { + printUsageExit("") + } + + subCmdsMap := map[string]subCmd{} + for _, subCmd := range subCmds { + subCmdsMap[subCmd.name] = subCmd + } + + subCmdName, args := args[0], args[1:] + subCmd, ok := subCmdsMap[subCmdName] + + if !ok { + printUsageExit(subCmdName) + } + + if subCmd.checkLock { + + err := crypticnet.NewProcLock(ctx.env.RuntimeDirPath).AssertLock() + + if err != nil { + return fmt.Errorf("checking lock file: %w", err) + } + } + + err := subCmd.do(subCmdCtx{ + subCmd: subCmd, + args: args, + subCmdNames: append(ctx.subCmdNames, subCmdName), + env: ctx.env, + }) + + if err != nil { + return err + } + + return nil +} diff --git a/go-workspace/src/cmd/entrypoint/version.go b/go-workspace/src/cmd/entrypoint/version.go new file mode 100644 index 0000000..f06af4e --- /dev/null +++ b/go-workspace/src/cmd/entrypoint/version.go @@ -0,0 +1,28 @@ +package entrypoint + +import ( + "fmt" + "os" + "path/filepath" +) + +var subCmdVersion = subCmd{ + name: "version", + descr: "Dumps version and build info to stdout", + do: func(subCmdCtx subCmdCtx) error { + + versionPath := filepath.Join(subCmdCtx.env.AppDirPath, "share/version") + + version, err := os.ReadFile(versionPath) + + if err != nil { + return fmt.Errorf("reading version info from %q: %w", versionPath, err) + } + + if _, err := os.Stdout.Write(version); err != nil { + return fmt.Errorf("writing version info to stdout: %w", err) + } + + return nil + }, +} diff --git a/go-workspace/src/cmd/garage-entrypoint/main.go b/go-workspace/src/cmd/garage-entrypoint/main.go new file mode 100644 index 0000000..232eadf --- /dev/null +++ b/go-workspace/src/cmd/garage-entrypoint/main.go @@ -0,0 +1,151 @@ +package garage_entrypoint + +import ( + "fmt" + "io/fs" + "log" + "net" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + crypticnet "cryptic-net" + "cryptic-net/garage" + + "github.com/cryptic-io/pmux/pmuxlib" +) + +func writeChildConf( + env *crypticnet.Env, + bootstrapPeers []string, + alloc crypticnet.DaemonYmlStorageAllocation, + rpcSecret string, +) (string, error) { + + if err := os.MkdirAll(alloc.MetaPath, 0750); err != nil { + return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err) + } + + pubKey, privKey, err := garage.GeneratePeerKey(env.ThisHost().Nebula.IP, alloc.RPCPort) + + if err != nil { + return "", fmt.Errorf( + "generating node key with input %q,%d: %w", + env.ThisHost().Nebula.IP, alloc.RPCPort, err, + ) + } + + 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: rpcSecret, + + RPCAddr: net.JoinHostPort(env.ThisHost().Nebula.IP, strconv.Itoa(alloc.RPCPort)), + APIAddr: net.JoinHostPort(env.ThisHost().Nebula.IP, strconv.Itoa(alloc.APIPort)), + WebAddr: net.JoinHostPort(env.ThisHost().Nebula.IP, strconv.Itoa(alloc.WebPort)), + + BootstrapPeers: bootstrapPeers, + }) + + if err != nil { + return "", fmt.Errorf("creating garage.toml file at %q: %w", garageTomlPath, err) + } + + return garageTomlPath, nil +} + +func waitForArgs(env *crypticnet.Env, bin string, binArgs ...string) []string { + + var args []string + + for _, alloc := range env.ThisDaemon().Storage.Allocations { + args = append( + args, + "wait-for", + net.JoinHostPort(env.ThisHost().Nebula.IP, strconv.Itoa(alloc.RPCPort)), + "--", + ) + } + + args = append(args, bin) + args = append(args, binArgs...) + + return args +} + +func Main() { + + env, err := crypticnet.ReadEnv() + + if err != nil { + log.Fatalf("reading envvars: %v", err) + } + + bootstrapPeers, err := garage.BootstrapPeerAddrs(env.Hosts) + + if err != nil { + log.Fatalf("generating set of bootstrap peers: %v", err) + } + + rpcSecretB, err := fs.ReadFile(env.BootstrapFS, "garage/rpc-secret.txt") + + if err != nil { + log.Fatalf("reading garage rpc secret bootstrap fs: %v", err) + } + + rpcSecret := strings.TrimSpace(string(rpcSecretB)) + + var pmuxProcConfigs []pmuxlib.ProcessConfig + + for _, alloc := range env.ThisDaemon().Storage.Allocations { + + childConfPath, err := writeChildConf(env, bootstrapPeers, alloc, rpcSecret) + + if err != nil { + log.Fatalf("writing child config file for alloc %+v: %v", alloc, err) + } + + log.Printf("wrote config file %q", childConfPath) + + pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{ + Name: fmt.Sprintf("garage-%d", alloc.RPCPort), + Cmd: "garage", + Args: []string{"-c", childConfPath, "server"}, + SigKillWait: 1 * time.Minute, + }) + + } + + pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{ + Name: "garage-apply-layout-diff", + Cmd: "bash", + Args: waitForArgs(env, "bash", "garage-apply-layout-diff"), + NoRestartOn: []int{0}, + }) + + pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{ + Name: "garage-update-global-bucket", + Cmd: "bash", + Args: waitForArgs(env, "cryptic-net-main", "garage-update-global-bucket"), + NoRestartOn: []int{0}, + }) + + pmuxlib.Run(env.Context, pmuxlib.Config{Processes: pmuxProcConfigs}) +} diff --git a/go-workspace/src/cmd/garage-layout-diff/main.go b/go-workspace/src/cmd/garage-layout-diff/main.go new file mode 100644 index 0000000..bdd05b0 --- /dev/null +++ b/go-workspace/src/cmd/garage-layout-diff/main.go @@ -0,0 +1,259 @@ +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, error) { + + var expNodes clusterNodes + + for _, alloc := range env.ThisDaemon().Storage.Allocations { + + id, err := garage.GeneratePeerID(env.ThisHost().Nebula.IP, alloc.RPCPort) + + if err != nil { + return nil, fmt.Errorf( + "generating peer id for ip:%q port:%d: %w", + env.ThisHost().Nebula.IP, alloc.RPCPort, err, + ) + } + + expNodes = append(expNodes, clusterNode{ + ID: id, + Zone: env.ThisHost().Name, + Capacity: alloc.Capacity / 100, + }) + } + + return expNodes, nil +} + +// 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.ThisHost().Name != node.Zone { + continue + } + + thisCurrNodes = append(thisCurrNodes, node) + } + + expNodes, err := readExpNodes(env) + + if err != nil { + panic(fmt.Errorf("reading expected layout from environment: %w", err)) + } + + 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) +} diff --git a/go-workspace/src/cmd/garage-layout-diff/main_test.go b/go-workspace/src/cmd/garage-layout-diff/main_test.go new file mode 100644 index 0000000..5c85674 --- /dev/null +++ b/go-workspace/src/cmd/garage-layout-diff/main_test.go @@ -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) + } +} diff --git a/go-workspace/src/cmd/garage-peer-keygen/main.go b/go-workspace/src/cmd/garage-peer-keygen/main.go new file mode 100644 index 0000000..436255b --- /dev/null +++ b/go-workspace/src/cmd/garage-peer-keygen/main.go @@ -0,0 +1,64 @@ +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") + } + + pubKey, privKey, err := garage.GeneratePeerKey(*ip, *port) + + if err != nil { + panic(fmt.Errorf("GeneratePeerKey returned: %w", err)) + } + + 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)) + } + } +} diff --git a/go-workspace/src/cmd/garage-update-global-bucket/main.go b/go-workspace/src/cmd/garage-update-global-bucket/main.go new file mode 100644 index 0000000..f0abb8e --- /dev/null +++ b/go-workspace/src/cmd/garage-update-global-bucket/main.go @@ -0,0 +1,86 @@ +package garage_update_global_bucket + +import ( + "bytes" + crypticnet "cryptic-net" + "cryptic-net/garage" + "fmt" + "log" + "path/filepath" + + "github.com/minio/minio-go/v7" + "gopkg.in/yaml.v3" +) + +func updateGlobalBucket(env *crypticnet.Env) error { + + ctx := env.Context + + client, err := garage.GlobalBucketAPIClient(env) + if err != nil { + return fmt.Errorf("creating client for global bucket: %w", err) + } + + filePath := filepath.Join("garage/hosts", env.ThisHost().Name+".yml") + + daemon := env.ThisDaemon() + + if len(daemon.Storage.Allocations) == 0 { + + err := client.RemoveObject( + ctx, garage.GlobalBucket, filePath, + minio.RemoveObjectOptions{}, + ) + + if garage.IsKeyNotFound(err) { + return nil + } else if err != nil { + return fmt.Errorf("removing %q from global bucket: %w", filePath, err) + } + + return nil + } + + var garageHost crypticnet.GarageHost + + for _, alloc := range daemon.Storage.Allocations { + + garageHostInstance := crypticnet.GarageHostInstance{ + APIPort: alloc.APIPort, + RPCPort: alloc.RPCPort, + WebPort: alloc.WebPort, + } + + garageHost.Instances = append(garageHost.Instances, garageHostInstance) + } + + buf := new(bytes.Buffer) + + if err := yaml.NewEncoder(buf).Encode(garageHost); err != nil { + return fmt.Errorf("yaml encoding garage host data: %w", err) + } + + _, 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 +} + +func Main() { + + env, err := crypticnet.ReadEnv() + + if err != nil { + log.Fatalf("reading envvars: %v", err) + } + + if err := updateGlobalBucket(env); err != nil { + log.Fatalf("updating global bucket: %v", err) + } +} diff --git a/go-workspace/src/cmd/nebula-entrypoint/main.go b/go-workspace/src/cmd/nebula-entrypoint/main.go new file mode 100644 index 0000000..039fbbe --- /dev/null +++ b/go-workspace/src/cmd/nebula-entrypoint/main.go @@ -0,0 +1,157 @@ +package nebula_entrypoint + +import ( + "cryptic-net/yamlutil" + "fmt" + "io/fs" + "log" + "net" + "path/filepath" + "strconv" + + crypticnet "cryptic-net" + + "github.com/cryptic-io/pmux/pmuxlib" +) + +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.Hosts { + + if host.Nebula.PublicAddr == "" { + continue + } + + lighthouseHostIPs = append(lighthouseHostIPs, host.Nebula.IP) + staticHostMap[host.Nebula.IP] = []string{host.Nebula.PublicAddr} + } + + readCertFile := func(name string) string { + if err != nil { + return "" + } + + path := filepath.Join("nebula", "certs", name) + + var b []byte + if b, err = fs.ReadFile(env.BootstrapFS, path); err != nil { + err = fmt.Errorf("reading %q from bootstrap fs: %w", path, err) + } + + return string(b) + } + + config := map[string]interface{}{ + "pki": map[string]string{ + "ca": readCertFile("ca.crt"), + "cert": readCertFile("host.crt"), + "key": readCertFile("host.key"), + }, + "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.APIPort), + Proto: "tcp", + Host: "any", + }, + crypticnet.ConfigFirewallRule{ + Port: strconv.Itoa(alloc.RPCPort), + Proto: "tcp", + Host: "any", + }, + crypticnet.ConfigFirewallRule{ + Port: strconv.Itoa(alloc.WebPort), + Proto: "tcp", + Host: "any", + }, + ) + } + + firewall := thisDaemon.VPN.Firewall + + firewall.Inbound = append(firewallInbound, firewall.Inbound...) + + config["firewall"] = firewall + + nebulaYmlPath := filepath.Join(env.RuntimeDirPath, "nebula.yml") + + if err := yamlutil.WriteYamlFile(config, nebulaYmlPath); err != nil { + log.Fatalf("writing nebula.yml to %q: %v", nebulaYmlPath, err) + } + + pmuxlib.Run(env.Context, pmuxlib.Config{Processes: []pmuxlib.ProcessConfig{ + { + Name: "nebula-update-global-bucket", + Cmd: "cryptic-net-main", + Args: []string{ + "nebula-update-global-bucket", + }, + NoRestartOn: []int{0}, + }, + { + Name: "nebula", + Cmd: "nebula", + Args: []string{"-config", nebulaYmlPath}, + }, + }}) +} diff --git a/go-workspace/src/cmd/nebula-update-global-bucket/main.go b/go-workspace/src/cmd/nebula-update-global-bucket/main.go new file mode 100644 index 0000000..4d32227 --- /dev/null +++ b/go-workspace/src/cmd/nebula-update-global-bucket/main.go @@ -0,0 +1,62 @@ +package nebula_update_global_bucket + +import ( + "bytes" + crypticnet "cryptic-net" + "cryptic-net/garage" + "fmt" + "log" + "path/filepath" + + "github.com/minio/minio-go/v7" + "gopkg.in/yaml.v3" +) + +func updateGlobalBucket(env *crypticnet.Env) error { + + ctx := env.Context + + client, err := garage.GlobalBucketAPIClient(env) + if err != nil { + return fmt.Errorf("creating client for global bucket: %w", err) + } + + daemon := env.ThisDaemon() + + host := env.ThisHost() + + host.Nebula.Name = host.Name + host.Nebula.PublicAddr = daemon.VPN.PublicAddr + + buf := new(bytes.Buffer) + + if err := yaml.NewEncoder(buf).Encode(host.Nebula); err != nil { + return fmt.Errorf("yaml encoding garage host data: %w", err) + } + + filePath := filepath.Join("nebula/hosts", 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 +} + +func Main() { + + env, err := crypticnet.ReadEnv() + + if err != nil { + log.Fatalf("reading envvars: %v", err) + } + + if err := updateGlobalBucket(env); err != nil { + log.Fatalf("updating global bucket: %v", err) + } +} diff --git a/go-workspace/src/daemon_yml.go b/go-workspace/src/daemon_yml.go new file mode 100644 index 0000000..c2a7f16 --- /dev/null +++ b/go-workspace/src/daemon_yml.go @@ -0,0 +1,51 @@ +package crypticnet + +type ConfigFirewall struct { + Conntrack ConfigConntrack `yaml:"conntrack"` + Outbound []ConfigFirewallRule `yaml:"outbound"` + Inbound []ConfigFirewallRule `yaml:"inbound"` +} + +type ConfigConntrack struct { + TCPTimeout string `yaml:"tcp_timeout"` + UDPTimeout string `yaml:"udp_timeout"` + DefaultTimeout string `yaml:"default_timeout"` + MaxConnections int `yaml:"max_connections"` +} + +type ConfigFirewallRule struct { + Port string `yaml:"port,omitempty"` + Code string `yaml:"code,omitempty"` + Proto string `yaml:"proto,omitempty"` + Host string `yaml:"host,omitempty"` + Group string `yaml:"group,omitempty"` + Groups []string `yaml:"groups,omitempty"` + CIDR string `yaml:"cidr,omitempty"` + CASha string `yaml:"ca_sha,omitempty"` + CAName string `yaml:"ca_name,omitempty"` +} + +// DaemonYmlStorageAllocation describes the structure of each storage allocation +// within the daemon.yml file. +type DaemonYmlStorageAllocation struct { + DataPath string `yaml:"data_path"` + MetaPath string `yaml:"meta_path"` + Capacity int `yaml:"capacity"` + APIPort int `yaml:"api_port"` + RPCPort int `yaml:"rpc_port"` + WebPort int `yaml:"web_port"` +} + +// DaemonYml describes the structure of the daemon.yml file. +type DaemonYml struct { + DNS struct { + Resolvers []string `yaml:"resolvers"` + } `yaml:"dns"` + VPN struct { + PublicAddr string `yaml:"public_addr"` + Firewall ConfigFirewall `yaml:"firewall"` + } `yaml:"vpn"` + Storage struct { + Allocations []DaemonYmlStorageAllocation + } `yaml:"storage"` +} diff --git a/go-workspace/src/doc.go b/go-workspace/src/doc.go new file mode 100644 index 0000000..4b95a28 --- /dev/null +++ b/go-workspace/src/doc.go @@ -0,0 +1,3 @@ +// Package globals defines global constants and variables which are valid +// across all cryptic-net processes and sub-processes. +package crypticnet diff --git a/go-workspace/src/env.go b/go-workspace/src/env.go new file mode 100644 index 0000000..77f5e8a --- /dev/null +++ b/go-workspace/src/env.go @@ -0,0 +1,252 @@ +package crypticnet + +import ( + "context" + "cryptic-net/tarutil" + "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 + BootstrapFS fs.FS + Hosts map[string]Host + HostName string + + 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 + + // load all values into temp variables before setting the fields on Env, + // so we don't leave it in an inconsistent state. + bootstrapFS fs.FS + hosts map[string]Host + hostNameB []byte + ) + + if bootstrapFS, err = tarutil.FSFromTGZFile(path); err != nil { + return fmt.Errorf("reading bootstrap file at %q: %w", e.BootstrapPath, err) + } + + if hosts, err = LoadHosts(bootstrapFS); err != nil { + return fmt.Errorf("loading hosts info from bootstrap fs: %w", err) + } + + if hostNameB, err = fs.ReadFile(bootstrapFS, "hostname"); err != nil { + return fmt.Errorf("loading hostname from bootstrap fs: %w", err) + } + + e.BootstrapPath = path + e.BootstrapFS = bootstrapFS + e.Hosts = hosts + e.HostName = string(hostNameB) + + 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 +} + +// ThisHost is a shortcut for returning env.Hosts[env.HostName]. +func (e *Env) ThisHost() Host { + return e.Hosts[e.HostName] +} + +// 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) +} diff --git a/go-workspace/src/garage/client.go b/go-workspace/src/garage/client.go new file mode 100644 index 0000000..0120dc2 --- /dev/null +++ b/go-workspace/src/garage/client.go @@ -0,0 +1,92 @@ +package garage + +import ( + crypticnet "cryptic-net" + "cryptic-net/yamlutil" + "errors" + "fmt" + "net" + "strconv" + + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +// IsKeyNotFound returns true if the given error is the result of a key not +// being found in a bucket. +func IsKeyNotFound(err error) bool { + var mErr minio.ErrorResponse + return errors.As(err, &mErr) && mErr.Code == "NoSuchKey" +} + +// APICredentials describe data fields necessary for authenticating with a +// garage api endpoint. +type APICredentials struct { + ID string `yaml:"id"` + Secret string `yaml:"secret"` +} + +// GlobalBucketAPICredentials returns APICredentials for the global bucket. +func GlobalBucketAPICredentials(env *crypticnet.Env) (APICredentials, error) { + + const path = "garage/cryptic-net-global-bucket-key.yml" + + var creds APICredentials + if err := yamlutil.LoadYamlFSFile(&creds, env.BootstrapFS, path); err != nil { + return APICredentials{}, fmt.Errorf("loading %q from bootstrap fs: %w", path, err) + } + + return creds, nil +} + +// APIAddr returns the network address of a garage api endpoint in the network. +// It will prefer an endpoint on this particular host, if there is one, but will +// otherwise return a random endpoint. +func APIAddr(env *crypticnet.Env) string { + + if allocs := env.ThisDaemon().Storage.Allocations; len(allocs) > 0 { + + return net.JoinHostPort( + env.ThisHost().Nebula.IP, + strconv.Itoa(allocs[0].APIPort), + ) + + } + + for _, host := range env.Hosts { + + if host.Garage == nil || len(host.Garage.Instances) == 0 { + continue + } + + return net.JoinHostPort( + host.Nebula.IP, + strconv.Itoa(host.Garage.Instances[0].APIPort), + ) + } + + panic("no garage instances configured") +} + +// APIClient returns a minio client configured to use the given garage API +// endpoint. +func APIClient(addr string, creds APICredentials) (*minio.Client, error) { + return minio.New(addr, &minio.Options{ + Creds: credentials.NewStaticV4(creds.ID, creds.Secret, ""), + Region: Region, + }) +} + +// GlobalBucketAPIClient returns a minio client pre-configured with access to +// the global bucket. +func GlobalBucketAPIClient(env *crypticnet.Env) (*minio.Client, error) { + + creds, err := GlobalBucketAPICredentials(env) + if err != nil { + return nil, fmt.Errorf("loading global bucket credentials: %w", err) + } + + addr := APIAddr(env) + + return APIClient(addr, creds) +} diff --git a/go-workspace/src/garage/garageutil.go b/go-workspace/src/garage/garageutil.go new file mode 100644 index 0000000..02fb5b7 --- /dev/null +++ b/go-workspace/src/garage/garageutil.go @@ -0,0 +1,100 @@ +// Package garage contains helper functions and types which are useful for +// setting up garage configs, processes, and deployments. +package garage + +import ( + crypticnet "cryptic-net" + "crypto/ed25519" + "encoding/hex" + "fmt" + "net" + "strconv" +) + +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" +) + +// GeneratePeerKey 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 GeneratePeerKey(ip string, port int) (pubKey, privKey []byte, err error) { + + input := []byte(net.JoinHostPort(ip, strconv.Itoa(port))) + + // 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) + + return ed25519.GenerateKey(NewInfiniteReader(input)) +} + +// GeneratePeerID generates the peer id for the given peer. +// +// DANGER: See warning on GenerateNodeKey. +func GeneratePeerID(ip string, port int) (string, error) { + + peerNodeKeyPub, _, err := GeneratePeerKey(ip, port) + + if err != nil { + return "", err + } + + return hex.EncodeToString(peerNodeKeyPub), nil +} + +// GeneratePeerAddr generates the peer address (e.g. "id@ip:port") for the +// given peer. +// +// DANGER: See warning on GenerateNodeKey. +func GeneratePeerAddr(ip string, port int) (string, error) { + + id, err := GeneratePeerID(ip, port) + + if err != nil { + return "", fmt.Errorf("generating peer id: %w", err) + } + + return fmt.Sprintf("%s@%s", id, net.JoinHostPort(ip, strconv.Itoa(port))), nil +} + +// BootstrapPeerAddrs generates the list of bootstrap peer strings based on the +// bootstrap hosts. +func BootstrapPeerAddrs(hosts map[string]crypticnet.Host) ([]string, error) { + + var peers []string + + for _, host := range hosts { + + if host.Garage == nil { + continue + } + + for _, instance := range host.Garage.Instances { + + peer, err := GeneratePeerAddr(host.Nebula.IP, instance.RPCPort) + + if err != nil { + return nil, fmt.Errorf( + "generating peer address with input %q,%d: %w", + host.Nebula.IP, instance.RPCPort, err, + ) + } + + peers = append(peers, peer) + } + } + + return peers, nil +} diff --git a/go-workspace/src/garage/infinite_reader.go b/go-workspace/src/garage/infinite_reader.go new file mode 100644 index 0000000..1b137f7 --- /dev/null +++ b/go-workspace/src/garage/infinite_reader.go @@ -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 + } + } +} diff --git a/go-workspace/src/garage/infinite_reader_test.go b/go-workspace/src/garage/infinite_reader_test.go new file mode 100644 index 0000000..477d7e9 --- /dev/null +++ b/go-workspace/src/garage/infinite_reader_test.go @@ -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)) + } + } + }) + } +} diff --git a/go-workspace/src/garage/tpl.go b/go-workspace/src/garage/tpl.go new file mode 100644 index 0000000..c561a3c --- /dev/null +++ b/go-workspace/src/garage/tpl.go @@ -0,0 +1,76 @@ +package garage + +import ( + "fmt" + "io" + "os" + "text/template" +) + +// GarageTomlData describes all fields needed for rendering a garage.toml +// file via this package's template. +type GarageTomlData struct { + MetaPath string + DataPath string + + RPCSecret string + + RPCAddr string + APIAddr string + WebAddr string + + BootstrapPeers []string +} + +var garageTomlTpl = template.Must(template.New("").Parse(` + +metadata_dir = "{{ .MetaPath }}" +data_dir = "{{ .DataPath }}" + +replication_mode = "3" + +rpc_secret = "{{ .RPCSecret }}" +rpc_bind_addr = "{{ .RPCAddr }}" +rpc_public_addr = "{{ .RPCAddr }}" + +bootstrap_peers = [{{- range .BootstrapPeers }} +"{{ . }}", +{{ end -}}] + +[s3_api] +api_bind_addr = "{{ .APIAddr }}" +s3_region = "garage" + +[s3_web] +bind_addr = "{{ .WebAddr }}" +root_domain = ".example.com" + +`)) + +// RenderGarageToml renders a garage.toml using the given data into the writer. +func RenderGarageToml(into io.Writer, data GarageTomlData) error { + return garageTomlTpl.Execute(into, data) +} + +// WriteGarageTomlFile renders a garage.toml using the given data to a new file +// at the given path. +func WriteGarageTomlFile(path string, data GarageTomlData) error { + + file, err := os.OpenFile( + path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640, + ) + + if err != nil { + return fmt.Errorf("creating file: %w", err) + } + + defer file.Close() + + err = RenderGarageToml(file, data) + + if err != nil { + return fmt.Errorf("rendering template to file: %w", err) + } + + return nil +} diff --git a/go-workspace/src/go.mod b/go-workspace/src/go.mod new file mode 100644 index 0000000..5ec5c9a --- /dev/null +++ b/go-workspace/src/go.mod @@ -0,0 +1,62 @@ +module cryptic-net + +go 1.17 + +require ( + 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/shirou/gopsutil v3.21.11+incompatible + github.com/spf13/pflag v1.0.5 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b +) + +require ( + github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/flynn/noise v0.0.0-20210331153838-4bdb43be3117 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.13.5 // indirect + github.com/klauspost/cpuid v1.3.1 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/miekg/dns v1.1.25 // indirect + github.com/minio/md5-simd v1.1.0 // indirect + github.com/minio/minio-go/v7 v7.0.28 // indirect + github.com/minio/sha256-simd v0.1.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nbrownus/go-metrics-prometheus v0.0.0-20180622211546-6e6d5173d99c // indirect + github.com/nlepage/go-tarfs v1.1.0 // indirect + github.com/prometheus/client_golang v1.2.1 // indirect + github.com/prometheus/client_model v0.0.0-20191202183732-d1d2010b5bee // indirect + github.com/prometheus/common v0.7.0 // indirect + github.com/prometheus/procfs v0.0.8 // indirect + github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 // indirect + github.com/rs/xid v1.2.1 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + github.com/slackhq/nebula v1.4.0 // indirect + github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b // indirect + github.com/tklauser/go-sysconf v0.3.10 // indirect + github.com/tklauser/numcpus v0.4.0 // indirect + github.com/vishvananda/netlink v1.0.1-0.20190522153524-00009fb8606a // indirect + github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + golang.org/x/text v0.3.3 // indirect + google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/ini.v1 v1.57.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go-workspace/src/go.sum b/go-workspace/src/go.sum new file mode 100644 index 0000000..82982f5 --- /dev/null +++ b/go-workspace/src/go.sum @@ -0,0 +1,242 @@ +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cryptic-io/pmux v0.0.0-20220227202618-cfc616613569 h1:5O4td9Lps2TJvb2q4R951SBHu/kxkfi/xBclsSxyYrI= +github.com/cryptic-io/pmux v0.0.0-20220227202618-cfc616613569/go.mod h1:fg/CCfMpcbqO7/iqLjoYskopsVO3JqdD3Oij83+YXSI= +github.com/cryptic-io/pmux v0.0.0-20220619015204-8fb99b53d715 h1:OGwn0GaxnWMNUH12iexckXIsEEExJfIxfDyoQZe7/KU= +github.com/cryptic-io/pmux v0.0.0-20220619015204-8fb99b53d715/go.mod h1:fg/CCfMpcbqO7/iqLjoYskopsVO3JqdD3Oij83+YXSI= +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/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= +github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v0.0.0-20210331153838-4bdb43be3117 h1:Dxhvhray2DpvNnrZEnoGG5rz238fUeQTh4sdzTr+d1U= +github.com/flynn/noise v0.0.0-20210331153838-4bdb43be3117/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kardianos/service v1.1.0/go.mod h1:RrJI2xn5vve/r32U5suTbeaSGoMU6GbNPoj36CVYcHc= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4= +github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= +github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg= +github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= +github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= +github.com/minio/minio-go/v7 v7.0.28 h1:VMr3K5qGIEt+/KW3poopRh8mzi5RwuCjmrmstK196Fg= +github.com/minio/minio-go/v7 v7.0.28/go.mod h1:x81+AX5gHSfCSqw7jxRKHvxUXMlE5uKX0Vb75Xk5yYg= +github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= +github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nbrownus/go-metrics-prometheus v0.0.0-20180622211546-6e6d5173d99c h1:G/mfx/MWYuaaGlHkZQBBXFAJiYnRt/GaOVxnRHjlxg4= +github.com/nbrownus/go-metrics-prometheus v0.0.0-20180622211546-6e6d5173d99c/go.mod h1:1yMri853KAI2pPAUnESjaqZj9JeImOUM+6A4GuuPmTs= +github.com/nlepage/go-tarfs v1.1.0 h1:bsACOiZMB/zFjYG/sE01070i9Fl26MnRpw0L6WuyfVs= +github.com/nlepage/go-tarfs v1.1.0/go.mod h1:IhxRcLhLkawBetnwu/JNuoPkq/6cclAllhgEa6SmzS8= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI= +github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20191202183732-d1d2010b5bee h1:iBZPTYkGLvdu6+A5TsMUJQkQX9Ad4aCEnSQtdxPuTCQ= +github.com/prometheus/client_model v0.0.0-20191202183732-d1d2010b5bee/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563 h1:dY6ETXrvDG7Sa4vE8ZQG4yqWg6UnOcbqTAahkV813vQ= +github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/slackhq/nebula v1.4.0 h1:EwjObdoI1a0V4hXGn8cc/5gbGvMKuKBp1H+bOCnyZU8= +github.com/slackhq/nebula v1.4.0/go.mod h1:N4OtbI4997CFRdZZiJSOwuQdvslvef5CkWR6Nd+tUB4= +github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b h1:+y4hCMc/WKsDbAPsOQZgBSaSZ26uh2afyaWeVg/3s/c= +github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= +github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= +github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= +github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= +github.com/vishvananda/netlink v1.0.1-0.20190522153524-00009fb8606a h1:Bt1IVPhiCDMqwGrc2nnbIN4QKvJGx6SK2NzWBmW00ao= +github.com/vishvananda/netlink v1.0.1-0.20190522153524-00009fb8606a/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f h1:aZp0e2vLN4MToVqnjNEYEtrEA8RH8U8FN1CU7JgqsPU= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 h1:XDXtA5hveEEV8JB2l7nhMTp3t3cHp9ZpwcdjqyEWLlo= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go-workspace/src/hosts.go b/go-workspace/src/hosts.go new file mode 100644 index 0000000..67392ad --- /dev/null +++ b/go-workspace/src/hosts.go @@ -0,0 +1,93 @@ +package crypticnet + +import ( + "errors" + "fmt" + "io/fs" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) + +// NebulaHost describes the contents of a `./nebula/hosts/.yml` file. +type NebulaHost struct { + Name string `yaml:"name"` + IP string `yaml:"ip"` + PublicAddr string `yaml:"public_addr,omitempty"` +} + +// GarageHostInstance describes a single garage instance running on a host. +type GarageHostInstance struct { + APIPort int `yaml:"api_port"` + RPCPort int `yaml:"rpc_port"` + WebPort int `yaml:"web_port"` +} + +// GarageHost describes the contents of a `./garage/hosts/.yml` file. +type GarageHost struct { + Instances []GarageHostInstance `yaml:"instances"` +} + +// Host consolidates all information about a single host from the bootstrap +// file. +type Host struct { + Name string + Nebula NebulaHost + Garage *GarageHost +} + +// LostHosts returns a mapping of hostnames to Host objects for each host. +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) + } + + { + nebulaHostFiles, err := fs.Glob(bootstrapFS, "nebula/hosts/*.yml") + if err != nil { + return nil, fmt.Errorf("listing nebula host files: %w", err) + } + + for _, nebulaHostPath := range nebulaHostFiles { + + hostName := filepath.Base(nebulaHostPath) + hostName = strings.TrimSuffix(hostName, filepath.Ext(hostName)) + + var nebulaHost NebulaHost + if err := readAsYaml(&nebulaHost, nebulaHostPath); err != nil { + return nil, fmt.Errorf("reading %q as yaml: %w", nebulaHostPath, err) + } + + hosts[hostName] = Host{ + Name: hostName, + Nebula: nebulaHost, + } + } + } + + for hostName, host := range hosts { + + garageHostPath := filepath.Join("garage/hosts", hostName+".yml") + + var garageHost GarageHost + if err := readAsYaml(&garageHost, garageHostPath); errors.Is(err, fs.ErrNotExist) { + continue + } else if err != nil { + return nil, fmt.Errorf("reading %q as yaml: %w", garageHostPath, err) + } + + host.Garage = &garageHost + hosts[hostName] = host + } + + return hosts, nil +} diff --git a/go-workspace/src/proc_lock.go b/go-workspace/src/proc_lock.go new file mode 100644 index 0000000..8647b12 --- /dev/null +++ b/go-workspace/src/proc_lock.go @@ -0,0 +1,99 @@ +package crypticnet + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/shirou/gopsutil/process" +) + +var errDaemonNotRunning = errors.New("no cryptic-net daemon process running") + +// ProcLock is used to lock a process. +type ProcLock interface { + + // WriteLock creates a new lock, or errors if the lock is alread held. + WriteLock() error + + // AssertLock returns an error if the lock already exists. + AssertLock() error +} + +type procLock struct { + dir string +} + +// NewProcLock returns a ProcLock which will use a file in the given directory +// to lock the process. +func NewProcLock(dir string) ProcLock { + return &procLock{dir: dir} +} + +func (pl *procLock) path() string { + return filepath.Join(pl.dir, "lock") +} + +func (pl *procLock) WriteLock() error { + + lockFilePath := pl.path() + + lockFile, err := os.OpenFile( + lockFilePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400, + ) + + if errors.Is(err, os.ErrExist) { + return fmt.Errorf( + "lock file %q already exists, if the cryptic-net daemon is not already running you can safely delete this file", + lockFilePath, + ) + + } else if err != nil { + return fmt.Errorf("opening lockfile %q: %w", lockFilePath, err) + } + + defer lockFile.Close() + + if _, err := fmt.Fprintf(lockFile, "%d\n", os.Getpid()); err != nil { + return fmt.Errorf("writing pid to %q: %w", lockFilePath, err) + } + + return nil +} + +// checks that the lock file exists and that the process which created it also +// still exists. +func (pl *procLock) AssertLock() error { + + lockFilePath := pl.path() + + lockFile, err := os.Open(lockFilePath) + + if errors.Is(err, fs.ErrNotExist) { + return errDaemonNotRunning + + } else if err != nil { + return fmt.Errorf("checking lock file %q: %w", lockFilePath, err) + } + + defer lockFile.Close() + + var pid int32 + + if _, err := fmt.Fscan(lockFile, &pid); err != nil { + return fmt.Errorf("scanning pid from lock file %q: %w", lockFilePath, err) + } + + procExists, err := process.PidExists(pid) + + if err != nil { + return fmt.Errorf("checking if process %d exists: %w", pid, err) + + } else if !procExists { + return errDaemonNotRunning + } + + return nil +} diff --git a/go-workspace/src/tarutil/tarutil.go b/go-workspace/src/tarutil/tarutil.go new file mode 100644 index 0000000..2f6a5cb --- /dev/null +++ b/go-workspace/src/tarutil/tarutil.go @@ -0,0 +1,37 @@ +// Package tarutil implements utilities which are useful for interacting with +// tar and tgz files. +package tarutil + +import ( + "compress/gzip" + "fmt" + "io" + "io/fs" + "os" + + "github.com/nlepage/go-tarfs" +) + +// FSFromTGZFile 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) +} + +// FSFromTGZFile returns a FS instance which will read the contents of a tgz +// file. +func FSFromTGZFile(path string) (fs.FS, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("opening file: %w", err) + } + defer f.Close() + + return FSFromReader(f) +} diff --git a/go-workspace/src/tarutil/tgz_writer.go b/go-workspace/src/tarutil/tgz_writer.go new file mode 100644 index 0000000..04acc03 --- /dev/null +++ b/go-workspace/src/tarutil/tgz_writer.go @@ -0,0 +1,151 @@ +package tarutil + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha512" + "fmt" + "io" + "path/filepath" + "sort" + "strings" +) + +const ( + + // Path to the file containing the content hash of the tgz, which is + // included as part of all tgz files created by TGZWriter. + HashBinPath = "hash.bin" +) + +type fileHash struct { + path string + hash []byte +} + +// 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). +// +// A `hash.bin` file will be automatically included in the resulting tgz, which +// will contain a consistent hash of all other contents in the tgz file. +type TGZWriter struct { + gzipW *gzip.Writer + tarW *tar.Writer + err error + + dirsWritten map[string]bool + fileHashes []fileHash +} + +// 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 { + + sort.Slice(w.fileHashes, func(i, j int) bool { + return w.fileHashes[i].path < w.fileHashes[j].path + }) + + h := sha512.New() + + for i := range w.fileHashes { + fmt.Fprintf(h, "%q:%x\n", w.fileHashes[i].path, w.fileHashes[i].hash) + } + + w.WriteFile(HashBinPath, int64(h.Size()), bytes.NewBuffer(h.Sum(nil))) + + 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 + } + + h := sha512.New() + + if _, err := io.Copy(io.MultiWriter(w.tarW, h), body); err != nil { + w.err = fmt.Errorf("writing file body of file %q: %w", path, err) + return + } + + w.fileHashes = append(w.fileHashes, fileHash{ + path: path, + hash: h.Sum(nil), + }) +} + +// 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) +} diff --git a/go-workspace/src/yamlutil/yamlutil.go b/go-workspace/src/yamlutil/yamlutil.go new file mode 100644 index 0000000..f76fee5 --- /dev/null +++ b/go-workspace/src/yamlutil/yamlutil.go @@ -0,0 +1,67 @@ +package yamlutil + +import ( + "fmt" + "io/fs" + "os" + + "gopkg.in/yaml.v3" +) + +// LoadYamlFile reads the file at the given path and unmarshals it into the +// given pointer. +func LoadYamlFile(into interface{}, path string) error { + + file, err := os.Open(path) + + if err != nil { + return fmt.Errorf("opening file: %w", err) + } + + defer file.Close() + + if err = yaml.NewDecoder(file).Decode(into); err != nil { + return fmt.Errorf("decoding yaml: %w", err) + } + + return nil +} + +// WriteYamlFile encodes the given data as a yaml document, and writes it to the +// given file path, overwriting any previous data. +func WriteYamlFile(data interface{}, path string) error { + + file, err := os.OpenFile( + path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640, + ) + + if err != nil { + return fmt.Errorf("opening file: %w", err) + } + + err = yaml.NewEncoder(file).Encode(data) + + file.Close() + + if err != nil { + return fmt.Errorf("writing/encoding file: %w", err) + } + + return nil +} + +// LoadYamlFSFile is like LoadYamlFile, but it will read the file from the given +// fs.FS instance. +func LoadYamlFSFile(into interface{}, f fs.FS, path string) error { + + body, err := fs.ReadFile(f, path) + if err != nil { + return fmt.Errorf("reading file from FS: %w", err) + } + + if err := yaml.Unmarshal(body, into); err != nil { + return fmt.Errorf("yaml unmarshaling: %w", err) + } + + return nil +} diff --git a/nix/appimagetool.nix b/nix/appimagetool.nix new file mode 100644 index 0000000..b7a8bd3 --- /dev/null +++ b/nix/appimagetool.nix @@ -0,0 +1,17 @@ +{ + + fetchFromGitHub, + callPackage, + +}: let + + src = fetchFromGitHub { + owner = "matthewbauer"; + repo = "nix-bundle"; + rev = "223f4ffc4179aa318c34dc873a08cb00090db829"; + sha256 = "0pqpx9vnjk9h24h9qlv4la76lh5ykljch6g487b26r1r2s9zg7kh"; + }; + +in + + callPackage "${src}/appimagetool.nix" {} diff --git a/nix/pkgs.nix b/nix/pkgs.nix new file mode 100644 index 0000000..b2cf4f7 --- /dev/null +++ b/nix/pkgs.nix @@ -0,0 +1,78 @@ +rec { + + overlays = [ + + (final: prev: { + + # 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). + rebase = name: oldroot: newroot: prev.stdenv.mkDerivation { + name = name; + inherit oldroot newroot; + builder = builtins.toFile "builder.sh" '' + source $stdenv/setup + mkdir -p "$out"/"$newroot" + cp -rL "$oldroot"/* "$out"/"$newroot" + ''; + }; + + # make buildGoModule default to static compilation + buildGoModule = args: prev.buildGoModule ({ + doCheck = false; + CGO_ENABLED=0; + tags = [ "netgo" "timetzdata" ]; + ldflags = [ "-w" "-extldflags=-static" ]; + } // args); + + }) + + (final: prev: { + + yq-go = prev.buildGoModule rec { + + pname = "yq-go"; + version = "4.21.1"; + + src = prev.fetchFromGitHub { + owner = "mikefarah"; + repo = "yq"; + rev = "v${version}"; + sha256 = "sha256-283xe7FVHYSsRl4cZD7WDzIW1gqNAFsNrWYJkthZheU="; + }; + + vendorSha256 = "sha256-F11FnDYJ59aKrdRXDPpKlhX52yQXdaN1sblSkVI2j9w="; + }; + + nebula = prev.buildGoModule rec { + pname = "nebula"; + + # If this changes, remember to change: + # - the AppDir/etc/daemon.yml vpn.firewall docs + # - the version imported in go-workspace + version = "1.4.0"; + + src = prev.fetchFromGitHub { + owner = "slackhq"; + repo = pname; + rev = "v${version}"; + sha256 = "lu2/rSB9cFD7VUiK+niuqCX9CI2x+k4Pi+U5yksETSU="; + }; + + vendorSha256 = "p1inJ9+NAb2d81cn+y+ofhxFz9ObUiLgj+9cACa6Jqg="; + + subPackages = [ "cmd/nebula" "cmd/nebula-cert" ]; + }; + + }) + + ]; + + stableSrc = fetchTarball { + name = "nixpkgs-21-05"; + url = "https://github.com/NixOS/nixpkgs/archive/7e9b0dff974c89e070da1ad85713ff3c20b0ca97.tar.gz"; + sha256 = "1ckzhh24mgz6jd1xhfgx0i9mijk6xjqxwsshnvq789xsavrmsc36"; + }; + + stable = import stableSrc { inherit overlays; }; +} diff --git a/nix/wait-for.nix b/nix/wait-for.nix new file mode 100644 index 0000000..7da480a --- /dev/null +++ b/nix/wait-for.nix @@ -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/ + ''; +}