Compare commits

..

3 Commits

Author SHA1 Message Date
Brian Picciano
836e69735d Some large inter-related refactors, moving towards network creation command
Host types have been moved within the `bootstrap` package.

Refactored how boostrap FS is interacted with. There is now a
`Bootstrap` struct which has pre-loaded all data within the bootstrap
FS. This helps centralize the logic around reading the data (though not
yet completely).

Choosing of the garage node to interact with no longer occurs in like
three different places. It occurs at the environment level now, and is
aided by the new `garage.Peer` type.
2022-10-15 16:28:03 +02:00
Brian Picciano
004be0c2aa Implement creation of CACert 2022-10-15 13:17:48 +02:00
Brian Picciano
0e41a06121 Refactor how bootstrap files are created
The new code makes it a lot clearer what the sources of each
file/directory is, and makes it more difficult to forget to add a file
or directory. This will be helpful when it comes to bootstrapping an
entire network, which will require yet a third way of generating
bootstrap files.
2022-10-11 22:01:19 +02:00
28 changed files with 815 additions and 671 deletions

View File

@ -2,33 +2,129 @@
package bootstrap package bootstrap
import ( import (
"cryptic-net/garage"
"cryptic-net/tarutil" "cryptic-net/tarutil"
"cryptic-net/yamlutil"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"os"
"strings"
) )
// GetHashFromFS returns the hash of the contents of the given bootstrap file. // Paths within the bootstrap FS which for general data.
// It may return nil if the bootstrap file doesn't have a hash. const (
func GetHashFromFS(bootstrapFS fs.FS) ([]byte, error) { HostNamePath = "hostname"
)
b, err := fs.ReadFile(bootstrapFS, tarutil.HashBinPath) // Bootstrap is used for accessing all information contained within a
// bootstrap.tgz file.
//
// An instance of Bootstrap is read-only, the creator sub-package should be used
// to create new instances.
type Bootstrap struct {
Hosts map[string]Host
HostName string
if err != nil { NebulaCertsCACert string
return nil, fmt.Errorf("reading file %q from bootstrap fs: %w", tarutil.HashBinPath, err) NebulaCertsHostCert string
NebulaCertsHostKey string
GarageRPCSecret string
GarageGlobalBucketS3APICredentials garage.S3APICredentials
// Hash is a determinstic hash of the contents of the bootstrap file. This
// will be populated when parsing a Bootstrap from a bootstrap.tgz, but will
// be ignored when creating a new bootstrap.tgz.
Hash []byte
// DEPRECATED do not use
FS fs.FS
}
// FromFS loads a Boostrap instance from the given fs.FS, which presumably
// represents the file structure of a bootstrap.tgz file.
func FromFS(bootstrapFS fs.FS) (Bootstrap, error) {
var (
b Bootstrap
err error
)
b.FS = bootstrapFS
if b.Hosts, err = loadHosts(bootstrapFS); err != nil {
return Bootstrap{}, fmt.Errorf("loading hosts info from fs: %w", err)
}
if err = yamlutil.LoadYamlFSFile(
&b.GarageGlobalBucketS3APICredentials,
bootstrapFS,
GarageGlobalBucketKeyYmlPath,
); err != nil {
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", b.GarageGlobalBucketS3APICredentials, err)
}
filesToLoadAsString := []struct {
into *string
path string
}{
{&b.HostName, HostNamePath},
{&b.NebulaCertsCACert, NebulaCertsCACertPath},
{&b.NebulaCertsHostCert, NebulaCertsHostCertPath},
{&b.NebulaCertsHostKey, NebulaCertsHostKeyPath},
{&b.GarageRPCSecret, GarageRPCSecretPath},
}
for _, f := range filesToLoadAsString {
body, err := fs.ReadFile(bootstrapFS, f.path)
if err != nil {
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
}
*f.into = string(body)
}
// TODO confirm if this is necessary
b.GarageRPCSecret = strings.TrimSpace(b.GarageRPCSecret)
if b.Hash, err = fs.ReadFile(bootstrapFS, tarutil.HashBinPath); err != nil {
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", tarutil.HashBinPath, err)
} }
return b, nil return b, nil
} }
// GetHashFromReader reads the given tgz file as an fs.FS, and passes that to // FromReader reads a bootstrap.tgz file from the given io.Reader.
// GetHashFromFS. func FromReader(r io.Reader) (Bootstrap, error) {
func GetHashFromReader(r io.Reader) ([]byte, error) {
bootstrapFS, err := tarutil.FSFromReader(r) fs, err := tarutil.FSFromReader(r)
if err != nil { if err != nil {
return nil, fmt.Errorf("reading tar fs from reader: %w", err) return Bootstrap{}, fmt.Errorf("reading bootstrap.tgz: %w", err)
} }
return GetHashFromFS(bootstrapFS) return FromFS(fs)
}
// FromFile reads a bootstrap.tgz from a file at the given path.
func FromFile(path string) (Bootstrap, error) {
f, err := os.Open(path)
if err != nil {
return Bootstrap{}, fmt.Errorf("opening file: %w", err)
}
defer f.Close()
return FromReader(f)
}
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
// HostName isn't found in the Hosts map.
func (b Bootstrap) ThisHost() Host {
host, ok := b.Hosts[b.HostName]
if !ok {
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.HostName))
}
return host
} }

View File

