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.
main
Brian Picciano 3 years ago
commit b35a3d6574
  1. 4
      .gitignore
  2. 4
      AppDir/AppRun
  3. 9
      AppDir/bin/wait-for-ip
  4. 215
      AppDir/cryptic-logo.svg
  5. 8
      AppDir/cryptic-net.desktop
  6. 76
      AppDir/etc/daemon.yml
  7. 107
      README.md
  8. 116
      default.nix
  9. 34
      dnsmasq/bin/dnsmasq-entrypoint
  10. 39
      dnsmasq/default.nix
  11. 41
      dnsmasq/etc/base.conf
  12. 94
      docs/admin/adding-a-host-to-the-network.md
  13. 69
      docs/dev/daemon-process-tree.plantuml
  14. 90
      docs/dev/daemon-process-tree.svg
  15. 21
      docs/dev/design-principles.md
  16. 14
      docs/dev/rebuilding-documentation.md
  17. 46
      docs/operator/contributing-a-lighthouse.md
  18. 67
      docs/operator/contributing-storage.md
  19. 65
      docs/operator/managing-garage.md
  20. 87
      docs/roadmap.md
  21. 14
      docs/shell.nix
  22. 32
      docs/user/creating-a-daemonyml-file.md
  23. 108
      docs/user/getting-started.md
  24. 37
      docs/user/using-dns.md
  25. 32
      garage/default.nix
  26. 24
      garage/src/bin/garage-apply-layout-diff
  27. 9
      go-workspace/README.md
  28. 18
      go-workspace/default.nix
  29. 34
      go-workspace/src/bootstrap/bootstrap.go
  30. 161
      go-workspace/src/bootstrap/new_for_host.go
  31. 49
      go-workspace/src/bootstrap/new_for_this_host.go
  32. 74
      go-workspace/src/bootstrap/util.go
  33. 83
      go-workspace/src/cmd/cryptic-net-main/main.go
  34. 341
      go-workspace/src/cmd/entrypoint/daemon.go
  35. 158
      go-workspace/src/cmd/entrypoint/garage.go
  36. 281
      go-workspace/src/cmd/entrypoint/hosts.go
  37. 36
      go-workspace/src/cmd/entrypoint/main.go
  38. 121
      go-workspace/src/cmd/entrypoint/sub_cmd.go
  39. 28
      go-workspace/src/cmd/entrypoint/version.go
  40. 151
      go-workspace/src/cmd/garage-entrypoint/main.go
  41. 259
      go-workspace/src/cmd/garage-layout-diff/main.go
  42. 137
      go-workspace/src/cmd/garage-layout-diff/main_test.go
  43. 64
      go-workspace/src/cmd/garage-peer-keygen/main.go
  44. 86
      go-workspace/src/cmd/garage-update-global-bucket/main.go
  45. 157
      go-workspace/src/cmd/nebula-entrypoint/main.go
  46. 62
      go-workspace/src/cmd/nebula-update-global-bucket/main.go
  47. 51
      go-workspace/src/daemon_yml.go
  48. 3
      go-workspace/src/doc.go
  49. 252
      go-workspace/src/env.go
  50. 92
      go-workspace/src/garage/client.go
  51. 100
      go-workspace/src/garage/garageutil.go
  52. 41
      go-workspace/src/garage/infinite_reader.go
  53. 101
      go-workspace/src/garage/infinite_reader_test.go
  54. 76
      go-workspace/src/garage/tpl.go
  55. 62
      go-workspace/src/go.mod
  56. 242
      go-workspace/src/go.sum
  57. 93
      go-workspace/src/hosts.go
  58. 99
      go-workspace/src/proc_lock.go
  59. 37
      go-workspace/src/tarutil/tarutil.go
  60. 151
      go-workspace/src/tarutil/tgz_writer.go
  61. 67
      go-workspace/src/yamlutil/yamlutil.go
  62. 17
      nix/appimagetool.nix
  63. 78
      nix/pkgs.nix
  64. 21
      nix/wait-for.nix

4
.gitignore vendored

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

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

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

