Compare commits

...

8 Commits

Author SHA1 Message Date
Brian Picciano
b26f4bdd6a Move proc locking into entrypoint
This completely cleans up all logic that used to be in crypticnet.
2022-10-27 00:45:40 +02:00
Brian Picciano
28159608c8 Factor out crypticnet.Env completely 2022-10-27 00:37:03 +02:00
Brian Picciano
b23a4cafa6 Remove Bootstrap from Env 2022-10-27 00:25:58 +02:00
Brian Picciano
08f47bd514 Move daemon.yml types and functionality out of entrypoint and Env 2022-10-26 23:21:31 +02:00
Brian Picciano
03618ba72c Reimplement dnsmasq-entrypoint in go
This allowed for deleting all script utilities and environment variable
logic.
2022-10-26 22:18:16 +02:00
Brian Picciano
2200d85992 Make populating garage ports optional 2022-10-26 21:47:39 +02:00
Brian Picciano
6ef21ff186 Don't set bootstrap host entry during admin create-network 2022-10-26 21:30:30 +02:00
Brian Picciano
be2250fddd Small fixes to get admin create-network working 2022-10-25 21:15:09 +02:00
36 changed files with 734 additions and 865 deletions

View File

@ -66,11 +66,18 @@ storage:
# #
# The ports are all required and must all be unique within and across # The ports are all required and must all be unique within and across
# allocations. # allocations.
#
# THe ports are all _optional_, and will be automatically assigned if they are
# not specified. If ports any ports are specified then all should be
# specified, and each should be unique across all allocations.
#
# Once assigned (either implicitly or explicitly) the rpc_port of an
# allocation should not be changed.
allocations: allocations:
#- data_path: /foo/bar/data #- data_path: /foo/bar/data
# meta_path: /foo/bar/meta # meta_path: /foo/bar/meta
# capacity: 1200 # capacity: 1200
# api_port: 3900 # #s3_api_port: 3900
# rpc_port: 3901 # #rpc_port: 3901
# admin_port: 3902 # #admin_port: 3902

View File