@ -0,0 +1,110 @@
// Package creator is responsible for creating bootstrap files. It exists
// separately from the main bootstrap package in order to prevent import loops
// due to its use of crypticnet.Env.
package creator
import (
"context"
crypticnet "cryptic-net"
"cryptic-net/bootstrap"
"cryptic-net/nebula"
"cryptic-net/tarutil"
"fmt"
"io"
"io/fs"
)
func newBootstrap(
ctx context.Context,
into io.Writer,
hostname provider,
nebulaCertsCACert provider,
nebulaCertsHostCert provider,
nebulaCertsHostKey provider,
nebulaHosts provider,
garageRPCSecret provider,
garageGlobalBucketKey provider,
garageHosts provider,
) error {
pairs := []struct {
path string
provider provider
}{
{bootstrap.HostNamePath, hostname},
{bootstrap.NebulaCertsCACertPath, nebulaCertsCACert},
{bootstrap.NebulaCertsHostCertPath, nebulaCertsHostCert},
{bootstrap.NebulaCertsHostKeyPath, nebulaCertsHostKey},
{bootstrap.NebulaHostsDirPath, nebulaHosts},
{bootstrap.GarageRPCSecretPath, garageRPCSecret},
{bootstrap.GarageGlobalBucketKeyYmlPath, garageGlobalBucketKey},
{bootstrap.GarageHostsDirPath, garageHosts},
}
w := tarutil.NewTGZWriter(into)
for _, pair := range pairs {
if err := pair.provider(ctx, w, pair.path); err != nil {
return fmt.Errorf("populating %q in new bootstrap: %w", pair.path, err)
}
}
return w.Close()
}
// 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 := env.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
return newBootstrap(
env.Context,
into,
provideFromFS(env.Bootstrap.FS), // hostname
provideFromFS(env.Bootstrap.FS), // nebulaCertsCACert
provideFromFS(env.Bootstrap.FS), // nebulaCertsHostCert
provideFromFS(env.Bootstrap.FS), // nebulaCertsHostKey
provideDirFromGarage(client), // nebulaHosts
provideFromFS(env.Bootstrap.FS), // garageRPCSecret
provideFromFS(env.Bootstrap.FS), // garageGlobalBucketKey
provideDirFromGarage(client), // garageHosts
)
}
// 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.Bootstrap.Hosts[name]
if !ok {
return fmt.Errorf("unknown host %q, make sure host entry has been created", name)
}
client, err := env.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
nebulaHostCert, err := nebula.NewHostCert(adminFS, host.Nebula)
if err != nil {
return fmt.Errorf("creating new nebula host key/cert: %w", err)
}
return newBootstrap(
env.Context,
into,
provideFromBytes([]byte(name)), // hostname
provideFromBytes(nebulaHostCert.CACert), // nebulaCertsCACert
provideFromBytes(nebulaHostCert.HostCert), // nebulaCertsHostCert
provideFromBytes(nebulaHostCert.HostKey), // nebulaCertsHostKey
provideDirFromGarage(client), // nebulaHosts
provideFromFS(adminFS), // garageRPCSecret
provideFromFS(adminFS), // garageGlobalBucketKey
provideDirFromGarage(client), // garageHosts
)
}

View File