@ -0,0 +1,215 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="420"
height="420"
viewBox="0 0 419.99999 419.99998"
id="svg2"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs4">
<marker
orient="auto"
refY="0"
refX="0"
id="SquareL"
style="overflow:visible">
<path
id="path4247"
d="m -5,-5 v 10 h 10 v -10 z"
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt"
transform="scale(0.8)" />
</marker>
<mask
maskUnits="userSpaceOnUse"
id="mask4868">
<g
id="g4870">
<circle
r="66.330765"
cy="507.67978"
cx="316.41565"
id="circle4872"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:12.654;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
r="61.913082"
cy="507.67978"
cx="316.41565"
id="circle4874"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:9.54212;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</mask>
<mask
maskUnits="userSpaceOnUse"
id="mask4868-5">
<g
id="g4870-7">
<circle
r="66.330765"
cy="507.67978"
cx="316.41565"
id="circle4872-4"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:12.654;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
r="61.913082"
cy="507.67978"
cx="316.41565"
id="circle4874-5"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:9.54212;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</mask>
<mask
maskUnits="userSpaceOnUse"
id="mask5253">
<g
id="g5255"
transform="translate(104.04572,-37.375641)">
<g
transform="translate(-104.04571,37.375644)"
id="g5257">
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:12.654;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle5259"
cx="316.41565"
cy="507.67978"
r="67.680229" />
<g
style="display:inline"
id="g5261"
transform="matrix(1.4288827,0,0,1.4288827,-136.36159,-217.40878)">
<g
id="g5263">
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:61.4;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle5265"
cx="316.87503"
cy="507.45145"
r="14.285714" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:12.3153;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 316.87503,507.45145 v 100.04624"
id="path5267" />
</g>
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:12.3153;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 316.87503,507.45145 v 100.04624"
id="path5269"
transform="rotate(-120,316.87503,507.45145)" />
<g
id="g5271"
transform="rotate(120,316.87503,507.45145)">
<circle
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:61.4;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle5273"
cx="316.87503"
cy="507.45145"
r="14.285714" />
<path
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:12.3153;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 316.87503,507.45145 v 100.04624"
id="path5275" />
</g>
</g>
</g>
<circle
r="63.134533"
cy="545.05542"
cx="212.36993"
id="circle5277"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:12.654;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</mask>
<mask
maskUnits="userSpaceOnUse"
id="mask4177">
<g
id="g4187">
<path
id="path4179"
d="m 210.00004,819.92622 v 339.45658"
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:41.7858;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path4181"
d="m 210.00004,819.92622 293.97799,-169.72828"
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:41.7858;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
r="48.471378"
cy="-591.82849"
cx="605.0769"
id="circle4183"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:208.328;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="rotate(120)" />
<path
id="path4185"
d="m 210.00003,819.9262 -293.978001,-169.72827"
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:41.7858;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</mask>
</defs>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
style="display:inline"
transform="translate(0,-652.36225)">
<g
id="g2165"
style="display:inline"
transform="translate(-5.45e-5)">
<path
id="path4443"
d="m 210.00006,819.92622 v 212.95428"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:26.2138;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path4443-5"
d="m 210.00006,819.92622 184.4238,-106.47713"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:26.2138;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
r="30.407976"
cy="-591.82849"
cx="605.0769"
id="path3351-0-8"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:130.693;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="rotate(120)" />
<path
id="path4443-5-2"
d="m 210.00005,819.92621 -184.423801,-106.47713"
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:26.2138;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
<path
style="opacity:1;fill:none;stroke:#000000;stroke-width:8;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path1764"
clip-path="none"
mask="none"
d="m 338.49504,768.01084 a 138.58638,138.58638 0 0 1 -8.47573,121.20856 138.58638,138.58638 0 0 1 -100.73183,67.94448" />
<path
style="display:inline;opacity:1;fill:none;stroke:#000000;stroke-width:8;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path1764-6"
clip-path="none"
mask="none"
d="m 733.57196,-643.7438 a 138.58638,138.58638 0 0 1 -8.47573,121.20856 138.58638,138.58638 0 0 1 -100.73183,67.94448"
transform="rotate(120)" />
<path
style="display:inline;opacity:1;fill:none;stroke:#000000;stroke-width:8;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path1764-6-2"
clip-path="none"
mask="none"
transform="rotate(-120)"
d="m -686.58191,-280.01315 a 138.58638,138.58638 0 0 1 -8.47573,121.20856 138.58638,138.58638 0 0 1 -100.73183,67.944479" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

@ -0,0 +1,8 @@
[Desktop Entry]
Name=Cryptic Net
Name[en]=Cryptic Net
Exec=AppRun
Icon=cryptic-logo
Type=Application
Categories=Network;

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

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

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

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

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

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

@ -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 <name> --ip <ip>
```
## Step 3: Create a `bootstrap.tgz` File
Access to an `admin.tgz` file is required for this step.
To create a `bootstrap.tgz` file for the new host, the admin should perform the
following command from their own host:
```
cryptic-net hosts make-bootstrap \
--name <name> \
--admin-path <path to admin.tgz> \
> 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 <path to admin.tgz.gpg> | cryptic-net hosts make-boostrap \
--name <name> \
--admin-path - \
> bootstrap.tgz
```
Note that the value of `--admin-path` is `-`, indicating that `admin.tgz` should
be read from stdin.
## Step 4: Optionally, Build Binary
If you wish to embed the `bootstrap.tgz` into a custom binary for the user (to
make installation _extremely_ easy for them) then you can run the following:
```
nix-build --arg bootstrap <path to bootstrap.tgz> -A appImage
```
The resulting binary can be found in the `result` directory which is created.
Note that this binary should be treated like a `bootstrap.tgz` in terms of its
uniqueness and sensitivity.

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

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

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

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

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

@ -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 <subcmd> <args>
```
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
```

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

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

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

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

@ -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 `<hostname>.hosts.cryptic.io` hostnames,
where `<hostname>` is any host's name in the `bootstrap/nebula/hosts` directory.
The returned IP will be the corresponding IP for the host, as listed in the
host's `bootstrap/nebula/hosts` file.
If a request for a 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.

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

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

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

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

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

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

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

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

@ -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 <cmd>\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Commands:\n\n")
for _, mainFn := range mainFns {
fmt.Fprintf(os.Stderr, "%s\n", mainFn.name)
}
os.Stderr.Sync()
os.Exit(1)
}
func main() {
if len(os.Args) < 2 {
usage()
}
mainFn, ok := mainFnsMap[os.Args[1]]
if !ok {
usage()
}
// remove os.Args[1] from the arg list, so that other commands which consume
// args don't get confused
os.Args = append(os.Args[:1], os.Args[2:]...)
mainFn.fn()
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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/<hostname>.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/<hostname>.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
}

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

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

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

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

@ -0,0 +1,17 @@
{
fetchFromGitHub,
callPackage,
}: let
src = fetchFromGitHub {
owner = "matthewbauer";
repo = "nix-bundle";
rev = "223f4ffc4179aa318c34dc873a08cb00090db829";
sha256 = "0pqpx9vnjk9h24h9qlv4la76lh5ykljch6g487b26r1r2s9zg7kh";
};
in
callPackage "${src}/appimagetool.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; };
}

@ -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/
'';
}
Loading…
Cancel
Save