@ -57,9 +57,11 @@ in rec {
entrypoint = pkgs.callPackage ./entrypoint {}; entrypoint = pkgs.callPackage ./entrypoint {};
dnsmasq = (pkgs.callPackage ./dnsmasq { dnsmasq = (pkgs.callPackage ./nix/dnsmasq.nix {
glibcStatic = pkgs.glibc.static; glibcStatic = pkgs.glibc.static;
}).env; });
nebula = pkgs.callPackage ./nix/nebula.nix {};
garage = (pkgs.callPackage ./nix/garage.nix {}).env; garage = (pkgs.callPackage ./nix/garage.nix {}).env;
@ -69,18 +71,10 @@ in rec {
name = "cryptic-net-AppDir"; name = "cryptic-net-AppDir";
paths = [ paths = [
pkgs.pkgsStatic.bash
pkgs.pkgsStatic.coreutils
pkgs.pkgsStatic.gnutar
pkgs.pkgsStatic.gzip
# custom packages from ./pkgs.nix
pkgs.yq-go
pkgs.nebula
./AppDir ./AppDir
version version
dnsmasq dnsmasq
nebula
garage garage
entrypoint entrypoint

View File

@ -1,36 +0,0 @@
# TODO implement this in go
set -e -o pipefail
cd "$APPDIR"
conf_path="$_RUNTIME_DIR_PATH"/dnsmasq.conf
cat etc/dnsmasq/base.conf > "$conf_path"
tmp="$(mktemp -d -t cryptic-net-dnsmasq-entrypoint-XXX)"
( trap "rm -rf '$tmp'" EXIT
tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./hosts
thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname)
thisHostIP=$(cat "$tmp"/hosts/"$thisHostName".yml | yq '.nebula.ip')
domain=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./admin/creation-params.yml | yq '.domain')
echo "listen-address=$thisHostIP" >> "$conf_path"
ls -1 "$tmp"/hosts | while read hostYml; do
hostName=$(echo "$hostYml" | cut -d. -f1)
hostIP=$(cat "$tmp"/hosts/"$hostYml" | yq '.nebula.ip')
echo "address=/${hostName}.hosts.$domain/$hostIP" >> "$conf_path"
done
)
cat "$_DAEMON_YML_PATH" | \
yq '.dns.resolvers | .[] | "server=" + .' \
>> "$conf_path"
exec bin/dnsmasq -d -C "$conf_path"

View File

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

View File

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

View File

@ -7,7 +7,7 @@
pname = "cryptic-net-entrypoint"; pname = "cryptic-net-entrypoint";
version = "unstable"; version = "unstable";
src = ./src; src = ./src;
vendorSha256 = "sha256-URmrK9Sd/5yhXrWxXZq05TS7aY7IWptQFMKfXKJY7Hc="; vendorSha256 = "sha256-1mHD0tmITlGjeo6F+Dvd2TdEPzxWtndy/J+uGHWKen4=";
subPackages = [ subPackages = [
"cmd/entrypoint" "cmd/entrypoint"
]; ];

View File

@ -25,6 +25,18 @@ const (
hostNamePath = "hostname" hostNamePath = "hostname"
) )
// DataDirPath returns the path within the user's data directory where the
// bootstrap file is stored.
func DataDirPath(dataDirPath string) string {
return filepath.Join(dataDirPath, "bootstrap.tgz")
}
// AppDirPath returns the path within the AppDir where an embedded bootstrap
// file might be found.
func AppDirPath(appDirPath string) string {
return filepath.Join(appDirPath, "share/bootstrap.tgz")
}
// Bootstrap is used for accessing all information contained within a // Bootstrap is used for accessing all information contained within a
// bootstrap.tgz file. // bootstrap.tgz file.
type Bootstrap struct { type Bootstrap struct {

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"cryptic-net/admin" "cryptic-net/admin"
"cryptic-net/bootstrap" "cryptic-net/bootstrap"
"cryptic-net/daemon"
"cryptic-net/garage" "cryptic-net/garage"
"cryptic-net/nebula" "cryptic-net/nebula"
"crypto/rand" "crypto/rand"
@ -53,7 +54,7 @@ var subCmdAdminCreateNetwork = subCmd{
flags := subCmdCtx.flagSet(false) flags := subCmdCtx.flagSet(false)
daemonYmlPath := flags.StringP( daemonConfigPath := flags.StringP(
"config-path", "c", "", "config-path", "c", "",
"Optional path to a daemon.yml file to load configuration from.", "Optional path to a daemon.yml file to load configuration from.",
) )
@ -82,10 +83,8 @@ var subCmdAdminCreateNetwork = subCmd{
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
env := subCmdCtx.env
if *dumpConfig { if *dumpConfig {
return writeBuiltinDaemonYml(env, os.Stdout) return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath)
} }
if *domain == "" || *subnetStr == "" || *hostName == "" { if *domain == "" || *subnetStr == "" || *hostName == "" {
@ -103,36 +102,19 @@ var subCmdAdminCreateNetwork = subCmd{
return fmt.Errorf("invalid hostname %q: %w", *hostName, err) return fmt.Errorf("invalid hostname %q: %w", *hostName, err)
} }
adminCreationParams := admin.CreationParams{ runtimeDirCleanup, err := setupAndLockRuntimeDir()
ID: randStr(32), if err != nil {
Domain: *domain, return fmt.Errorf("setting up runtime directory: %w", err)
}
defer runtimeDirCleanup()
daemonConfig, err := daemon.LoadConfig(envAppDirPath, *daemonConfigPath)
if err != nil {
return fmt.Errorf("loading daemon config: %w", err)
} }
{ if len(daemonConfig.Storage.Allocations) < 3 {
runtimeDirPath := env.RuntimeDirPath return fmt.Errorf("daemon config with at least 3 allocations was not provided")
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)
}
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 err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil {
return fmt.Errorf("merging and writing daemon.yml file: %w", err)
}
daemon := env.ThisDaemon()
if len(daemon.Storage.Allocations) < 3 {
return fmt.Errorf("daemon.yml with at least 3 allocations was not provided")
} }
nebulaCACert, err := nebula.NewCACert(*domain, subnet) nebulaCACert, err := nebula.NewCACert(*domain, subnet)
@ -145,7 +127,12 @@ var subCmdAdminCreateNetwork = subCmd{
return fmt.Errorf("creating nebula cert for host: %w", err) return fmt.Errorf("creating nebula cert for host: %w", err)
} }
env.Bootstrap = bootstrap.Bootstrap{ adminCreationParams := admin.CreationParams{
ID: randStr(32),
Domain: *domain,
}
hostBootstrap := bootstrap.Bootstrap{
AdminCreationParams: adminCreationParams, AdminCreationParams: adminCreationParams,
Hosts: map[string]bootstrap.Host{ Hosts: map[string]bootstrap.Host{
*hostName: bootstrap.Host{ *hostName: bootstrap.Host{
@ -158,27 +145,20 @@ var subCmdAdminCreateNetwork = subCmd{
HostName: *hostName, HostName: *hostName,
NebulaHostCert: nebulaHostCert, NebulaHostCert: nebulaHostCert,
GarageRPCSecret: randStr(32), GarageRPCSecret: randStr(32),
GarageAdminToken: randStr(32),
GarageGlobalBucketS3APICredentials: garage.NewS3APICredentials(), GarageGlobalBucketS3APICredentials: garage.NewS3APICredentials(),
} }
if env, err = mergeDaemonIntoBootstrap(env); err != nil { if hostBootstrap, err = mergeDaemonConfigIntoBootstrap(hostBootstrap, daemonConfig); err != nil {
return fmt.Errorf("merging daemon.yml into bootstrap data: %w", err) return fmt.Errorf("merging daemon config into bootstrap data: %w", err)
} }
// TODO this can be gotten rid of once nebula-entrypoint is rolled into nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(hostBootstrap, daemonConfig)
// daemon itself
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)
}
}
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(env)
if err != nil { if err != nil {
return fmt.Errorf("generating nebula config: %w", err) return fmt.Errorf("generating nebula config: %w", err)
} }
garagePmuxProcConfigs, err := garagePmuxProcConfigs(env) garagePmuxProcConfigs, err := garagePmuxProcConfigs(hostBootstrap, daemonConfig)
if err != nil { if err != nil {
return fmt.Errorf("generating garage configs: %w", err) return fmt.Errorf("generating garage configs: %w", err)
} }
@ -192,12 +172,14 @@ var subCmdAdminCreateNetwork = subCmd{
), ),
} }
ctx, cancel := context.WithCancel(env.Context) ctx, cancel := context.WithCancel(subCmdCtx.ctx)
pmuxDoneCh := make(chan struct{}) pmuxDoneCh := make(chan struct{})
fmt.Fprintln(os.Stderr, "starting child processes") fmt.Fprintln(os.Stderr, "starting child processes")
go func() { go func() {
pmuxlib.Run(ctx, pmuxConfig) // NOTE both stdout and stderr are sent to stderr, so that the user
// can pipe the resulting admin.tgz to stdout.
pmuxlib.Run(ctx, os.Stderr, os.Stderr, pmuxConfig)
close(pmuxDoneCh) close(pmuxDoneCh)
}() }()
@ -208,35 +190,32 @@ var subCmdAdminCreateNetwork = subCmd{
}() }()
fmt.Fprintln(os.Stderr, "waiting for garage instances to come online") fmt.Fprintln(os.Stderr, "waiting for garage instances to come online")
if err := waitForGarageAndNebula(ctx, env); err != nil { if err := waitForGarageAndNebula(ctx, hostBootstrap, daemonConfig); err != nil {
return fmt.Errorf("waiting for garage to start up: %w", err) return fmt.Errorf("waiting for garage to start up: %w", err)
} }
fmt.Fprintln(os.Stderr, "applying initial garage layout") fmt.Fprintln(os.Stderr, "applying initial garage layout")
if err := garageApplyLayout(ctx, env); err != nil { if err := garageApplyLayout(ctx, hostBootstrap, daemonConfig); err != nil {
return fmt.Errorf("applying initial garage layout: %w", err) return fmt.Errorf("applying initial garage layout: %w", err)
} }
fmt.Fprintln(os.Stderr, "initializing garage shared global bucket") fmt.Fprintln(os.Stderr, "initializing garage shared global bucket")
if err := garageInitializeGlobalBucket(ctx, env); err != nil { err = garageInitializeGlobalBucket(ctx, hostBootstrap, daemonConfig)
if cErr := (garage.AdminClientError{}); errors.As(err, &cErr) && cErr.StatusCode == 409 {
return fmt.Errorf("shared global bucket has already been created, are the storage allocations from a previously initialized cryptic-net being used?")
} else if err != nil {
return fmt.Errorf("initializing garage shared global bucket: %w", err) return fmt.Errorf("initializing garage shared global bucket: %w", err)
} }
garageS3Client := env.Bootstrap.GlobalBucketS3APIClient()
fmt.Fprintln(os.Stderr, "writing data for this host into garage")
err = bootstrap.PutGarageBoostrapHost(ctx, garageS3Client, env.Bootstrap.ThisHost())
if err != nil {
return fmt.Errorf("putting host data into garage: %w", err)
}
fmt.Fprintln(os.Stderr, "cluster initialized successfully, writing admin.tgz to stdout") fmt.Fprintln(os.Stderr, "cluster initialized successfully, writing admin.tgz to stdout")
err = admin.Admin{ err = admin.Admin{
CreationParams: adminCreationParams, CreationParams: adminCreationParams,
NebulaCACert: nebulaCACert, NebulaCACert: nebulaCACert,
GarageRPCSecret: env.Bootstrap.GarageRPCSecret, GarageRPCSecret: hostBootstrap.GarageRPCSecret,
GarageGlobalBucketS3APICredentials: env.Bootstrap.GarageGlobalBucketS3APICredentials, GarageGlobalBucketS3APICredentials: hostBootstrap.GarageGlobalBucketS3APICredentials,
GarageAdminBucketS3APICredentials: garage.NewS3APICredentials(), GarageAdminBucketS3APICredentials: garage.NewS3APICredentials(),
}.WriteTo(os.Stdout) }.WriteTo(os.Stdout)
@ -274,20 +253,23 @@ var subCmdAdminMakeBootstrap = subCmd{
return errors.New("--name and --admin-path are required") return errors.New("--name and --admin-path are required")
} }
env := subCmdCtx.env hostBootstrap, err := loadHostBootstrap()
if err != nil {
return fmt.Errorf("loading host bootstrap: %w", err)
}
adm, err := readAdmin(*adminPath) adm, err := readAdmin(*adminPath)
if err != nil { if err != nil {
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err) return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
} }
client := env.Bootstrap.GlobalBucketS3APIClient() client := hostBootstrap.GlobalBucketS3APIClient()
// NOTE this isn't _technically_ required, but if the `hosts add` // NOTE this isn't _technically_ required, but if the `hosts add`
// command for this host has been run recently then it might not have // command for this host has been run recently then it might not have
// made it into the bootstrap file yet, and so won't be in // made it into the bootstrap file yet, and so won't be in
// `env.Bootstrap`. // `hostBootstrap`.
hosts, err := bootstrap.GetGarageBootstrapHosts(env.Context, client) hosts, err := bootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx, client)
if err != nil { if err != nil {
return fmt.Errorf("retrieving host info from garage: %w", err) return fmt.Errorf("retrieving host info from garage: %w", err)
} }
@ -307,7 +289,7 @@ var subCmdAdminMakeBootstrap = subCmd{
return fmt.Errorf("creating new nebula host key/cert: %w", err) return fmt.Errorf("creating new nebula host key/cert: %w", err)
} }
newBootstrap := bootstrap.Bootstrap{ newHostBootstrap := bootstrap.Bootstrap{
AdminCreationParams: adm.CreationParams, AdminCreationParams: adm.CreationParams,
Hosts: hosts, Hosts: hosts,
@ -320,7 +302,7 @@ var subCmdAdminMakeBootstrap = subCmd{
GarageGlobalBucketS3APICredentials: adm.GarageGlobalBucketS3APICredentials, GarageGlobalBucketS3APICredentials: adm.GarageGlobalBucketS3APICredentials,
} }
return newBootstrap.WriteTo(os.Stdout) return newHostBootstrap.WriteTo(os.Stdout)
}, },
} }
@ -329,6 +311,7 @@ var subCmdAdmin = subCmd{
descr: "Sub-commands which only admins can run", descr: "Sub-commands which only admins can run",
do: func(subCmdCtx subCmdCtx) error { do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd( return subCmdCtx.doSubCmd(
subCmdAdminCreateNetwork,
subCmdAdminMakeBootstrap, subCmdAdminMakeBootstrap,
) )
}, },

View File

@ -0,0 +1,44 @@
package main
import (
"cryptic-net/bootstrap"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
)
func loadHostBootstrap() (bootstrap.Bootstrap, error) {
dataDirPath := bootstrap.DataDirPath(envDataDirPath)
hostBootstrap, err := bootstrap.FromFile(dataDirPath)
if errors.Is(err, fs.ErrNotExist) {
return bootstrap.Bootstrap{}, errors.New("%q not found, has the daemon ever been run?")
} else if err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf("loading %q: %w", dataDirPath, err)
}
return hostBootstrap, nil
}
func writeBootstrapToDataDir(hostBootstrap bootstrap.Bootstrap) error {
path := bootstrap.DataDirPath(envDataDirPath)
dirPath := filepath.Dir(path)
if err := os.MkdirAll(dirPath, 0700); err != nil {
return fmt.Errorf("creating directory %q: %w", dirPath, err)
}
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating file %q: %w", path, err)
}
defer f.Close()
return hostBootstrap.WriteTo(f)
}

View File