@ -0,0 +1,121 @@
package creator
import (
"context"
"cryptic-net/garage"
"cryptic-net/tarutil"
"fmt"
"io/fs"
"github.com/minio/minio-go/v7"
)
// provider is a function which will populate the given filePath into the given
// TGZWriter. The path may be a file or a directory.
type provider func(context.Context, *tarutil.TGZWriter, string) error
func provideFromBytes(body []byte) provider {
return func(
ctx context.Context,
w *tarutil.TGZWriter,
filePath string,
) error {
w.WriteFileBytes(filePath, body)
return nil
}
}
func provideFromFS(srcFS fs.FS) provider {
return func(
ctx context.Context,
w *tarutil.TGZWriter,
filePath string,
) error {
return w.CopyFileFromFS(filePath, srcFS)
}
}
func provideDirFromFS(srcFS fs.FS) provider {
return func(
ctx context.Context,
w *tarutil.TGZWriter,
dirPath string,
) error {
return fs.WalkDir(
srcFS, dirPath,
func(filePath string, dirEntry fs.DirEntry, err error) error {
if err != nil {
return err
} else if dirEntry.IsDir() {
return nil
} else if err := w.CopyFileFromFS(filePath, srcFS); err != nil {
return fmt.Errorf("copying file %q: %w", filePath, err)
}
return nil
},
)
}
}
// TODO it'd be great if we could wrap a minio.Client into an fs.FS. That would
// get rid of a weird dependency in this package, and clean up this code a ton.
func provideDirFromGarage(client *minio.Client) provider {
return func(
ctx context.Context,
w *tarutil.TGZWriter,
dirPath string,
) error {
objInfoCh := client.ListObjects(
ctx, garage.GlobalBucket,
minio.ListObjectsOptions{
Prefix: dirPath,
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
}
}

View File

@ -0,0 +1,48 @@
package bootstrap
import (
"cryptic-net/garage"
)
// Paths within the bootstrap FS related to garage.
const (
GarageGlobalBucketKeyYmlPath = "garage/global-bucket-key.yml"
GarageRPCSecretPath = "garage/rpc-secret.txt"
GarageHostsDirPath = "garage/hosts"
)
// GaragePeers returns a Peer for each known garage instance in the network.
func (b Bootstrap) GaragePeers() []garage.Peer {
var peers []garage.Peer
for _, host := range b.Hosts {
if host.Garage == nil {
continue
}
for _, instance := range host.Garage.Instances {
peer := garage.Peer{
IP: host.Nebula.IP,
RPCPort: instance.RPCPort,
S3APIPort: instance.S3APIPort,
}
peers = append(peers, peer)
}
}
return peers
}
// GarageRPCPeerAddrs returns the full RPC peer address for each known garage
// instance in the network.
func (b Bootstrap) GarageRPCPeerAddrs() []string {
var addrs []string
for _, peer := range b.GaragePeers() {
addrs = append(addrs, peer.RPCPeerAddr())
}
return addrs
}

View File

@ -1,4 +1,4 @@
package crypticnet package bootstrap
import ( import (
"errors" "errors"
@ -19,9 +19,9 @@ type NebulaHost struct {
// GarageHostInstance describes a single garage instance running on a host. // GarageHostInstance describes a single garage instance running on a host.
type GarageHostInstance struct { type GarageHostInstance struct {
APIPort int `yaml:"api_port"` RPCPort int `yaml:"rpc_port"`
RPCPort int `yaml:"rpc_port"` S3APIPort int `yaml:"s3_api_port"`
WebPort int `yaml:"web_port"` WebPort int `yaml:"web_port"`
} }
// GarageHost describes the contents of a `./garage/hosts/<hostname>.yml` file. // GarageHost describes the contents of a `./garage/hosts/<hostname>.yml` file.
@ -37,8 +37,7 @@ type Host struct {
Garage *GarageHost Garage *GarageHost
} }
// LostHosts returns a mapping of hostnames to Host objects for each host. func loadHosts(bootstrapFS fs.FS) (map[string]Host, error) {
func LoadHosts(bootstrapFS fs.FS) (map[string]Host, error) {
hosts := map[string]Host{} hosts := map[string]Host{}
@ -52,9 +51,11 @@ func LoadHosts(bootstrapFS fs.FS) (map[string]Host, error) {
} }
{ {
nebulaHostFiles, err := fs.Glob(bootstrapFS, "nebula/hosts/*.yml") globPath := filepath.Join(NebulaHostsDirPath, "*.yml")
nebulaHostFiles, err := fs.Glob(bootstrapFS, globPath)
if err != nil { if err != nil {
return nil, fmt.Errorf("listing nebula host files: %w", err) return nil, fmt.Errorf("listing nebula host files at %q in fs: %w", globPath, err)
} }
for _, nebulaHostPath := range nebulaHostFiles { for _, nebulaHostPath := range nebulaHostFiles {
@ -76,7 +77,7 @@ func LoadHosts(bootstrapFS fs.FS) (map[string]Host, error) {
for hostName, host := range hosts { for hostName, host := range hosts {
garageHostPath := filepath.Join("garage/hosts", hostName+".yml") garageHostPath := filepath.Join(GarageHostsDirPath, hostName+".yml")
var garageHost GarageHost var garageHost GarageHost
if err := readAsYaml(&garageHost, garageHostPath); errors.Is(err, fs.ErrNotExist) { if err := readAsYaml(&garageHost, garageHostPath); errors.Is(err, fs.ErrNotExist) {

View File

@ -0,0 +1,10 @@
package bootstrap
// Paths within the bootstrap FS related to nebula.
const (
NebulaHostsDirPath = "nebula/hosts"
NebulaCertsCACertPath = "nebula/certs/ca.crt"
NebulaCertsHostCertPath = "nebula/certs/host.crt"
NebulaCertsHostKeyPath = "nebula/certs/host.key"
)

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import (
crypticnet "cryptic-net" crypticnet "cryptic-net"
"cryptic-net/bootstrap" "cryptic-net/bootstrap"
bootstrap_creator "cryptic-net/bootstrap/creator"
"cryptic-net/yamlutil" "cryptic-net/yamlutil"
"github.com/cryptic-io/pmux/pmuxlib" "github.com/cryptic-io/pmux/pmuxlib"
@ -111,21 +112,16 @@ func reloadBootstrap(env *crypticnet.Env) (bool, error) {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
if err := bootstrap.NewForThisHost(env, buf); err != nil { if err := bootstrap_creator.NewForThisHost(env, buf); err != nil {
return false, fmt.Errorf("generating new bootstrap from env: %w", err) return false, fmt.Errorf("generating new bootstrap from env: %w", err)
} }
newHash, err := bootstrap.GetHashFromReader(bytes.NewReader(buf.Bytes())) newBootstrap, err := bootstrap.FromReader(bytes.NewReader(buf.Bytes()))
if err != nil { if err != nil {
return false, fmt.Errorf("reading hash from new bootstrap file: %w", err) return false, fmt.Errorf("parsing bootstrap which was just created: %w", err)
} }
currHash, err := bootstrap.GetHashFromFS(env.BootstrapFS) if bytes.Equal(newBootstrap.Hash, env.Bootstrap.Hash) {
if err != nil {
return false, fmt.Errorf("reading hash from existing bootstrap fs: %w", err)
}
if bytes.Equal(newHash, currHash) {
return false, nil return false, nil
} }
@ -136,12 +132,12 @@ func reloadBootstrap(env *crypticnet.Env) (bool, error) {
return true, nil return true, nil
} }
// runs a single pmux process for daemon, returning only once the env.Context // runs a single pmux process ofor daemon, returning only once the env.Context
// has been canceled or bootstrap info has been changed. This will always block // has been canceled or bootstrap info has been changed. This will always block
// until the spawned pmux has returned. // until the spawned pmux has returned.
func runDaemonPmuxOnce(env *crypticnet.Env) error { func runDaemonPmuxOnce(env *crypticnet.Env) error {
thisHost := env.ThisHost() thisHost := env.Bootstrap.ThisHost()
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)
pmuxProcConfigs := []pmuxlib.ProcessConfig{ pmuxProcConfigs := []pmuxlib.ProcessConfig{
@ -157,7 +153,7 @@ func runDaemonPmuxOnce(env *crypticnet.Env) error {
Cmd: "bash", Cmd: "bash",
Args: []string{ Args: []string{
"wait-for-ip", "wait-for-ip",
env.ThisHost().Nebula.IP, thisHost.Nebula.IP,
"bash", "bash",
"dnsmasq-entrypoint", "dnsmasq-entrypoint",
}, },
@ -170,7 +166,7 @@ func runDaemonPmuxOnce(env *crypticnet.Env) error {
Cmd: "bash", Cmd: "bash",
Args: []string{ Args: []string{
"wait-for-ip", "wait-for-ip",
env.ThisHost().Nebula.IP, thisHost.Nebula.IP,
"cryptic-net-main", "garage-entrypoint", "cryptic-net-main", "garage-entrypoint",
}, },

View File

@ -2,31 +2,10 @@ package entrypoint
import ( import (
"fmt" "fmt"
"io/fs"
"log"
"os" "os"
"strings"
"syscall" "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{ var subCmdGarageMC = subCmd{
name: "mc", name: "mc",
descr: "Runs the mc (minio-client) binary. The cryptic-net garage can be accessed under the `garage` alias", descr: "Runs the mc (minio-client) binary. The cryptic-net garage can be accessed under the `garage` alias",
@ -51,21 +30,16 @@ var subCmdGarageMC = subCmd{
env := subCmdCtx.env env := subCmdCtx.env
apiAddr := garage.APIAddr(env) s3APIAddr := env.ChooseGaragePeer().S3APIAddr()
if *keyID == "" || *keySecret == "" { if *keyID == "" || *keySecret == "" {
globalBucketCreds, err := garage.GlobalBucketAPICredentials(env)
if err != nil {
return fmt.Errorf("loading global bucket credentials: %w", err)
}
if *keyID == "" { if *keyID == "" {
*keyID = globalBucketCreds.ID *keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.ID
} }
if *keySecret == "" { if *keySecret == "" {
*keySecret = globalBucketCreds.Secret *keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.Secret
} }
} }
@ -83,7 +57,7 @@ var subCmdGarageMC = subCmd{
os.Environ(), os.Environ(),
fmt.Sprintf( fmt.Sprintf(
"MC_HOST_garage=http://%s:%s@%s", "MC_HOST_garage=http://%s:%s@%s",
*keyID, *keySecret, apiAddr, *keyID, *keySecret, s3APIAddr,
), ),
// The garage docs say this is necessary, though nothing bad // The garage docs say this is necessary, though nothing bad
@ -111,27 +85,13 @@ var subCmdGarageCLI = subCmd{
env := subCmdCtx.env 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 ( var (
binPath = env.BinPath("garage") binPath = env.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="+peerAddr, "GARAGE_RPC_HOST="+env.ChooseGaragePeer().RPCAddr(),
"GARAGE_RPC_SECRET="+rpcSecret, "GARAGE_RPC_SECRET="+env.Bootstrap.GarageRPCSecret,
) )
) )

View File

@ -2,8 +2,8 @@ package entrypoint
import ( import (
"bytes" "bytes"
crypticnet "cryptic-net"
"cryptic-net/bootstrap" "cryptic-net/bootstrap"
bootstrap_creator "cryptic-net/bootstrap/creator"
"cryptic-net/garage" "cryptic-net/garage"
"cryptic-net/tarutil" "cryptic-net/tarutil"
"errors" "errors"
@ -73,12 +73,12 @@ var subCmdHostsAdd = subCmd{
env := subCmdCtx.env env := subCmdCtx.env
client, err := garage.GlobalBucketAPIClient(env) client, err := env.GlobalBucketS3APIClient()
if err != nil { if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err) return fmt.Errorf("creating client for global bucket: %w", err)
} }
nebulaHost := crypticnet.NebulaHost{ nebulaHost := bootstrap.NebulaHost{
Name: *name, Name: *name,
IP: *ip, IP: *ip,
} }
@ -113,7 +113,7 @@ var subCmdHostsList = subCmd{
env := subCmdCtx.env env := subCmdCtx.env
client, err := garage.GlobalBucketAPIClient(env) client, err := env.GlobalBucketS3APIClient()
if err != nil { if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err) return fmt.Errorf("creating client for global bucket: %w", err)
} }
@ -147,7 +147,7 @@ var subCmdHostsList = subCmd{
return fmt.Errorf("retrieving object %q from global bucket: %w", objInfo.Key, err) return fmt.Errorf("retrieving object %q from global bucket: %w", objInfo.Key, err)
} }
var nebulaHost crypticnet.NebulaHost var nebulaHost bootstrap.NebulaHost
err = yaml.NewDecoder(obj).Decode(&nebulaHost) err = yaml.NewDecoder(obj).Decode(&nebulaHost)
obj.Close() obj.Close()
@ -191,7 +191,7 @@ var subCmdHostsDelete = subCmd{
filePath := nebulaHostPath(*name) filePath := nebulaHostPath(*name)
client, err := garage.GlobalBucketAPIClient(env) client, err := env.GlobalBucketS3APIClient()
if err != nil { if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err) return fmt.Errorf("creating client for global bucket: %w", err)
} }
@ -263,7 +263,7 @@ var subCmdHostsMakeBootstrap = subCmd{
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)
} }
return bootstrap.NewForHost(subCmdCtx.env, adminFS, *name, os.Stdout) return bootstrap_creator.NewForHost(subCmdCtx.env, adminFS, *name, os.Stdout)
}, },
} }

View File

@ -2,13 +2,11 @@ package garage_entrypoint
import ( import (
"fmt" "fmt"
"io/fs"
"log" "log"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings"
"time" "time"
crypticnet "cryptic-net" crypticnet "cryptic-net"
@ -19,24 +17,23 @@ import (
func writeChildConf( func writeChildConf(
env *crypticnet.Env, env *crypticnet.Env,
bootstrapPeers []string,
alloc crypticnet.DaemonYmlStorageAllocation, alloc crypticnet.DaemonYmlStorageAllocation,
rpcSecret string,
) (string, error) { ) (string, error) {
if err := os.MkdirAll(alloc.MetaPath, 0750); err != nil { if err := os.MkdirAll(alloc.MetaPath, 0750); err != nil {
return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err) return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err)
} }
pubKey, privKey, err := garage.GeneratePeerKey(env.ThisHost().Nebula.IP, alloc.RPCPort) thisHost := env.Bootstrap.ThisHost()
if err != nil { peer := garage.Peer{
return "", fmt.Errorf( IP: thisHost.Nebula.IP,
"generating node key with input %q,%d: %w", RPCPort: alloc.RPCPort,
env.ThisHost().Nebula.IP, alloc.RPCPort, err, S3APIPort: alloc.S3APIPort,
)
} }
pubKey, privKey := peer.RPCPeerKey()
nodeKeyPath := filepath.Join(alloc.MetaPath, "node_key") nodeKeyPath := filepath.Join(alloc.MetaPath, "node_key")
nodeKeyPubPath := filepath.Join(alloc.MetaPath, "node_keypub") nodeKeyPubPath := filepath.Join(alloc.MetaPath, "node_keypub")
@ -51,17 +48,17 @@ func writeChildConf(
env.RuntimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort), env.RuntimeDirPath, 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: rpcSecret, RPCSecret: env.Bootstrap.GarageRPCSecret,
RPCAddr: net.JoinHostPort(env.ThisHost().Nebula.IP, strconv.Itoa(alloc.RPCPort)), RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
APIAddr: net.JoinHostPort(env.ThisHost().Nebula.IP, strconv.Itoa(alloc.APIPort)), APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)),
WebAddr: net.JoinHostPort(env.ThisHost().Nebula.IP, strconv.Itoa(alloc.WebPort)), WebAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.WebPort)),
BootstrapPeers: bootstrapPeers, BootstrapPeers: env.Bootstrap.GarageRPCPeerAddrs(),
}) })
if err != nil { if err != nil {
@ -73,13 +70,15 @@ func writeChildConf(
func waitForArgs(env *crypticnet.Env, bin string, binArgs ...string) []string { func waitForArgs(env *crypticnet.Env, bin string, binArgs ...string) []string {
thisHost := env.Bootstrap.ThisHost()
var args []string var args []string
for _, alloc := range env.ThisDaemon().Storage.Allocations { for _, alloc := range env.ThisDaemon().Storage.Allocations {
args = append( args = append(
args, args,
"wait-for", "wait-for",
net.JoinHostPort(env.ThisHost().Nebula.IP, strconv.Itoa(alloc.RPCPort)), net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
"--", "--",
) )
} }
@ -98,25 +97,11 @@ func Main() {
log.Fatalf("reading envvars: %v", err) 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 var pmuxProcConfigs []pmuxlib.ProcessConfig
for _, alloc := range env.ThisDaemon().Storage.Allocations { for _, alloc := range env.ThisDaemon().Storage.Allocations {
childConfPath, err := writeChildConf(env, bootstrapPeers, alloc, rpcSecret) childConfPath, err := writeChildConf(env, alloc)
if err != nil { if err != nil {
log.Fatalf("writing child config file for alloc %+v: %v", alloc, err) log.Fatalf("writing child config file for alloc %+v: %v", alloc, err)

View File

@ -148,29 +148,30 @@ func readCurrNodes(r io.Reader) (clusterNodes, int, error) {
return currNodes, version, nil return currNodes, version, nil
} }
func readExpNodes(env *crypticnet.Env) (clusterNodes, error) { func readExpNodes(env *crypticnet.Env) clusterNodes {
thisHost := env.Bootstrap.ThisHost()
var expNodes clusterNodes var expNodes clusterNodes
for _, alloc := range env.ThisDaemon().Storage.Allocations { for _, alloc := range env.ThisDaemon().Storage.Allocations {
id, err := garage.GeneratePeerID(env.ThisHost().Nebula.IP, alloc.RPCPort) peer := garage.Peer{
IP: thisHost.Nebula.IP,
if err != nil { RPCPort: alloc.RPCPort,
return nil, fmt.Errorf( S3APIPort: alloc.S3APIPort,
"generating peer id for ip:%q port:%d: %w",
env.ThisHost().Nebula.IP, alloc.RPCPort, err,
)
} }
id := peer.RPCPeerID()
expNodes = append(expNodes, clusterNode{ expNodes = append(expNodes, clusterNode{
ID: id, ID: id,
Zone: env.ThisHost().Name, Zone: env.Bootstrap.HostName,
Capacity: alloc.Capacity / 100, Capacity: alloc.Capacity / 100,
}) })
} }
return expNodes, nil return expNodes
} }
// NOTE: The id formatting for currNodes and expNodes is different; expNodes has // NOTE: The id formatting for currNodes and expNodes is different; expNodes has
@ -232,18 +233,14 @@ func Main() {
for _, node := range currNodes { for _, node := range currNodes {
if env.ThisHost().Name != node.Zone { if env.Bootstrap.HostName != node.Zone {
continue continue
} }
thisCurrNodes = append(thisCurrNodes, node) thisCurrNodes = append(thisCurrNodes, node)
} }
expNodes, err := readExpNodes(env) expNodes := readExpNodes(env)
if err != nil {
panic(fmt.Errorf("reading expected layout from environment: %w", err))
}
lines := diff(thisCurrNodes, expNodes) lines := diff(thisCurrNodes, expNodes)

View File

@ -42,12 +42,13 @@ func Main() {
panic("The arguments -ip, -port, and -danger are required") panic("The arguments -ip, -port, and -danger are required")
} }
pubKey, privKey, err := garage.GeneratePeerKey(*ip, *port) peer := garage.Peer{
IP: *ip,
if err != nil { RPCPort: *port,
panic(fmt.Errorf("GeneratePeerKey returned: %w", err))
} }
pubKey, privKey := peer.RPCPeerKey()
fmt.Fprintln(os.Stdout, hex.EncodeToString(pubKey)) fmt.Fprintln(os.Stdout, hex.EncodeToString(pubKey))
if *outPub != "" { if *outPub != "" {

View File

@ -3,6 +3,7 @@ package garage_update_global_bucket
import ( import (
"bytes" "bytes"
crypticnet "cryptic-net" crypticnet "cryptic-net"
"cryptic-net/bootstrap"
"cryptic-net/garage" "cryptic-net/garage"
"fmt" "fmt"
"log" "log"
@ -16,12 +17,15 @@ func updateGlobalBucket(env *crypticnet.Env) error {
ctx := env.Context ctx := env.Context
client, err := garage.GlobalBucketAPIClient(env) client, err := env.GlobalBucketS3APIClient()
if err != nil { if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err) return fmt.Errorf("creating client for global bucket: %w", err)
} }
filePath := filepath.Join("garage/hosts", env.ThisHost().Name+".yml") filePath := filepath.Join(
"garage/hosts",
env.Bootstrap.HostName+".yml",
)
daemon := env.ThisDaemon() daemon := env.ThisDaemon()
@ -41,14 +45,14 @@ func updateGlobalBucket(env *crypticnet.Env) error {
return nil return nil
} }
var garageHost crypticnet.GarageHost var garageHost bootstrap.GarageHost
for _, alloc := range daemon.Storage.Allocations { for _, alloc := range daemon.Storage.Allocations {
garageHostInstance := crypticnet.GarageHostInstance{ garageHostInstance := bootstrap.GarageHostInstance{
APIPort: alloc.APIPort, RPCPort: alloc.RPCPort,
RPCPort: alloc.RPCPort, S3APIPort: alloc.S3APIPort,
WebPort: alloc.WebPort, WebPort: alloc.WebPort,
} }
garageHost.Instances = append(garageHost.Instances, garageHostInstance) garageHost.Instances = append(garageHost.Instances, garageHostInstance)

View File

@ -2,8 +2,6 @@ package nebula_entrypoint
import ( import (
"cryptic-net/yamlutil" "cryptic-net/yamlutil"
"fmt"
"io/fs"
"log" "log"
"net" "net"
"path/filepath" "path/filepath"
@ -27,7 +25,7 @@ func Main() {
staticHostMap = map[string][]string{} staticHostMap = map[string][]string{}
) )
for _, host := range env.Hosts { for _, host := range env.Bootstrap.Hosts {
if host.Nebula.PublicAddr == "" { if host.Nebula.PublicAddr == "" {
continue continue
@ -37,26 +35,11 @@ func Main() {
staticHostMap[host.Nebula.IP] = []string{host.Nebula.PublicAddr} 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{}{ config := map[string]interface{}{
"pki": map[string]string{ "pki": map[string]string{
"ca": readCertFile("ca.crt"), "ca": env.Bootstrap.NebulaCertsCACert,
"cert": readCertFile("host.crt"), "cert": env.Bootstrap.NebulaCertsHostCert,
"key": readCertFile("host.key"), "key": env.Bootstrap.NebulaCertsHostKey,
}, },
"static_host_map": staticHostMap, "static_host_map": staticHostMap,
"punchy": map[string]bool{ "punchy": map[string]bool{
@ -110,7 +93,7 @@ func Main() {
firewallInbound = append( firewallInbound = append(
firewallInbound, firewallInbound,
crypticnet.ConfigFirewallRule{ crypticnet.ConfigFirewallRule{
Port: strconv.Itoa(alloc.APIPort), Port: strconv.Itoa(alloc.S3APIPort),
Proto: "tcp", Proto: "tcp",
Host: "any", Host: "any",
}, },

View File

@ -16,14 +16,14 @@ func updateGlobalBucket(env *crypticnet.Env) error {
ctx := env.Context ctx := env.Context
client, err := garage.GlobalBucketAPIClient(env) client, err := env.GlobalBucketS3APIClient()
if err != nil { if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err) return fmt.Errorf("creating client for global bucket: %w", err)
} }
daemon := env.ThisDaemon() daemon := env.ThisDaemon()
host := env.ThisHost() host := env.Bootstrap.ThisHost()
host.Nebula.Name = host.Name host.Nebula.Name = host.Name
host.Nebula.PublicAddr = daemon.VPN.PublicAddr host.Nebula.PublicAddr = daemon.VPN.PublicAddr

View File

@ -28,12 +28,12 @@ type ConfigFirewallRule struct {
// DaemonYmlStorageAllocation describes the structure of each storage allocation // DaemonYmlStorageAllocation describes the structure of each storage allocation
// within the daemon.yml file. // within the daemon.yml file.
type DaemonYmlStorageAllocation struct { type DaemonYmlStorageAllocation 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"`
APIPort int `yaml:"api_port"` S3APIPort int `yaml:"api_port"` // TODO fix field name here
RPCPort int `yaml:"rpc_port"` RPCPort int `yaml:"rpc_port"`
WebPort int `yaml:"web_port"` WebPort int `yaml:"web_port"`
} }
// DaemonYml describes the structure of the daemon.yml file. // DaemonYml describes the structure of the daemon.yml file.

View File

@ -2,7 +2,7 @@ package crypticnet
import ( import (
"context" "context"
"cryptic-net/tarutil" "cryptic-net/bootstrap"
"cryptic-net/yamlutil" "cryptic-net/yamlutil"
"errors" "errors"
"fmt" "fmt"
@ -37,9 +37,7 @@ type Env struct {
// If NewEnv is called with bootstrapOptional, and a bootstrap file is not // If NewEnv is called with bootstrapOptional, and a bootstrap file is not
// found, then these fields will not be set. // found, then these fields will not be set.
BootstrapPath string BootstrapPath string
BootstrapFS fs.FS Bootstrap bootstrap.Bootstrap
Hosts map[string]Host
HostName string
thisDaemon DaemonYml thisDaemon DaemonYml
thisDaemonOnce sync.Once thisDaemonOnce sync.Once
@ -118,32 +116,13 @@ func (e *Env) DataDirBootstrapPath() string {
// and all derived fields based on that. // and all derived fields based on that.
func (e *Env) LoadBootstrap(path string) error { func (e *Env) LoadBootstrap(path string) error {
var ( var err error
err error
// load all values into temp variables before setting the fields on Env, if e.Bootstrap, err = bootstrap.FromFile(path); err != nil {
// so we don't leave it in an inconsistent state. return fmt.Errorf("parsing bootstrap.tgz at %q: %w", path, err)
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.BootstrapPath = path
e.BootstrapFS = bootstrapFS
e.Hosts = hosts
e.HostName = string(hostNameB)
return nil return nil
} }
@ -219,11 +198,6 @@ func (e *Env) init(bootstrapOptional bool) error {
return nil 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 // 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. // a process's environment, then that process can read it back using ReadEnv.
func (e *Env) ToMap() map[string]string { func (e *Env) ToMap() map[string]string {

View File

@ -0,0 +1,42 @@
package crypticnet
import (
"cryptic-net/garage"
"fmt"
)
// ChooseGaragePeer returns a Peer for a garage instance from the network. It
// will prefer a garage instance on this particular host, if there is one, but
// will otherwise return a random endpoint.
func (env *Env) ChooseGaragePeer() garage.Peer {
if allocs := env.ThisDaemon().Storage.Allocations; len(allocs) > 0 {
return garage.Peer{
IP: env.Bootstrap.ThisHost().Nebula.IP,
RPCPort: allocs[0].RPCPort,
S3APIPort: allocs[0].S3APIPort,
}
}
for _, peer := range env.Bootstrap.GaragePeers() {
return peer
}
panic("no garage instances configured")
}
// GlobalBucketS3APIClient returns an S3 client pre-configured with access to
// the global bucket.
func (env *Env) GlobalBucketS3APIClient() (garage.S3APIClient, error) {
addr := env.ChooseGaragePeer().S3APIAddr()
creds := env.Bootstrap.GarageGlobalBucketS3APICredentials
client, err := garage.NewS3APIClient(addr, creds)
if err != nil {
return nil, fmt.Errorf("connecting to garage S3 API At %q: %w", addr, err)
}
return client, err
}

View File

@ -1,12 +1,7 @@
package garage package garage
import ( import (
crypticnet "cryptic-net"
"cryptic-net/yamlutil"
"errors" "errors"
"fmt"
"net"
"strconv"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
@ -19,74 +14,21 @@ func IsKeyNotFound(err error) bool {
return errors.As(err, &mErr) && mErr.Code == "NoSuchKey" return errors.As(err, &mErr) && mErr.Code == "NoSuchKey"
} }
// APICredentials describe data fields necessary for authenticating with a // S3APIClient is a client used to interact with garage's S3 API.
// garage api endpoint. type S3APIClient = *minio.Client
type APICredentials struct {
// S3APICredentials describe data fields necessary for authenticating with a
// garage S3 API endpoint.
type S3APICredentials struct {
ID string `yaml:"id"` ID string `yaml:"id"`
Secret string `yaml:"secret"` Secret string `yaml:"secret"`
} }
// GlobalBucketAPICredentials returns APICredentials for the global bucket. // NewS3APIClient returns a minio client configured to use the given garage S3 API
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. // endpoint.
func APIClient(addr string, creds APICredentials) (*minio.Client, error) { func NewS3APIClient(addr string, creds S3APICredentials) (S3APIClient, error) {
return minio.New(addr, &minio.Options{ return minio.New(addr, &minio.Options{
Creds: credentials.NewStaticV4(creds.ID, creds.Secret, ""), Creds: credentials.NewStaticV4(creds.ID, creds.Secret, ""),
Region: Region, 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)
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,172 @@
// Package nebula contains helper functions and types which are useful for
// setting up nebula configs, processes, and deployments.
package nebula
import (
"cryptic-net/bootstrap"
"crypto/ed25519"
"crypto/rand"
"fmt"
"io"
"io/fs"
"net"
"time"
"github.com/slackhq/nebula/cert"
"golang.org/x/crypto/curve25519"
)
// TODO this should one day not be hardcoded
var ipCIDRMask = func() net.IPMask {
_, ipNet, err := net.ParseCIDR("10.10.0.0/16")
if err != nil {
panic(err)
}
return ipNet.Mask
}()
// HostCert contains the certificate and private key files which will need to
// be present on a particular host. Each file is PEM encoded.
type HostCert struct {
CACert []byte
HostKey []byte
HostCert []byte
}
// CACert contains the certificate and private files which can be used to create
// HostCerts. Each file is PEM encoded.
type CACert struct {
CACert []byte
CAKey []byte
}
// NewHostCert generates a new key/cert for a nebula host using the CA key
// which will be found in the adminFS.
func NewHostCert(
adminFS fs.FS, host bootstrap.NebulaHost,
) (
HostCert, error,
) {
// The logic here is largely based on
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
caKeyPEM, err := fs.ReadFile(adminFS, "nebula/certs/ca.key")
if err != nil {
return HostCert{}, fmt.Errorf("reading ca.key from admin fs: %w", err)
}
caKey, _, err := cert.UnmarshalEd25519PrivateKey(caKeyPEM)
if err != nil {
return HostCert{}, fmt.Errorf("unmarshaling ca.key: %w", err)
}
caCrtPEM, err := fs.ReadFile(adminFS, "nebula/certs/ca.crt")
if err != nil {
return HostCert{}, fmt.Errorf("reading ca.crt from admin fs: %w", err)
}
caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM(caCrtPEM)
if err != nil {
return HostCert{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
}
issuer, err := caCrt.Sha256Sum()
if err != nil {
return HostCert{}, fmt.Errorf("getting ca.crt issuer: %w", err)
}
expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second)
ip := net.ParseIP(host.IP)
if ip == nil {
return HostCert{}, 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 HostCert{}, fmt.Errorf("reading random bytes to form private key: %w", err)
}
curve25519.ScalarBaseMult(&pubkey, &privkey)
hostPub, hostKey = pubkey[:], privkey[:]
}
hostCrt := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: 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 HostCert{}, fmt.Errorf("validating certificate constraints: %w", err)
}
if err := hostCrt.Sign(caKey); err != nil {
return HostCert{}, fmt.Errorf("signing host cert with ca.key: %w", err)
}
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
hostCrtPEM, err := hostCrt.MarshalToPEM()
if err != nil {
return HostCert{}, fmt.Errorf("marshalling host.crt: %w", err)
}
return HostCert{
CACert: caCrtPEM,
HostKey: hostKeyPEM,
HostCert: hostCrtPEM,
}, nil
}
// NewCACert generates a CACert. The domain should be the network's root domain,
// and is included in the signing certificate's Name field.
func NewCACert(domain string) (CACert, error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(fmt.Errorf("generating ed25519 key: %w", err))
}
now := time.Now()
expireAt := now.Add(2 * 365 * 24 * time.Hour)
caCrt := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: fmt.Sprintf("%s cryptic-net root cert", domain),
NotBefore: now,
NotAfter: expireAt,
PublicKey: pubKey,
IsCA: true,
},
}
if err := caCrt.Sign(privKey); err != nil {
return CACert{}, fmt.Errorf("signing caCrt: %w", err)
}
caKeyPEM := cert.MarshalEd25519PrivateKey(privKey)
caCrtPem, err := caCrt.MarshalToPEM()
if err != nil {
return CACert{}, fmt.Errorf("marshaling caCrt: %w", err)
}
return CACert{
CACert: caCrtPem,
CAKey: caKeyPEM,
}, nil
}

View File

@ -7,12 +7,11 @@ import (
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"os"
"github.com/nlepage/go-tarfs" "github.com/nlepage/go-tarfs"
) )
// FSFromTGZFile returns a FS instance which will read the contents of a tgz // FSFromReader returns a FS instance which will read the contents of a tgz
// file from the given Reader. // file from the given Reader.
func FSFromReader(r io.Reader) (fs.FS, error) { func FSFromReader(r io.Reader) (fs.FS, error) {
gf, err := gzip.NewReader(r) gf, err := gzip.NewReader(r)
@ -23,15 +22,3 @@ func FSFromReader(r io.Reader) (fs.FS, error) {
return tarfs.New(gf) 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)
}

View File

@ -7,6 +7,7 @@ import (
"crypto/sha512" "crypto/sha512"
"fmt" "fmt"
"io" "io"
"io/fs"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@ -149,3 +150,22 @@ func (w *TGZWriter) WriteFileBytes(path string, body []byte) {
bodyR := bytes.NewReader(body) bodyR := bytes.NewReader(body)
w.WriteFile(path, bodyR.Size(), bodyR) w.WriteFile(path, bodyR.Size(), bodyR)
} }
// CopyFileFromFS copies the file at the given path from srcFS into the same
// path in the TGZWriter.
func (w *TGZWriter) CopyFileFromFS(path string, srcFS fs.FS) error {
f, err := srcFS.Open(path)
if err != nil {
return fmt.Errorf("opening: %w", err)
}
defer f.Close()
fStat, err := f.Stat()
if err != nil {
return fmt.Errorf("stating: %w", err)
}
w.WriteFile(path, fStat.Size(), f)
return nil
}