@ -5,12 +5,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io/fs"
"os" "os"
"sync" "sync"
"time" "time"
crypticnet "cryptic-net"
"cryptic-net/bootstrap" "cryptic-net/bootstrap"
"cryptic-net/daemon"
"cryptic-net/garage" "cryptic-net/garage"
"code.betamike.com/cryptic-io/pmux/pmuxlib" "code.betamike.com/cryptic-io/pmux/pmuxlib"
@ -25,11 +26,8 @@ import (
// * Creates the data directory and copies the appdir bootstrap file into there, // * Creates the data directory and copies the appdir bootstrap file into there,
// if it's not already there. // if it's not already there.
// //
// * Merges the user-provided daemon.yml file with the default, and writes the // * Merges daemon configuration into the bootstrap configuration, and rewrites
// result to the runtime dir. // the bootstrap file.
//
// * Merges daemon.yml configuration into the bootstrap configuration, and
// rewrites the bootstrap file.
// //
// * Sets up environment variables that all other sub-processes then use, based // * Sets up environment variables that all other sub-processes then use, based
// on the runtime dir. // on the runtime dir.
@ -40,108 +38,112 @@ import (
// creates a new bootstrap file using available information from the network. If // 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 // the new bootstrap file is different than the existing one, the existing one
// is overwritten, env's bootstrap is reloaded, true is returned. // is overwritten and true is returned.
func reloadBootstrap(env crypticnet.Env, s3Client garage.S3APIClient) (crypticnet.Env, bool, error) { func reloadBootstrap(
ctx context.Context,
hostBootstrap bootstrap.Bootstrap,
s3Client garage.S3APIClient,
) (
bootstrap.Bootstrap, bool, error,
) {
newHosts, err := bootstrap.GetGarageBootstrapHosts(env.Context, s3Client) newHosts, err := bootstrap.GetGarageBootstrapHosts(ctx, s3Client)
if err != nil { if err != nil {
return crypticnet.Env{}, false, fmt.Errorf("getting hosts from garage: %w", err) return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err)
} }
newHostsHash, err := bootstrap.HostsHash(newHosts) newHostsHash, err := bootstrap.HostsHash(newHosts)
if err != nil { if err != nil {
return crypticnet.Env{}, false, fmt.Errorf("calculating hash of new hosts: %w", err) return bootstrap.Bootstrap{}, false, fmt.Errorf("calculating hash of new hosts: %w", err)
} }
currHostsHash, err := bootstrap.HostsHash(env.Bootstrap.Hosts) currHostsHash, err := bootstrap.HostsHash(hostBootstrap.Hosts)
if err != nil { if err != nil {
return crypticnet.Env{}, false, fmt.Errorf("calculating hash of current hosts: %w", err) return bootstrap.Bootstrap{}, false, fmt.Errorf("calculating hash of current hosts: %w", err)
} }
if bytes.Equal(newHostsHash, currHostsHash) { if bytes.Equal(newHostsHash, currHostsHash) {
return crypticnet.Env{}, false, nil return hostBootstrap, false, nil
} }
buf := new(bytes.Buffer) newHostBootstrap := hostBootstrap.WithHosts(newHosts)
if err := env.Bootstrap.WithHosts(newHosts).WriteTo(buf); err != nil {
return crypticnet.Env{}, false, fmt.Errorf("writing new bootstrap file to buffer: %w", err) if err := writeBootstrapToDataDir(newHostBootstrap); err != nil {
return bootstrap.Bootstrap{}, false, fmt.Errorf("writing new bootstrap.tgz to data dir: %w", err)
} }
if env, err = copyBootstrapToDataDirAndReload(env, buf); err != nil { return newHostBootstrap, true, nil
return crypticnet.Env{}, false, fmt.Errorf("copying new bootstrap file to data dir: %w", err)
}
return env, true, nil
} }
// runs a single pmux process of daemon, returning only once the env.Context has // runs a single pmux process of daemon, returning only once the env.Context has
// been canceled or bootstrap info has been changed. This will always block // been canceled or bootstrap info has been changed. This will always block
// until the spawned pmux has returned, and returns a copy of Env with updated // until the spawned pmux has returned, and returns a copy of hostBootstrap with
// boostrap info. // updated boostrap info.
func runDaemonPmuxOnce(env crypticnet.Env) (crypticnet.Env, error) { func runDaemonPmuxOnce(
ctx context.Context,
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) (
bootstrap.Bootstrap, error,
) {
thisHost := env.Bootstrap.ThisHost() thisHost := hostBootstrap.ThisHost()
thisDaemon := env.ThisDaemon()
fmt.Fprintf(os.Stderr, "host name is %q, ip is %q\n", thisHost.Name, thisHost.Nebula.IP) fmt.Fprintf(os.Stderr, "host name is %q, ip is %q\n", thisHost.Name, thisHost.Nebula.IP)
// create s3Client anew on every loop, in case the topology has // create s3Client anew on every loop, in case the topology has
// changed and we should be connecting to a different garage // changed and we should be connecting to a different garage
// endpoint. // endpoint.
s3Client := env.Bootstrap.GlobalBucketS3APIClient() s3Client := hostBootstrap.GlobalBucketS3APIClient()
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(env) nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(hostBootstrap, daemonConfig)
if err != nil { if err != nil {
return crypticnet.Env{}, fmt.Errorf("generating nebula config: %w", err) return bootstrap.Bootstrap{}, fmt.Errorf("generating nebula config: %w", err)
} }
pmuxProcConfigs := []pmuxlib.ProcessConfig{ dnsmasqPmuxProcConfig, err := dnsmasqPmuxProcConfig(hostBootstrap, daemonConfig)
if err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf("generating dnsmasq config: %w", err)
}
garagePmuxProcConfigs, err := garagePmuxProcConfigs(hostBootstrap, daemonConfig)
if err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf("generating garage children configs: %w", err)
}
pmuxConfig := pmuxlib.Config{
Processes: append(
[]pmuxlib.ProcessConfig{
nebulaPmuxProcConfig, nebulaPmuxProcConfig,
{ dnsmasqPmuxProcConfig,
Name: "dnsmasq",
Cmd: "bash",
Args: []string{"dnsmasq-entrypoint"},
StartAfterFunc: func(ctx context.Context) error {
return waitForNebula(ctx, env)
},
}, },
garagePmuxProcConfigs...,
),
} }
if len(thisDaemon.Storage.Allocations) > 0 { doneCh := ctx.Done()
garagePmuxProcConfigs, err := garagePmuxProcConfigs(env)
if err != nil {
return crypticnet.Env{}, fmt.Errorf("generating garage children configs: %w", err)
}
pmuxProcConfigs = append(pmuxProcConfigs, garagePmuxProcConfigs...)
}
pmuxConfig := pmuxlib.Config{Processes: pmuxProcConfigs}
doneCh := env.Context.Done()
var wg sync.WaitGroup var wg sync.WaitGroup
defer wg.Wait() defer wg.Wait()
ctx, cancel := context.WithCancel(env.Context) ctx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
pmuxlib.Run(ctx, pmuxConfig) pmuxlib.Run(ctx, os.Stdout, os.Stderr, pmuxConfig)
}() }()
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
if err := waitForGarageAndNebula(ctx, env); err != nil { if err := waitForGarageAndNebula(ctx, hostBootstrap, daemonConfig); err != nil {
fmt.Fprintf(os.Stderr, "aborted waiting for garage instances to be accessible: %v\n", err) fmt.Fprintf(os.Stderr, "aborted waiting for garage instances to be accessible: %v\n", err)
return return
} }
thisHost := env.Bootstrap.ThisHost() thisHost := hostBootstrap.ThisHost()
err := doOnce(ctx, func(ctx context.Context) error { err := doOnce(ctx, func(ctx context.Context) error {
fmt.Fprintln(os.Stderr, "updating host info in garage") fmt.Fprintln(os.Stderr, "updating host info in garage")
@ -153,19 +155,19 @@ func runDaemonPmuxOnce(env crypticnet.Env) (crypticnet.Env, error) {
} }
}() }()
if len(thisDaemon.Storage.Allocations) > 0 { if len(daemonConfig.Storage.Allocations) > 0 {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
if err := waitForGarageAndNebula(ctx, env); err != nil { if err := waitForGarageAndNebula(ctx, hostBootstrap, daemonConfig); err != nil {
fmt.Fprintf(os.Stderr, "aborted waiting for garage instances to be accessible: %v\n", err) fmt.Fprintf(os.Stderr, "aborted waiting for garage instances to be accessible: %v\n", err)
return return
} }
err := doOnce(ctx, func(ctx context.Context) error { err := doOnce(ctx, func(ctx context.Context) error {
fmt.Fprintln(os.Stderr, "applying garage layout") fmt.Fprintln(os.Stderr, "applying garage layout")
return garageApplyLayout(ctx, env) return garageApplyLayout(ctx, hostBootstrap, daemonConfig)
}) })
if err != nil { if err != nil {
@ -181,7 +183,7 @@ func runDaemonPmuxOnce(env crypticnet.Env) (crypticnet.Env, error) {
select { select {
case <-doneCh: case <-doneCh:
return crypticnet.Env{}, env.Context.Err() return bootstrap.Bootstrap{}, ctx.Err()
case <-ticker.C: case <-ticker.C:
@ -192,12 +194,12 @@ func runDaemonPmuxOnce(env crypticnet.Env) (crypticnet.Env, error) {
err error err error
) )
if env, changed, err = reloadBootstrap(env, s3Client); err != nil { if hostBootstrap, changed, err = reloadBootstrap(ctx, hostBootstrap, s3Client); err != nil {
return crypticnet.Env{}, fmt.Errorf("reloading bootstrap: %w", err) return bootstrap.Bootstrap{}, fmt.Errorf("reloading bootstrap: %w", err)
} else if changed { } else if changed {
fmt.Fprintln(os.Stderr, "bootstrap info has changed, restarting all processes") fmt.Fprintln(os.Stderr, "bootstrap info has changed, restarting all processes")
return env, nil return hostBootstrap, nil
} }
} }
} }
@ -210,7 +212,7 @@ var subCmdDaemon = subCmd{
flags := subCmdCtx.flagSet(false) flags := subCmdCtx.flagSet(false)
daemonYmlPath := flags.StringP( daemonConfigPath := flags.StringP(
"config-path", "c", "", "config-path", "c", "",
"Optional path to a daemon.yml file to load configuration from.", "Optional path to a daemon.yml file to load configuration from.",
) )
@ -229,91 +231,81 @@ var subCmdDaemon = subCmd{
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
env := subCmdCtx.env
if *dumpConfig { if *dumpConfig {
return writeBuiltinDaemonYml(env, os.Stdout) return daemon.CopyDefaultConfig(os.Stdout, envAppDirPath)
} }
runtimeDirPath := env.RuntimeDirPath runtimeDirCleanup, err := setupAndLockRuntimeDir()
if err != nil {
return fmt.Errorf("setting up runtime directory: %w", err)
}
defer runtimeDirCleanup()
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath) var (
bootstrapDataDirPath = bootstrap.DataDirPath(envDataDirPath)
bootstrapAppDirPath = bootstrap.AppDirPath(envAppDirPath)
if err := os.MkdirAll(runtimeDirPath, 0700); err != nil { hostBootstrapPath string
return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err) hostBootstrap bootstrap.Bootstrap
foundHostBootstrap bool
)
} else if err := crypticnet.NewProcLock(runtimeDirPath).WriteLock(); err != nil { tryLoadBootstrap := func(path string) bool {
return err
if err != nil {
return false
} else if hostBootstrap, err = bootstrap.FromFile(path); errors.Is(err, fs.ErrNotExist) {
err = nil
return false
} else if err != nil {
err = fmt.Errorf("parsing bootstrap.tgz at %q: %w", path, err)
return false
} }
// do not defer the cleaning of the runtime directory until the lock has hostBootstrapPath = path
// been obtained, otherwise we might delete the directory out from under return true
// 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 foundHostBootstrap = tryLoadBootstrap(bootstrapDataDirPath)
// there and reload the bootstrap info foundHostBootstrap = !foundHostBootstrap && *bootstrapPath != "" && tryLoadBootstrap(*bootstrapPath)
if env.BootstrapPath != env.DataDirBootstrapPath() { foundHostBootstrap = !foundHostBootstrap && tryLoadBootstrap(bootstrapAppDirPath)
path := env.BootstrapPath if err != nil {
return fmt.Errorf("attempting to load bootstrap.tgz file: %w", err)
// If there's no BootstrapPath then no bootstrap file could be } else if !foundHostBootstrap {
// 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") return errors.New("No bootstrap.tgz file could be found, and one is not provided with --bootstrap-path")
} else if hostBootstrapPath != bootstrapDataDirPath {
// If the bootstrap file is not being stored in the data dir, copy
// it there, so it can be loaded from there next time.
if err := writeBootstrapToDataDir(hostBootstrap); err != nil {
return fmt.Errorf("writing bootstrap.tgz to data dir: %w", err)
}
} }
path = *bootstrapPath daemonConfig, err := daemon.LoadConfig(envAppDirPath, *daemonConfigPath)
}
f, err := os.Open(path)
if err != nil { if err != nil {
return fmt.Errorf("opening file %q: %w", env.BootstrapPath, err) return fmt.Errorf("loading daemon config: %w", err)
} }
env, err = copyBootstrapToDataDirAndReload(env, f)
f.Close()
if err != nil {
return fmt.Errorf("copying bootstrap file from %q: %w", path, err)
}
}
if err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil {
return fmt.Errorf("merging and writing daemon.yml file: %w", err)
}
var err error
// we update this Host's data using whatever configuration has been // we update this Host's data using whatever configuration has been
// provided by daemon.yml. This way the daemon has the most // provided by the daemon config. This way the daemon has the most
// up-to-date possible bootstrap. This updated bootstrap will later // up-to-date possible bootstrap. This updated bootstrap will later get
// get updated in garage using update-global-bucket, so other hosts // updated in garage using bootstrap.PutGarageBoostrapHost, so other
// will see it as well. // hosts will see it as well.
if env, err = mergeDaemonIntoBootstrap(env); err != nil { if hostBootstrap, err = mergeDaemonConfigIntoBootstrap(hostBootstrap, daemonConfig); err != nil {
return fmt.Errorf("merging daemon.yml into bootstrap data: %w", err) return fmt.Errorf("merging daemon config into bootstrap data: %w", err)
}
// TODO once dnsmasq entrypoint is written in go and the config
// generation is inlined into this process then this Setenv won't be
// necessary.
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 { for {
if env, err = runDaemonPmuxOnce(env); errors.Is(err, context.Canceled) { hostBootstrap, err = runDaemonPmuxOnce(subCmdCtx.ctx, hostBootstrap, daemonConfig)
if errors.Is(err, context.Canceled) {
return nil return nil
} else if err != nil { } else if err != nil {

View File

@ -1,50 +1,26 @@
package main package main
import ( import (
"bytes"
"context" "context"
crypticnet "cryptic-net"
"cryptic-net/bootstrap" "cryptic-net/bootstrap"
"cryptic-net/daemon"
"fmt" "fmt"
"io"
"os"
"path/filepath"
"time" "time"
) )
func copyBootstrapToDataDirAndReload(env crypticnet.Env, r io.Reader) (crypticnet.Env, error) { func mergeDaemonConfigIntoBootstrap(
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) (
bootstrap.Bootstrap, error,
) {
host := hostBootstrap.ThisHost()
path := env.DataDirBootstrapPath() host.Nebula.PublicAddr = daemonConfig.VPN.PublicAddr
dirPath := filepath.Dir(path)
if err := os.MkdirAll(dirPath, 0700); err != nil {
return crypticnet.Env{}, fmt.Errorf("creating directory %q: %w", dirPath, err)
}
f, err := os.Create(path)
if err != nil {
return crypticnet.Env{}, fmt.Errorf("creating file %q: %w", path, err)
}
_, err = io.Copy(f, r)
f.Close()
if err != nil {
return crypticnet.Env{}, fmt.Errorf("copying bootstrap file to %q: %w", path, err)
}
return env.LoadBootstrap(path)
}
func mergeDaemonIntoBootstrap(env crypticnet.Env) (crypticnet.Env, error) {
daemon := env.ThisDaemon()
host := env.Bootstrap.ThisHost()
host.Nebula.PublicAddr = daemon.VPN.PublicAddr
host.Garage = nil host.Garage = nil
if allocs := daemon.Storage.Allocations; len(allocs) > 0 { if allocs := daemonConfig.Storage.Allocations; len(allocs) > 0 {
host.Garage = new(bootstrap.GarageHost) host.Garage = new(bootstrap.GarageHost)
@ -56,14 +32,13 @@ func mergeDaemonIntoBootstrap(env crypticnet.Env) (crypticnet.Env, error) {
} }
} }
env.Bootstrap.Hosts[host.Name] = host hostBootstrap.Hosts[host.Name] = host
buf := new(bytes.Buffer) if err := writeBootstrapToDataDir(hostBootstrap); err != nil {
if err := env.Bootstrap.WithHosts(env.Bootstrap.Hosts).WriteTo(buf); err != nil { return bootstrap.Bootstrap{}, fmt.Errorf("writing bootstrap file: %w", err)
return crypticnet.Env{}, fmt.Errorf("writing new bootstrap file to buffer: %w", err)
} }
return copyBootstrapToDataDirAndReload(env, buf) return hostBootstrap, nil
} }
func doOnce(ctx context.Context, fn func(context.Context) error) error { func doOnce(ctx context.Context, fn func(context.Context) error) error {

View File

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

View File

@ -0,0 +1,48 @@
package main
import (
"cryptic-net/bootstrap"
"cryptic-net/daemon"
"cryptic-net/dnsmasq"
"fmt"
"path/filepath"
"sort"
"code.betamike.com/cryptic-io/pmux/pmuxlib"
)
func dnsmasqPmuxProcConfig(
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) (
pmuxlib.ProcessConfig, error,
) {
confPath := filepath.Join(envRuntimeDirPath, "dnsmasq.conf")
hostsSlice := make([]bootstrap.Host, 0, len(hostBootstrap.Hosts))
for _, host := range hostBootstrap.Hosts {
hostsSlice = append(hostsSlice, host)
}
sort.Slice(hostsSlice, func(i, j int) bool {
return hostsSlice[i].Nebula.IP < hostsSlice[j].Nebula.IP
})
confData := dnsmasq.ConfData{
Resolvers: daemonConfig.DNS.Resolvers,
Domain: hostBootstrap.AdminCreationParams.Domain,
IP: hostBootstrap.ThisHost().Nebula.IP,
Hosts: hostsSlice,
}
if err := dnsmasq.WriteConfFile(confPath, confData); err != nil {
return pmuxlib.ProcessConfig{}, fmt.Errorf("writing dnsmasq.conf to %q: %w", confPath, err)
}
return pmuxlib.ProcessConfig{
Name: "dnsmasq",
Cmd: "dnsmasq",
Args: []string{"-d", "-C", confPath},
}, nil
}

View File

@ -28,18 +28,21 @@ var subCmdGarageMC = subCmd{
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
env := subCmdCtx.env hostBootstrap, err := loadHostBootstrap()
if err != nil {
return fmt.Errorf("loading host bootstrap: %w", err)
}
s3APIAddr := env.Bootstrap.ChooseGaragePeer().S3APIAddr() s3APIAddr := hostBootstrap.ChooseGaragePeer().S3APIAddr()
if *keyID == "" || *keySecret == "" { if *keyID == "" || *keySecret == "" {
if *keyID == "" { if *keyID == "" {
*keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.ID *keyID = hostBootstrap.GarageGlobalBucketS3APICredentials.ID
} }
if *keySecret == "" { if *keySecret == "" {
*keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.Secret *keyID = hostBootstrap.GarageGlobalBucketS3APICredentials.Secret
} }
} }
@ -52,7 +55,7 @@ var subCmdGarageMC = subCmd{
args = append([]string{"mc"}, args...) args = append([]string{"mc"}, args...)
var ( var (
binPath = env.BinPath("mc") binPath = "mc"
cliEnv = append( cliEnv = append(
os.Environ(), os.Environ(),
fmt.Sprintf( fmt.Sprintf(
@ -83,15 +86,18 @@ var subCmdGarageCLI = subCmd{
checkLock: true, checkLock: true,
do: func(subCmdCtx subCmdCtx) error { do: func(subCmdCtx subCmdCtx) error {
env := subCmdCtx.env hostBootstrap, err := loadHostBootstrap()
if err != nil {
return fmt.Errorf("loading host bootstrap: %w", err)
}
var ( var (
binPath = env.BinPath("garage") binPath = "garage"
args = append([]string{"garage"}, subCmdCtx.args...) args = append([]string{"garage"}, subCmdCtx.args...)
cliEnv = append( cliEnv = append(
os.Environ(), os.Environ(),
"GARAGE_RPC_HOST="+env.Bootstrap.ChooseGaragePeer().RPCAddr(), "GARAGE_RPC_HOST="+hostBootstrap.ChooseGaragePeer().RPCAddr(),
"GARAGE_RPC_SECRET="+env.Bootstrap.GarageRPCSecret, "GARAGE_RPC_SECRET="+hostBootstrap.GarageRPCSecret,
) )
) )

View File

@ -2,7 +2,8 @@ package main
import ( import (
"context" "context"
crypticnet "cryptic-net" "cryptic-net/bootstrap"
"cryptic-net/daemon"
"cryptic-net/garage" "cryptic-net/garage"
"fmt" "fmt"
"net" "net"
@ -13,26 +14,47 @@ import (
"code.betamike.com/cryptic-io/pmux/pmuxlib" "code.betamike.com/cryptic-io/pmux/pmuxlib"
) )
func waitForGarageAndNebula(ctx context.Context, env crypticnet.Env) error { // newGarageAdminClient will return an AdminClient for a local garage instance,
// or it will _panic_ if there is no local instance configured.
func newGarageAdminClient(
hostBootstrap bootstrap.Bootstrap, daemonConfig daemon.Config,
) *garage.AdminClient {
allocs := env.ThisDaemon().Storage.Allocations thisHost := hostBootstrap.ThisHost()
return garage.NewAdminClient(
net.JoinHostPort(
thisHost.Nebula.IP,
strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort),
),
hostBootstrap.GarageAdminToken,
)
}
func waitForGarageAndNebula(
ctx context.Context,
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) error {
allocs := daemonConfig.Storage.Allocations
// if this host doesn't have any allocations specified then fall back to // if this host doesn't have any allocations specified then fall back to
// waiting for nebula // waiting for nebula
if len(allocs) == 0 { if len(allocs) == 0 {
return waitForNebula(ctx, env) return waitForNebula(ctx, hostBootstrap)
} }
for _, alloc := range allocs { for _, alloc := range allocs {
adminAddr := net.JoinHostPort( adminAddr := net.JoinHostPort(
env.Bootstrap.ThisHost().Nebula.IP, hostBootstrap.ThisHost().Nebula.IP,
strconv.Itoa(alloc.AdminPort), strconv.Itoa(alloc.AdminPort),
) )
adminClient := garage.NewAdminClient( adminClient := garage.NewAdminClient(
adminAddr, adminAddr,
env.Bootstrap.GarageAdminToken, hostBootstrap.GarageAdminToken,
) )
if err := adminClient.Wait(ctx); err != nil { if err := adminClient.Wait(ctx); err != nil {
@ -44,9 +66,9 @@ func waitForGarageAndNebula(ctx context.Context, env crypticnet.Env) error {
} }
func garageWriteChildConf( func garageWriteChildConfig(
env crypticnet.Env, hostBootstrap bootstrap.Bootstrap,
alloc crypticnet.DaemonYmlStorageAllocation, alloc daemon.ConfigStorageAllocation,
) ( ) (
string, error, string, error,
) { ) {
@ -55,7 +77,7 @@ func garageWriteChildConf(
return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err) return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err)
} }
thisHost := env.Bootstrap.ThisHost() thisHost := hostBootstrap.ThisHost()
peer := garage.Peer{ peer := garage.Peer{
IP: thisHost.Nebula.IP, IP: thisHost.Nebula.IP,
@ -76,21 +98,21 @@ func garageWriteChildConf(
} }
garageTomlPath := filepath.Join( garageTomlPath := filepath.Join(
env.RuntimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort), envRuntimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort),
) )
err := garage.WriteGarageTomlFile(garageTomlPath, garage.GarageTomlData{ err := garage.WriteGarageTomlFile(garageTomlPath, garage.GarageTomlData{
MetaPath: alloc.MetaPath, MetaPath: alloc.MetaPath,
DataPath: alloc.DataPath, DataPath: alloc.DataPath,
RPCSecret: env.Bootstrap.GarageRPCSecret, RPCSecret: hostBootstrap.GarageRPCSecret,
AdminToken: env.Bootstrap.GarageAdminToken, AdminToken: hostBootstrap.GarageAdminToken,
RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)), RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)), APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)),
AdminAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.AdminPort)), AdminAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.AdminPort)),
BootstrapPeers: env.Bootstrap.GarageRPCPeerAddrs(), BootstrapPeers: hostBootstrap.GarageRPCPeerAddrs(),
}) })
if err != nil { if err != nil {
@ -100,13 +122,18 @@ func garageWriteChildConf(
return garageTomlPath, nil return garageTomlPath, nil
} }
func garagePmuxProcConfigs(env crypticnet.Env) ([]pmuxlib.ProcessConfig, error) { func garagePmuxProcConfigs(
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) (
[]pmuxlib.ProcessConfig, error,
) {
var pmuxProcConfigs []pmuxlib.ProcessConfig var pmuxProcConfigs []pmuxlib.ProcessConfig
for _, alloc := range env.ThisDaemon().Storage.Allocations { for _, alloc := range daemonConfig.Storage.Allocations {
childConfPath, err := garageWriteChildConf(env, alloc) childConfigPath, err := garageWriteChildConfig(hostBootstrap, alloc)
if err != nil { if err != nil {
return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err) return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
@ -115,9 +142,9 @@ func garagePmuxProcConfigs(env crypticnet.Env) ([]pmuxlib.ProcessConfig, error)
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{ pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
Name: fmt.Sprintf("garage-%d", alloc.RPCPort), Name: fmt.Sprintf("garage-%d", alloc.RPCPort),
Cmd: "garage", Cmd: "garage",
Args: []string{"-c", childConfPath, "server"}, Args: []string{"-c", childConfigPath, "server"},
StartAfterFunc: func(ctx context.Context) error { StartAfterFunc: func(ctx context.Context) error {
return waitForNebula(ctx, env) return waitForNebula(ctx, hostBootstrap)
}, },
}) })
} }
@ -125,11 +152,15 @@ func garagePmuxProcConfigs(env crypticnet.Env) ([]pmuxlib.ProcessConfig, error)
return pmuxProcConfigs, nil return pmuxProcConfigs, nil
} }
func garageInitializeGlobalBucket(ctx context.Context, env crypticnet.Env) error { func garageInitializeGlobalBucket(
ctx context.Context,
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) error {
var ( var (
adminClient = env.GarageAdminClient() adminClient = newGarageAdminClient(hostBootstrap, daemonConfig)
globalBucketCreds = env.Bootstrap.GarageGlobalBucketS3APICredentials globalBucketCreds = hostBootstrap.GarageGlobalBucketS3APICredentials
) )
// first attempt to import the key // first attempt to import the key
@ -183,14 +214,18 @@ func garageInitializeGlobalBucket(ctx context.Context, env crypticnet.Env) error
return nil return nil
} }
func garageApplyLayout(ctx context.Context, env crypticnet.Env) error { func garageApplyLayout(
ctx context.Context,
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) error {
var ( var (
adminClient = env.GarageAdminClient() adminClient = newGarageAdminClient(hostBootstrap, daemonConfig)
thisHost = env.Bootstrap.ThisHost() thisHost = hostBootstrap.ThisHost()
hostName = thisHost.Name hostName = thisHost.Name
ip = thisHost.Nebula.IP ip = thisHost.Nebula.IP
allocs = env.ThisDaemon().Storage.Allocations allocs = daemonConfig.Storage.Allocations
) )
type peerLayout struct { type peerLayout struct {
@ -213,6 +248,7 @@ func garageApplyLayout(ctx context.Context, env crypticnet.Env) error {
clusterLayout[peer.RPCPeerID()] = peerLayout{ clusterLayout[peer.RPCPeerID()] = peerLayout{
Capacity: alloc.Capacity / 100, Capacity: alloc.Capacity / 100,
Zone: hostName, Zone: hostName,
Tags: []string{},
} }
} }

View File

@ -59,8 +59,12 @@ var subCmdHostsAdd = subCmd{
// TODO validate that the IP is in the correct CIDR // TODO validate that the IP is in the correct CIDR
env := subCmdCtx.env hostBootstrap, err := loadHostBootstrap()
client := env.Bootstrap.GlobalBucketS3APIClient() if err != nil {
return fmt.Errorf("loading host bootstrap: %w", err)
}
client := hostBootstrap.GlobalBucketS3APIClient()
host := bootstrap.Host{ host := bootstrap.Host{
Name: *name, Name: *name,
@ -69,7 +73,7 @@ var subCmdHostsAdd = subCmd{
}, },
} }
return bootstrap.PutGarageBoostrapHost(env.Context, client, host) return bootstrap.PutGarageBoostrapHost(subCmdCtx.ctx, client, host)
}, },
} }
@ -79,11 +83,14 @@ var subCmdHostsList = subCmd{
checkLock: true, checkLock: true,
do: func(subCmdCtx subCmdCtx) error { do: func(subCmdCtx subCmdCtx) error {
env := subCmdCtx.env hostBootstrap, err := loadHostBootstrap()
if err != nil {
return fmt.Errorf("loading host bootstrap: %w", err)
}
client := env.Bootstrap.GlobalBucketS3APIClient() client := hostBootstrap.GlobalBucketS3APIClient()
hostsMap, err := bootstrap.GetGarageBootstrapHosts(env.Context, client) hostsMap, err := bootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx, client)
if err != nil { if err != nil {
return fmt.Errorf("retrieving hosts from garage: %w", err) return fmt.Errorf("retrieving hosts from garage: %w", err)
} }
@ -120,10 +127,14 @@ var subCmdHostsDelete = subCmd{
return errors.New("--name is required") return errors.New("--name is required")
} }
env := subCmdCtx.env hostBootstrap, err := loadHostBootstrap()
client := env.Bootstrap.GlobalBucketS3APIClient() if err != nil {
return fmt.Errorf("loading host bootstrap: %w", err)
}
return bootstrap.RemoveGarageBootstrapHost(env.Context, client, *name) client := hostBootstrap.GlobalBucketS3APIClient()
return bootstrap.RemoveGarageBootstrapHost(subCmdCtx.ctx, client, *name)
}, },
} }

View File

@ -1,10 +1,14 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"os/signal"
"path/filepath"
"syscall"
crypticnet "cryptic-net" "github.com/adrg/xdg"
) )
// The purpose of this binary is to act as the entrypoint of the cryptic-net // The purpose of this binary is to act as the entrypoint of the cryptic-net
@ -12,17 +16,42 @@ import (
// then passes execution along to an appropriate binary housed in AppDir/bin // then passes execution along to an appropriate binary housed in AppDir/bin
// (usually a bash script, which is more versatile than a go program). // (usually a bash script, which is more versatile than a go program).
func main() { func getAppDirPath() string {
appDirPath := os.Getenv("APPDIR")
env, err := crypticnet.NewEnv(true) if appDirPath == "" {
appDirPath = "."
if err != nil { }
panic(fmt.Sprintf("loading environment: %v", err)) return appDirPath
} }
err = subCmdCtx{ var (
envAppDirPath = getAppDirPath()
envRuntimeDirPath = filepath.Join(xdg.RuntimeDir, "cryptic-net")
envDataDirPath = filepath.Join(xdg.DataHome, "cryptic-net")
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
signalCh := make(chan os.Signal, 2)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-signalCh
cancel()
fmt.Fprintf(os.Stderr, "got signal %v, will exit gracefully\n", sig)
sig = <-signalCh
fmt.Fprintf(os.Stderr, "second interrupt signal %v received, force quitting, there may be zombie children left behind, good luck!\n", sig)
os.Stderr.Sync()
os.Exit(1)
}()
err := subCmdCtx{
args: os.Args[1:], args: os.Args[1:],
env: env, ctx: ctx,
}.doSubCmd( }.doSubCmd(
subCmdAdmin, subCmdAdmin,
subCmdDaemon, subCmdDaemon,

View File

@ -2,12 +2,12 @@ package main
import ( import (
"context" "context"
crypticnet "cryptic-net" "cryptic-net/bootstrap"
"cryptic-net/daemon"
"cryptic-net/yamlutil" "cryptic-net/yamlutil"
"fmt" "fmt"
"net" "net"
"path/filepath" "path/filepath"
"strconv"
"code.betamike.com/cryptic-io/pmux/pmuxlib" "code.betamike.com/cryptic-io/pmux/pmuxlib"
) )
@ -16,9 +16,9 @@ import (
// this by attempting to create a UDP connection which has the nebula IP set as // this by attempting to create a UDP connection which has the nebula IP set as
// its source. If this succeeds we can assume that at the very least the nebula // its source. If this succeeds we can assume that at the very least the nebula
// interface has been initialized. // interface has been initialized.
func waitForNebula(ctx context.Context, env crypticnet.Env) error { func waitForNebula(ctx context.Context, hostBootstrap bootstrap.Bootstrap) error {
ipStr := env.Bootstrap.ThisHost().Nebula.IP ipStr := hostBootstrap.ThisHost().Nebula.IP
ip := net.ParseIP(ipStr) ip := net.ParseIP(ipStr)
lUdpAddr := &net.UDPAddr{IP: ip, Port: 0} lUdpAddr := &net.UDPAddr{IP: ip, Port: 0}
@ -34,14 +34,19 @@ func waitForNebula(ctx context.Context, env crypticnet.Env) error {
}) })
} }
func nebulaPmuxProcConfig(env crypticnet.Env) (pmuxlib.ProcessConfig, error) { func nebulaPmuxProcConfig(
hostBootstrap bootstrap.Bootstrap,
daemonConfig daemon.Config,
) (
pmuxlib.ProcessConfig, error,
) {
var ( var (
lighthouseHostIPs []string lighthouseHostIPs []string
staticHostMap = map[string][]string{} staticHostMap = map[string][]string{}
) )
for _, host := range env.Bootstrap.Hosts { for _, host := range hostBootstrap.Hosts {
if host.Nebula.PublicAddr == "" { if host.Nebula.PublicAddr == "" {
continue continue
@ -53,9 +58,9 @@ func nebulaPmuxProcConfig(env crypticnet.Env) (pmuxlib.ProcessConfig, error) {
config := map[string]interface{}{ config := map[string]interface{}{
"pki": map[string]string{ "pki": map[string]string{
"ca": env.Bootstrap.NebulaHostCert.CACert, "ca": hostBootstrap.NebulaHostCert.CACert,
"cert": env.Bootstrap.NebulaHostCert.HostCert, "cert": hostBootstrap.NebulaHostCert.HostCert,
"key": env.Bootstrap.NebulaHostCert.HostKey, "key": hostBootstrap.NebulaHostCert.HostKey,
}, },
"static_host_map": staticHostMap, "static_host_map": staticHostMap,
"punchy": map[string]bool{ "punchy": map[string]bool{
@ -63,11 +68,12 @@ func nebulaPmuxProcConfig(env crypticnet.Env) (pmuxlib.ProcessConfig, error) {
"respond": true, "respond": true,
}, },
"tun": map[string]interface{}{ "tun": map[string]interface{}{
"dev": "cryptic-nebula1", "dev": "cryptic-net-nebula",
}, },
"firewall": daemonConfig.VPN.Firewall,
} }
if publicAddr := env.ThisDaemon().VPN.PublicAddr; publicAddr == "" { if publicAddr := daemonConfig.VPN.PublicAddr; publicAddr == "" {
config["listen"] = map[string]string{ config["listen"] = map[string]string{
"host": "0.0.0.0", "host": "0.0.0.0",
@ -97,33 +103,7 @@ func nebulaPmuxProcConfig(env crypticnet.Env) (pmuxlib.ProcessConfig, error) {
} }
} }
thisDaemon := env.ThisDaemon() nebulaYmlPath := filepath.Join(envRuntimeDirPath, "nebula.yml")
var firewallInbound []crypticnet.ConfigFirewallRule
for _, alloc := range thisDaemon.Storage.Allocations {
firewallInbound = append(
firewallInbound,
crypticnet.ConfigFirewallRule{
Port: strconv.Itoa(alloc.S3APIPort),
Proto: "tcp",
Host: "any",
},
crypticnet.ConfigFirewallRule{
Port: strconv.Itoa(alloc.RPCPort),
Proto: "tcp",
Host: "any",
},
)
}
firewall := thisDaemon.VPN.Firewall
firewall.Inbound = append(firewallInbound, firewall.Inbound...)
config["firewall"] = firewall
nebulaYmlPath := filepath.Join(env.RuntimeDirPath, "nebula.yml")
if err := yamlutil.WriteYamlFile(config, nebulaYmlPath); err != nil { if err := yamlutil.WriteYamlFile(config, nebulaYmlPath); err != nil {
return pmuxlib.ProcessConfig{}, fmt.Errorf("writing nebula.yml to %q: %w", nebulaYmlPath, err) return pmuxlib.ProcessConfig{}, fmt.Errorf("writing nebula.yml to %q: %w", nebulaYmlPath, err)

View File

@ -1,4 +1,4 @@
package crypticnet package main
import ( import (
"errors" "errors"
@ -12,33 +12,13 @@ import (
var errDaemonNotRunning = errors.New("no cryptic-net daemon process running") var errDaemonNotRunning = errors.New("no cryptic-net daemon process running")
// ProcLock is used to lock a process. func lockFilePath() string {
type ProcLock interface { return filepath.Join(envRuntimeDirPath, "lock")
// 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 { func writeLock() error {
dir string
}
// NewProcLock returns a ProcLock which will use a file in the given directory lockFilePath := lockFilePath()
// 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( lockFile, err := os.OpenFile(
lockFilePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400, lockFilePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400,
@ -63,11 +43,31 @@ func (pl *procLock) WriteLock() error {
return nil return nil
} }
// returns a cleanup function which will clean up the created runtime directory.
func setupAndLockRuntimeDir() (func(), error) {
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", envRuntimeDirPath)
if err := os.MkdirAll(envRuntimeDirPath, 0700); err != nil {
return nil, fmt.Errorf("creating directory %q: %w", envRuntimeDirPath, err)
} else if err := writeLock(); err != nil {
return nil, err
}
return func() {
fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", envRuntimeDirPath)
if err := os.RemoveAll(envRuntimeDirPath); err != nil {
fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", envRuntimeDirPath, err)
}
}, nil
}
// checks that the lock file exists and that the process which created it also // checks that the lock file exists and that the process which created it also
// still exists. // still exists.
func (pl *procLock) AssertLock() error { func assertLock() error {
lockFilePath := pl.path() lockFilePath := lockFilePath()
lockFile, err := os.Open(lockFilePath) lockFile, err := os.Open(lockFilePath)

View File

@ -1,7 +1,7 @@
package main package main
import ( import (
crypticnet "cryptic-net" "context"
"fmt" "fmt"
"os" "os"
"strings" "strings"
@ -14,7 +14,8 @@ type subCmdCtx struct {
subCmd subCmd // the subCmd itself subCmd subCmd // the subCmd itself
args []string // command-line arguments, excluding the subCmd itself. args []string // command-line arguments, excluding the subCmd itself.
subCmdNames []string // names of subCmds so far, including this one subCmdNames []string // names of subCmds so far, including this one
env crypticnet.Env
ctx context.Context
} }
type subCmd struct { type subCmd struct {
@ -99,9 +100,7 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
if subCmd.checkLock { if subCmd.checkLock {
err := crypticnet.NewProcLock(ctx.env.RuntimeDirPath).AssertLock() if err := assertLock(); err != nil {
if err != nil {
return fmt.Errorf("checking lock file: %w", err) return fmt.Errorf("checking lock file: %w", err)
} }
} }
@ -110,7 +109,7 @@ func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
subCmd: subCmd, subCmd: subCmd,
args: args, args: args,
subCmdNames: append(ctx.subCmdNames, subCmdName), subCmdNames: append(ctx.subCmdNames, subCmdName),
env: ctx.env, ctx: ctx.ctx,
}) })
if err != nil { if err != nil {

View File

@ -11,7 +11,7 @@ var subCmdVersion = subCmd{
descr: "Dumps version and build info to stdout", descr: "Dumps version and build info to stdout",
do: func(subCmdCtx subCmdCtx) error { do: func(subCmdCtx subCmdCtx) error {
versionPath := filepath.Join(subCmdCtx.env.AppDirPath, "share/version") versionPath := filepath.Join(envAppDirPath, "share/version")
version, err := os.ReadFile(versionPath) version, err := os.ReadFile(versionPath)

View File

@ -1,4 +1,6 @@
package crypticnet package daemon
import "strconv"
type ConfigFirewall struct { type ConfigFirewall struct {
Conntrack ConfigConntrack `yaml:"conntrack"` Conntrack ConfigConntrack `yaml:"conntrack"`
@ -25,9 +27,9 @@ type ConfigFirewallRule struct {
CAName string `yaml:"ca_name,omitempty"` CAName string `yaml:"ca_name,omitempty"`
} }
// DaemonYmlStorageAllocation describes the structure of each storage allocation // ConfigStorageAllocation describes the structure of each storage allocation
// within the daemon.yml file. // within the daemon config file.
type DaemonYmlStorageAllocation struct { type ConfigStorageAllocation struct {
DataPath string `yaml:"data_path"` DataPath string `yaml:"data_path"`
MetaPath string `yaml:"meta_path"` MetaPath string `yaml:"meta_path"`
Capacity int `yaml:"capacity"` Capacity int `yaml:"capacity"`
@ -36,8 +38,8 @@ type DaemonYmlStorageAllocation struct {
AdminPort int `yaml:"admin_port"` AdminPort int `yaml:"admin_port"`
} }
// DaemonYml describes the structure of the daemon.yml file. // Config describes the structure of the daemon config file.
type DaemonYml struct { type Config struct {
DNS struct { DNS struct {
Resolvers []string `yaml:"resolvers"` Resolvers []string `yaml:"resolvers"`
} `yaml:"dns"` } `yaml:"dns"`
@ -46,6 +48,47 @@ type DaemonYml struct {
Firewall ConfigFirewall `yaml:"firewall"` Firewall ConfigFirewall `yaml:"firewall"`
} `yaml:"vpn"` } `yaml:"vpn"`
Storage struct { Storage struct {
Allocations []DaemonYmlStorageAllocation Allocations []ConfigStorageAllocation
} `yaml:"storage"` } `yaml:"storage"`
} }
func (c *Config) fillDefaults() {
var firewallGarageInbound []ConfigFirewallRule
for i := range c.Storage.Allocations {
if c.Storage.Allocations[i].RPCPort == 0 {
c.Storage.Allocations[i].RPCPort = 3900 + (i * 10)
}
if c.Storage.Allocations[i].S3APIPort == 0 {
c.Storage.Allocations[i].S3APIPort = 3901 + (i * 10)
}
if c.Storage.Allocations[i].AdminPort == 0 {
c.Storage.Allocations[i].AdminPort = 3902 + (i * 10)
}
alloc := c.Storage.Allocations[i]
firewallGarageInbound = append(
firewallGarageInbound,
ConfigFirewallRule{
Port: strconv.Itoa(alloc.S3APIPort),
Proto: "tcp",
Host: "any",
},
ConfigFirewallRule{
Port: strconv.Itoa(alloc.RPCPort),
Proto: "tcp",
Host: "any",
},
)
}
c.VPN.Firewall.Inbound = append(
c.VPN.Firewall.Inbound,
firewallGarageInbound...,
)
}

View File

@ -0,0 +1,85 @@
// Package daemon contains types and functions related specifically to the
// cryptic-net daemon.
package daemon
import (
"cryptic-net/yamlutil"
"fmt"
"io"
"os"
"path/filepath"
"github.com/imdario/mergo"
"gopkg.in/yaml.v3"
)
func defaultConfigPath(appDirPath string) string {
return filepath.Join(appDirPath, "etc", "daemon.yml")
}
// CopyDefaultConfig copies the daemon config file embedded in the AppDir into
// the given io.Writer.
func CopyDefaultConfig(into io.Writer, appDirPath string) error {
defaultConfigPath := defaultConfigPath(appDirPath)
f, err := os.Open(defaultConfigPath)
if err != nil {
return fmt.Errorf("opening daemon config at %q: %w", defaultConfigPath, err)
}
defer f.Close()
if _, err := io.Copy(into, f); err != nil {
return fmt.Errorf("copying daemon config from %q: %w", defaultConfigPath, err)
}
return nil
}
// LoadConfig loads the daemon config from userConfigPath, merges it with
// the default found in the appDirPath, and returns the result.
//
// If userConfigPath is not given then the default is loaded and returned.
func LoadConfig(
appDirPath, userConfigPath string,
) (
Config, error,
) {
defaultConfigPath := defaultConfigPath(appDirPath)
var fullDaemon map[string]interface{}
if err := yamlutil.LoadYamlFile(&fullDaemon, defaultConfigPath); err != nil {
return Config{}, fmt.Errorf("parsing default daemon config file: %w", err)
}
if userConfigPath != "" {
var daemonConfig map[string]interface{}
if err := yamlutil.LoadYamlFile(&daemonConfig, userConfigPath); err != nil {
return Config{}, fmt.Errorf("parsing %q: %w", userConfigPath, err)
}
err := mergo.Merge(&fullDaemon, daemonConfig, mergo.WithOverride)
if err != nil {
return Config{}, fmt.Errorf("merging contents of file %q: %w", userConfigPath, err)
}
}
fullDaemonB, err := yaml.Marshal(fullDaemon)
if err != nil {
return Config{}, fmt.Errorf("yaml marshaling: %w", err)
}
var config Config
if err := yaml.Unmarshal(fullDaemonB, &config); err != nil {
return Config{}, fmt.Errorf("yaml unmarshaling back into Config struct: %w", err)
}
config.fillDefaults()
return config, nil
}

View File

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

View File

@ -0,0 +1,58 @@
package dnsmasq
import (
"cryptic-net/bootstrap"
"fmt"
"os"
"text/template"
)
// ConfData describes all the data needed to populate a dnsmasq.conf file.
type ConfData struct {
Resolvers []string
Domain string
IP string
Hosts []bootstrap.Host
}
var confTpl = template.Must(template.New("").Parse(`
port=53
bind-interfaces
listen-address={{ .IP }}
no-resolv
no-hosts
user=
group=
{{- range $host := .Hosts }}
address=/{{ $host.Name }}.hosts.{{ .Domain }}/{{ $host.Nebula.IP }}
{{ end -}}
{{- range .Resolvers }}
server={{ . }}
{{ end -}}
`))
// WriteConfFile renders a dnsmasq.conf using the given data to a new
// file at the given path.
func WriteConfFile(path string, data ConfData) error {
file, err := os.OpenFile(
path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640,
)
if err != nil {
return fmt.Errorf("creating file: %w", err)
}
defer file.Close()
if err := confTpl.Execute(file, data); err != nil {
return fmt.Errorf("rendering template to file: %w", err)
}
return nil
}

View File

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

View File

@ -1,241 +0,0 @@
package crypticnet
import (
"context"
"cryptic-net/bootstrap"
"cryptic-net/garage"
"cryptic-net/yamlutil"
"errors"
"fmt"
"io/fs"
"net"
"os"
"os/signal"
"path/filepath"
"strconv"
"sync"
"syscall"
"github.com/adrg/xdg"
)
// Names of various environment variables which get set by the entrypoint.
const (
DaemonYmlPathEnvVar = "_DAEMON_YML_PATH"
BootstrapPathEnvVar = "_BOOTSTRAP_PATH"
RuntimeDirPathEnvVar = "_RUNTIME_DIR_PATH"
DataDirPathEnvVar = "_DATA_DIR_PATH"
)
// Env contains the values of environment variables, as well as other entities
// which are useful across all processes.
type Env struct {
Context context.Context
AppDirPath string
DaemonYmlPath string
RuntimeDirPath string
DataDirPath string
// If NewEnv is called with bootstrapOptional, and a bootstrap file is not
// found, then these fields will not be set.
BootstrapPath string
Bootstrap bootstrap.Bootstrap
thisDaemon DaemonYml
thisDaemonOnce sync.Once
}
func getAppDirPath() string {
appDirPath := os.Getenv("APPDIR")
if appDirPath == "" {
appDirPath = "."
}
return appDirPath
}
// NewEnv calculates an Env instance based on the APPDIR and XDG envvars.
//
// If bootstrapOptional is true then NewEnv will first check if a bootstrap file
// can be found in the expected places, and if not then it will not populate
// BootstrapFS or any other fields based on it.
func NewEnv(bootstrapOptional bool) (Env, error) {
runtimeDirPath := filepath.Join(xdg.RuntimeDir, "cryptic-net")
appDirPath := getAppDirPath()
env := Env{
AppDirPath: appDirPath,
DaemonYmlPath: filepath.Join(runtimeDirPath, "daemon.yml"),
RuntimeDirPath: runtimeDirPath,
DataDirPath: filepath.Join(xdg.DataHome, "cryptic-net"),
}
return env.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 Env{}, err
}
return 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 loads a Bootstrap from the given path, and returns a copy of
// the Env with that Bootstrap set along with the BootstrapPath (or an error).
func (e Env) LoadBootstrap(path string) (Env, error) {
var err error
if e.Bootstrap, err = bootstrap.FromFile(path); err != nil {
return Env{}, fmt.Errorf("parsing bootstrap.tgz at %q: %w", path, err)
}
e.BootstrapPath = path
return e, nil
}
func (e Env) initBootstrap(bootstrapOptional bool) (Env, 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 Env{}, 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 Env{}, fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
} else if !exists && !bootstrapOptional {
return Env{}, fmt.Errorf("boostrap file not found at %q", bootstrapPath)
} else if exists {
return e.LoadBootstrap(bootstrapPath)
}
}
return e, nil
}
func (e Env) init(bootstrapOptional bool) (Env, 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)
}()
return e.initBootstrap(bootstrapOptional)
}
// 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)
}
// GarageAdminClient will return an AdminClient for a local garage instance, or
// it will _panic_ if there is no local instance configured.
func (e Env) GarageAdminClient() *garage.AdminClient {
thisHost := e.Bootstrap.ThisHost()
thisDaemon := e.ThisDaemon()
return garage.NewAdminClient(
net.JoinHostPort(
thisHost.Nebula.IP,
strconv.Itoa(thisDaemon.Storage.Allocations[0].AdminPort),
),
e.Bootstrap.GarageAdminToken,
)
}

View File

@ -9,6 +9,17 @@ import (
"net/http" "net/http"
) )
// AdminClientError gets returned from AdminClient's Do method for non-200
// errors.
type AdminClientError struct {
StatusCode int
Body []byte
}
func (e AdminClientError) Error() string {
return fmt.Sprintf("%d response from admin: %q", e.StatusCode, e.Body)
}
// AdminClient is a helper type for performing actions against the garage admin // AdminClient is a helper type for performing actions against the garage admin
// interface. // interface.
type AdminClient struct { type AdminClient struct {
@ -64,7 +75,11 @@ func (c *AdminClient) Do(
defer res.Body.Close() defer res.Body.Close()
if res.StatusCode != 200 { if res.StatusCode != 200 {
return fmt.Errorf("unexpected %s response returned", res.Status) body, _ := io.ReadAll(res.Body)
return AdminClientError{
StatusCode: res.StatusCode,
Body: body,
}
} }
if rcv == nil { if rcv == nil {

View File

@ -67,9 +67,7 @@ func WriteGarageTomlFile(path string, data GarageTomlData) error {
defer file.Close() defer file.Close()
err = RenderGarageToml(file, data) if err := garageTomlTpl.Execute(file, data); err != nil {
if err != nil {
return fmt.Errorf("rendering template to file: %w", err) return fmt.Errorf("rendering template to file: %w", err)
} }

View File

@ -3,7 +3,7 @@ module cryptic-net
go 1.17 go 1.17
require ( require (
code.betamike.com/cryptic-io/pmux v0.0.0-20221020185531-7a7868003822 code.betamike.com/cryptic-io/pmux v0.0.0-20221025185405-29241f144a2d
github.com/adrg/xdg v0.4.0 github.com/adrg/xdg v0.4.0
github.com/imdario/mergo v0.3.12 github.com/imdario/mergo v0.3.12
github.com/minio/minio-go/v7 v7.0.28 github.com/minio/minio-go/v7 v7.0.28

View File

@ -1,5 +1,7 @@
code.betamike.com/cryptic-io/pmux v0.0.0-20221020185531-7a7868003822 h1:c7Eu2h8gXOpOfhC1LvSYLNfiSsWTyvdI1XVpUuqMFHE= code.betamike.com/cryptic-io/pmux v0.0.0-20221020185531-7a7868003822 h1:c7Eu2h8gXOpOfhC1LvSYLNfiSsWTyvdI1XVpUuqMFHE=
code.betamike.com/cryptic-io/pmux v0.0.0-20221020185531-7a7868003822/go.mod h1:cBuEN/rkaM/GH24uQroX/++qDmte+mLudDUqMt6XJWs= code.betamike.com/cryptic-io/pmux v0.0.0-20221020185531-7a7868003822/go.mod h1:cBuEN/rkaM/GH24uQroX/++qDmte+mLudDUqMt6XJWs=
code.betamike.com/cryptic-io/pmux v0.0.0-20221025185405-29241f144a2d h1:s6nDTg23o9ujZZnl8ohZBDoG4SqPUyFfvod9DQjwmNU=
code.betamike.com/cryptic-io/pmux v0.0.0-20221025185405-29241f144a2d/go.mod h1:cBuEN/rkaM/GH24uQroX/++qDmte+mLudDUqMt6XJWs=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

25
nix/dnsmasq.nix Normal file
View File

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

View File

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

View File

@ -2,7 +2,8 @@ rec {
overlays = [ overlays = [
# Make both buildGoModules use static compilation by default. # Make buildGoModules use static compilation by default, and use go 1.18
# everywhere.
(final: prev: (final: prev:
let let
@ -17,19 +18,11 @@ rec {
in { in {
go = prev.go_1_18; go = prev.go_1_18;
buildGoModule = args: prev.buildGoModule (buildArgs // args); buildGoModule = args: prev.buildGo118Module (buildArgs // args);
buildGo118Module = args: prev.buildGo118Module (buildArgs // args); buildGo118Module = args: prev.buildGo118Module (buildArgs // args);
} }
) )
(final: prev: { rebase = prev.callPackage ./rebase.nix {}; })
(final: prev: { yq-go = prev.callPackage ./yq-go.nix {}; })
(final: prev: { nebula = prev.callPackage ./nebula.nix {
buildGoModule = prev.buildGo118Module;
}; })
]; ];
version = "22-05"; version = "22-05";

View File

@ -1,18 +0,0 @@
# rebase is a helper which takes all files/dirs under oldroot, and
# creates a new derivation with those files/dirs copied under newroot
# (where newroot is a relative path to the root of the derivation).
{
stdenv,
}: name: oldroot: newroot: stdenv.mkDerivation {
inherit name oldroot newroot;
builder = builtins.toFile "builder.sh" ''
source $stdenv/setup
mkdir -p "$out"/"$newroot"
cp -rL "$oldroot"/* "$out"/"$newroot"
'';
}

View File

@ -1,19 +0,0 @@
{
buildGoModule,
fetchFromGitHub,
}: buildGoModule rec {
pname = "yq-go";
version = "4.21.1";
src = fetchFromGitHub {
owner = "mikefarah";
repo = "yq";
rev = "v${version}";
sha256 = "sha256-283xe7FVHYSsRl4cZD7WDzIW1gqNAFsNrWYJkthZheU=";
};
vendorSha256 = "sha256-F11FnDYJ59aKrdRXDPpKlhX52yQXdaN1sblSkVI2j9w=";
}