Compare commits
2 Commits
b26f4bdd6a
...
7dceb659ef
Author | SHA1 | Date | |
---|---|---|---|
|
7dceb659ef | ||
|
711d568036 |
@ -35,7 +35,7 @@ type CreationParams struct {
|
||||
type Admin struct {
|
||||
CreationParams CreationParams
|
||||
|
||||
NebulaCACert nebula.CACert
|
||||
NebulaCACredentials nebula.CACredentials
|
||||
|
||||
GarageRPCSecret string
|
||||
GarageGlobalBucketS3APICredentials garage.S3APICredentials
|
||||
@ -67,8 +67,8 @@ func FromFS(adminFS fs.FS) (Admin, error) {
|
||||
into *string
|
||||
path string
|
||||
}{
|
||||
{&a.NebulaCACert.CACert, nebulaCertsCACertPath},
|
||||
{&a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
|
||||
{&a.NebulaCACredentials.CACertPEM, nebulaCertsCACertPath},
|
||||
{&a.NebulaCACredentials.CAKeyPEM, nebulaCertsCAKeyPath},
|
||||
{&a.GarageRPCSecret, garageRPCSecretPath},
|
||||
}
|
||||
|
||||
@ -122,8 +122,8 @@ func (a Admin) WriteTo(into io.Writer) error {
|
||||
value string
|
||||
path string
|
||||
}{
|
||||
{a.NebulaCACert.CACert, nebulaCertsCACertPath},
|
||||
{a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
|
||||
{a.NebulaCACredentials.CACertPEM, nebulaCertsCACertPath},
|
||||
{a.NebulaCACredentials.CAKeyPEM, nebulaCertsCAKeyPath},
|
||||
{a.GarageRPCSecret, garageRPCSecretPath},
|
||||
}
|
||||
|
||||
|
@ -45,7 +45,7 @@ type Bootstrap struct {
|
||||
Hosts map[string]Host
|
||||
HostName string
|
||||
|
||||
NebulaHostCert nebula.HostCert
|
||||
NebulaHostCredentials nebula.HostCredentials
|
||||
|
||||
GarageRPCSecret string
|
||||
GarageAdminToken string
|
||||
@ -84,9 +84,9 @@ func FromFS(bootstrapFS fs.FS) (Bootstrap, error) {
|
||||
path string
|
||||
}{
|
||||
{&b.HostName, hostNamePath},
|
||||
{&b.NebulaHostCert.CACert, nebulaCertsCACertPath},
|
||||
{&b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
|
||||
{&b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
|
||||
{&b.NebulaHostCredentials.CACertPEM, nebulaCertsCACertPath},
|
||||
{&b.NebulaHostCredentials.HostCertPEM, nebulaCertsHostCertPath},
|
||||
{&b.NebulaHostCredentials.HostKeyPEM, nebulaCertsHostKeyPath},
|
||||
{&b.GarageRPCSecret, garageRPCSecretPath},
|
||||
{&b.GarageAdminToken, garageAdminTokenPath},
|
||||
}
|
||||
@ -165,9 +165,9 @@ func (b Bootstrap) WriteTo(into io.Writer) error {
|
||||
path string
|
||||
}{
|
||||
{b.HostName, hostNamePath},
|
||||
{b.NebulaHostCert.CACert, nebulaCertsCACertPath},
|
||||
{b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
|
||||
{b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
|
||||
{b.NebulaHostCredentials.CACertPEM, nebulaCertsCACertPath},
|
||||
{b.NebulaHostCredentials.HostCertPEM, nebulaCertsHostCertPath},
|
||||
{b.NebulaHostCredentials.HostKeyPEM, nebulaCertsHostKeyPath},
|
||||
{b.GarageRPCSecret, garageRPCSecretPath},
|
||||
{b.GarageAdminToken, garageAdminTokenPath},
|
||||
}
|
||||
@ -209,19 +209,3 @@ func HostsHash(hostsMap map[string]Host) ([]byte, error) {
|
||||
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
// WithHosts returns a copy of the Bootstrap with the given set of Hosts applied
|
||||
// to it. It will _not_ overwrite the Host for _this_ host, however.
|
||||
func (b Bootstrap) WithHosts(hosts map[string]Host) Bootstrap {
|
||||
|
||||
hostsCopy := make(map[string]Host, len(hosts))
|
||||
|
||||
for name, host := range hosts {
|
||||
hostsCopy[name] = host
|
||||
}
|
||||
|
||||
hostsCopy[b.HostName] = b.ThisHost()
|
||||
|
||||
b.Hosts = hostsCopy
|
||||
return b
|
||||
}
|
||||
|
@ -12,9 +12,9 @@ const (
|
||||
)
|
||||
|
||||
// GaragePeers returns a Peer for each known garage instance in the network.
|
||||
func (b Bootstrap) GaragePeers() []garage.Peer {
|
||||
func (b Bootstrap) GaragePeers() []garage.RemotePeer {
|
||||
|
||||
var peers []garage.Peer
|
||||
var peers []garage.RemotePeer
|
||||
|
||||
for _, host := range b.Hosts {
|
||||
|
||||
@ -24,8 +24,9 @@ func (b Bootstrap) GaragePeers() []garage.Peer {
|
||||
|
||||
for _, instance := range host.Garage.Instances {
|
||||
|
||||
peer := garage.Peer{
|
||||
IP: host.Nebula.IP,
|
||||
peer := garage.RemotePeer{
|
||||
ID: instance.ID,
|
||||
IP: host.IP().String(),
|
||||
RPCPort: instance.RPCPort,
|
||||
S3APIPort: instance.S3APIPort,
|
||||
}
|
||||
@ -50,14 +51,16 @@ func (b Bootstrap) GarageRPCPeerAddrs() []string {
|
||||
// ChooseGaragePeer returns a Peer for a garage instance from the network. It
|
||||
// will prefer a garage instance on this particular host, if there is one, but
|
||||
// will otherwise return a random endpoint.
|
||||
func (b Bootstrap) ChooseGaragePeer() garage.Peer {
|
||||
func (b Bootstrap) ChooseGaragePeer() garage.RemotePeer {
|
||||
|
||||
thisHost := b.ThisHost()
|
||||
|
||||
if thisHost.Garage != nil && len(thisHost.Garage.Instances) > 0 {
|
||||
|
||||
inst := thisHost.Garage.Instances[0]
|
||||
return garage.Peer{
|
||||
IP: thisHost.Nebula.IP,
|
||||
return garage.RemotePeer{
|
||||
ID: inst.ID,
|
||||
IP: thisHost.IP().String(),
|
||||
RPCPort: inst.RPCPort,
|
||||
S3APIPort: inst.S3APIPort,
|
||||
}
|
||||
|
@ -4,8 +4,9 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"cryptic-net/garage"
|
||||
"cryptic-net/nebula"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
@ -17,23 +18,32 @@ const (
|
||||
garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts"
|
||||
)
|
||||
|
||||
// PutGarageBoostrapHost places the <hostname>.yml file for the given host into
|
||||
// garage so that other hosts are able to see relevant configuration for it.
|
||||
//
|
||||
// The given client should be for the global bucket.
|
||||
func PutGarageBoostrapHost(
|
||||
ctx context.Context, client garage.S3APIClient, host Host,
|
||||
) error {
|
||||
// PutGarageBoostrapHost places the <hostname>.yml.signed file for this host
|
||||
// into garage so that other hosts are able to see relevant configuration for
|
||||
// it.
|
||||
func (b Bootstrap) PutGarageBoostrapHost(ctx context.Context) error {
|
||||
|
||||
host := b.ThisHost()
|
||||
client := b.GlobalBucketS3APIClient()
|
||||
|
||||
hostB, err := yaml.Marshal(host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("yaml encoding host data: %w", err)
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if err := yaml.NewEncoder(buf).Encode(host); err != nil {
|
||||
log.Fatalf("yaml encoding host data: %v", err)
|
||||
err = nebula.SignAndWrap(buf, b.NebulaHostCredentials.HostKeyPEM, hostB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("signing encoded host data: %w", err)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, host.Name+".yml")
|
||||
filePath := filepath.Join(
|
||||
garageGlobalBucketBootstrapHostsDirPath,
|
||||
host.Name+".yml.signed",
|
||||
)
|
||||
|
||||
_, err := client.PutObject(
|
||||
_, err = client.PutObject(
|
||||
ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()),
|
||||
minio.PutObjectOptions{},
|
||||
)
|
||||
@ -45,15 +55,18 @@ func PutGarageBoostrapHost(
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveGarageBootstrapHost removes the <hostname>.yml for the given host from
|
||||
// garage.
|
||||
// RemoveGarageBootstrapHost removes the <hostname>.yml.signed for the given
|
||||
// host from garage.
|
||||
//
|
||||
// The given client should be for the global bucket.
|
||||
func RemoveGarageBootstrapHost(
|
||||
ctx context.Context, client garage.S3APIClient, hostName string,
|
||||
) error {
|
||||
|
||||
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, hostName+".yml")
|
||||
filePath := filepath.Join(
|
||||
garageGlobalBucketBootstrapHostsDirPath,
|
||||
hostName+".yml.signed",
|
||||
)
|
||||
|
||||
return client.RemoveObject(
|
||||
ctx, garage.GlobalBucket, filePath,
|
||||
@ -61,16 +74,17 @@ func RemoveGarageBootstrapHost(
|
||||
)
|
||||
}
|
||||
|
||||
// GetGarageBootstrapHosts loads the <hostname>.yml file for all hosts stored in
|
||||
// garage.
|
||||
//
|
||||
// The given client should be for the global bucket.
|
||||
func GetGarageBootstrapHosts(
|
||||
ctx context.Context, client garage.S3APIClient,
|
||||
// GetGarageBootstrapHosts loads the <hostname>.yml.signed file for all hosts
|
||||
// stored in garage.
|
||||
func (b Bootstrap) GetGarageBootstrapHosts(
|
||||
ctx context.Context,
|
||||
) (
|
||||
map[string]Host, error,
|
||||
) {
|
||||
|
||||
caCertPEM := b.NebulaHostCredentials.CACertPEM
|
||||
client := b.GlobalBucketS3APIClient()
|
||||
|
||||
hosts := map[string]Host{}
|
||||
|
||||
objInfoCh := client.ListObjects(
|
||||
@ -95,15 +109,30 @@ func GetGarageBootstrapHosts(
|
||||
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
|
||||
}
|
||||
|
||||
var host Host
|
||||
|
||||
err = yaml.NewDecoder(obj).Decode(&host)
|
||||
hostB, sig, err := nebula.Unwrap(obj)
|
||||
obj.Close()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unwrapping signature from %q: %w", objInfo.Key, err)
|
||||
}
|
||||
|
||||
var host Host
|
||||
if err = yaml.Unmarshal(hostB, &host); err != nil {
|
||||
return nil, fmt.Errorf("yaml decoding object %q: %w", objInfo.Key, err)
|
||||
}
|
||||
|
||||
hostCertPEM := host.Nebula.CertPEM
|
||||
|
||||
if err := nebula.ValidateSignature(hostCertPEM, hostB, sig); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid host data for %q: %w\n", objInfo.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := nebula.ValidateHostCertPEM(caCertPEM, hostCertPEM); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "invalid nebula cert for %q: %w\n", objInfo.Key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
hosts[host.Name] = host
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"cryptic-net/nebula"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@ -16,14 +18,15 @@ const (
|
||||
// NebulaHost describes the nebula configuration of a Host which is relevant for
|
||||
// other hosts to know.
|
||||
type NebulaHost struct {
|
||||
IP string `yaml:"ip"`
|
||||
CertPEM string `yaml:"crt"`
|
||||
PublicAddr string `yaml:"public_addr,omitempty"`
|
||||
}
|
||||
|
||||
// GarageHost describes a single garage instance in the GarageHost.
|
||||
type GarageHostInstance struct {
|
||||
RPCPort int `yaml:"rpc_port"`
|
||||
S3APIPort int `yaml:"s3_api_port"`
|
||||
ID string `yaml:"id"`
|
||||
RPCPort int `yaml:"rpc_port"`
|
||||
S3APIPort int `yaml:"s3_api_port"`
|
||||
}
|
||||
|
||||
// GarageHost describes the garage configuration of a Host which is relevant for
|
||||
@ -40,6 +43,18 @@ type Host struct {
|
||||
Garage *GarageHost `yaml:"garage,omitempty"`
|
||||
}
|
||||
|
||||
// IP returns the IP address encoded in the Host's nebula certificate, or panics
|
||||
// if there is an error.
|
||||
func (h Host) IP() net.IP {
|
||||
|
||||
ip, err := nebula.IPFromHostCertPEM(h.Nebula.CertPEM)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("could not parse IP out of cert for host %q: %w", h.Name, err))
|
||||
}
|
||||
|
||||
return ip
|
||||
}
|
||||
|
||||
func loadHosts(bootstrapFS fs.FS) (map[string]Host, error) {
|
||||
|
||||
hosts := map[string]Host{}
|
||||
|
@ -117,12 +117,12 @@ var subCmdAdminCreateNetwork = subCmd{
|
||||
return fmt.Errorf("daemon config with at least 3 allocations was not provided")
|
||||
}
|
||||
|
||||
nebulaCACert, err := nebula.NewCACert(*domain, subnet)
|
||||
nebulaCACreds, err := nebula.NewCACredentials(*domain, subnet)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating nebula CA cert: %w", err)
|
||||
}
|
||||
|
||||
nebulaHostCert, err := nebula.NewHostCert(nebulaCACert, *hostName, ip)
|
||||
nebulaHostCreds, err := nebula.NewHostCredentials(nebulaCACreds, *hostName, ip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating nebula cert for host: %w", err)
|
||||
}
|
||||
@ -138,12 +138,12 @@ var subCmdAdminCreateNetwork = subCmd{
|
||||
*hostName: bootstrap.Host{
|
||||
Name: *hostName,
|
||||
Nebula: bootstrap.NebulaHost{
|
||||
IP: ip.String(),
|
||||
CertPEM: nebulaHostCreds.HostCertPEM,
|
||||
},
|
||||
},
|
||||
},
|
||||
HostName: *hostName,
|
||||
NebulaHostCert: nebulaHostCert,
|
||||
NebulaHostCredentials: nebulaHostCreds,
|
||||
GarageRPCSecret: randStr(32),
|
||||
GarageAdminToken: randStr(32),
|
||||
GarageGlobalBucketS3APICredentials: garage.NewS3APICredentials(),
|
||||
@ -213,7 +213,7 @@ var subCmdAdminCreateNetwork = subCmd{
|
||||
|
||||
err = admin.Admin{
|
||||
CreationParams: adminCreationParams,
|
||||
NebulaCACert: nebulaCACert,
|
||||
NebulaCACredentials: nebulaCACreds,
|
||||
GarageRPCSecret: hostBootstrap.GarageRPCSecret,
|
||||
GarageGlobalBucketS3APICredentials: hostBootstrap.GarageGlobalBucketS3APICredentials,
|
||||
GarageAdminBucketS3APICredentials: garage.NewS3APICredentials(),
|
||||
@ -240,6 +240,11 @@ var subCmdAdminMakeBootstrap = subCmd{
|
||||
"Name of the host to generate bootstrap.tgz for",
|
||||
)
|
||||
|
||||
ipStr := flags.StringP(
|
||||
"ip", "i", "",
|
||||
"IP of the new host",
|
||||
)
|
||||
|
||||
adminPath := flags.StringP(
|
||||
"admin-path", "a", "",
|
||||
`Path to admin.tgz file. If the given path is "-" then stdin is used.`,
|
||||
@ -249,13 +254,18 @@ var subCmdAdminMakeBootstrap = subCmd{
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if *name == "" || *adminPath == "" {
|
||||
return errors.New("--name and --admin-path are required")
|
||||
if *name == "" || *ipStr == "" || *adminPath == "" {
|
||||
return errors.New("--name, --ip, and --admin-path are required")
|
||||
}
|
||||
|
||||
hostBootstrap, err := loadHostBootstrap()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||
if err := validateHostName(*name); err != nil {
|
||||
return fmt.Errorf("invalid hostname %q: %w", *name, err)
|
||||
}
|
||||
|
||||
ip := net.ParseIP(*ipStr)
|
||||
|
||||
if ip == nil {
|
||||
return fmt.Errorf("invalid ip %q", *ipStr)
|
||||
}
|
||||
|
||||
adm, err := readAdmin(*adminPath)
|
||||
@ -263,28 +273,12 @@ var subCmdAdminMakeBootstrap = subCmd{
|
||||
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
|
||||
}
|
||||
|
||||
client := hostBootstrap.GlobalBucketS3APIClient()
|
||||
|
||||
// NOTE this isn't _technically_ required, but if the `hosts add`
|
||||
// command for this host has been run recently then it might not have
|
||||
// made it into the bootstrap file yet, and so won't be in
|
||||
// `hostBootstrap`.
|
||||
hosts, err := bootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx, client)
|
||||
hostBootstrap, err := loadHostBootstrap()
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving host info from garage: %w", err)
|
||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||
}
|
||||
|
||||
host, ok := hosts[*name]
|
||||
if !ok {
|
||||
return fmt.Errorf("couldn't find host into for %q in garage, has `cryptic-net hosts add` been run yet?", *name)
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host.Nebula.IP)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("invalid IP stored with host %q: %q", *name, host.Nebula.IP)
|
||||
}
|
||||
|
||||
nebulaHostCert, err := nebula.NewHostCert(adm.NebulaCACert, host.Name, ip)
|
||||
nebulaHostCreds, err := nebula.NewHostCredentials(adm.NebulaCACredentials, *name, ip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating new nebula host key/cert: %w", err)
|
||||
}
|
||||
@ -292,10 +286,10 @@ var subCmdAdminMakeBootstrap = subCmd{
|
||||
newHostBootstrap := bootstrap.Bootstrap{
|
||||
AdminCreationParams: adm.CreationParams,
|
||||
|
||||
Hosts: hosts,
|
||||
Hosts: hostBootstrap.Hosts,
|
||||
HostName: *name,
|
||||
|
||||
NebulaHostCert: nebulaHostCert,
|
||||
NebulaHostCredentials: nebulaHostCreds,
|
||||
|
||||
GarageRPCSecret: adm.GarageRPCSecret,
|
||||
GarageAdminToken: randStr(32),
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/daemon"
|
||||
"cryptic-net/garage"
|
||||
|
||||
"code.betamike.com/cryptic-io/pmux/pmuxlib"
|
||||
)
|
||||
@ -42,16 +41,21 @@ import (
|
||||
func reloadBootstrap(
|
||||
ctx context.Context,
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
s3Client garage.S3APIClient,
|
||||
) (
|
||||
bootstrap.Bootstrap, bool, error,
|
||||
) {
|
||||
|
||||
newHosts, err := bootstrap.GetGarageBootstrapHosts(ctx, s3Client)
|
||||
thisHost := hostBootstrap.ThisHost()
|
||||
|
||||
newHosts, err := hostBootstrap.GetGarageBootstrapHosts(ctx)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err)
|
||||
}
|
||||
|
||||
// the daemon's view of this host's bootstrap info takes precedence over
|
||||
// whatever is in garage
|
||||
newHosts[thisHost.Name] = thisHost
|
||||
|
||||
newHostsHash, err := bootstrap.HostsHash(newHosts)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, false, fmt.Errorf("calculating hash of new hosts: %w", err)
|
||||
@ -66,13 +70,8 @@ func reloadBootstrap(
|
||||
return hostBootstrap, false, nil
|
||||
}
|
||||
|
||||
newHostBootstrap := hostBootstrap.WithHosts(newHosts)
|
||||
|
||||
if err := writeBootstrapToDataDir(newHostBootstrap); err != nil {
|
||||
return bootstrap.Bootstrap{}, false, fmt.Errorf("writing new bootstrap.tgz to data dir: %w", err)
|
||||
}
|
||||
|
||||
return newHostBootstrap, true, nil
|
||||
hostBootstrap.Hosts = newHosts
|
||||
return hostBootstrap, true, nil
|
||||
}
|
||||
|
||||
// runs a single pmux process of daemon, returning only once the env.Context has
|
||||
@ -87,14 +86,6 @@ func runDaemonPmuxOnce(
|
||||
bootstrap.Bootstrap, error,
|
||||
) {
|
||||
|
||||
thisHost := hostBootstrap.ThisHost()
|
||||
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
|
||||
// changed and we should be connecting to a different garage
|
||||
// endpoint.
|
||||
s3Client := hostBootstrap.GlobalBucketS3APIClient()
|
||||
|
||||
nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(hostBootstrap, daemonConfig)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf("generating nebula config: %w", err)
|
||||
@ -143,11 +134,9 @@ func runDaemonPmuxOnce(
|
||||
return
|
||||
}
|
||||
|
||||
thisHost := hostBootstrap.ThisHost()
|
||||
|
||||
err := doOnce(ctx, func(ctx context.Context) error {
|
||||
fmt.Fprintln(os.Stderr, "updating host info in garage")
|
||||
return bootstrap.PutGarageBoostrapHost(ctx, s3Client, thisHost)
|
||||
return hostBootstrap.PutGarageBoostrapHost(ctx)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@ -194,7 +183,7 @@ func runDaemonPmuxOnce(
|
||||
err error
|
||||
)
|
||||
|
||||
if hostBootstrap, changed, err = reloadBootstrap(ctx, hostBootstrap, s3Client); err != nil {
|
||||
if hostBootstrap, changed, err = reloadBootstrap(ctx, hostBootstrap); err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf("reloading bootstrap: %w", err)
|
||||
|
||||
} else if changed {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"cryptic-net/bootstrap"
|
||||
"cryptic-net/daemon"
|
||||
"cryptic-net/garage"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
@ -25,7 +26,14 @@ func mergeDaemonConfigIntoBootstrap(
|
||||
host.Garage = new(bootstrap.GarageHost)
|
||||
|
||||
for _, alloc := range allocs {
|
||||
|
||||
id, err := garage.InitAlloc(alloc.MetaPath)
|
||||
if err != nil {
|
||||
return bootstrap.Bootstrap{}, fmt.Errorf("initializing alloc at %q: %w", alloc.MetaPath, err)
|
||||
}
|
||||
|
||||
host.Garage.Instances = append(host.Garage.Instances, bootstrap.GarageHostInstance{
|
||||
ID: id,
|
||||
RPCPort: alloc.RPCPort,
|
||||
S3APIPort: alloc.S3APIPort,
|
||||
})
|
||||
|
@ -20,19 +20,22 @@ func dnsmasqPmuxProcConfig(
|
||||
|
||||
confPath := filepath.Join(envRuntimeDirPath, "dnsmasq.conf")
|
||||
|
||||
hostsSlice := make([]bootstrap.Host, 0, len(hostBootstrap.Hosts))
|
||||
hostsSlice := make([]dnsmasq.ConfDataHost, 0, len(hostBootstrap.Hosts))
|
||||
for _, host := range hostBootstrap.Hosts {
|
||||
hostsSlice = append(hostsSlice, host)
|
||||
hostsSlice = append(hostsSlice, dnsmasq.ConfDataHost{
|
||||
Name: host.Name,
|
||||
IP: host.IP().String(),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(hostsSlice, func(i, j int) bool {
|
||||
return hostsSlice[i].Nebula.IP < hostsSlice[j].Nebula.IP
|
||||
return hostsSlice[i].IP < hostsSlice[j].IP
|
||||
})
|
||||
|
||||
confData := dnsmasq.ConfData{
|
||||
Resolvers: daemonConfig.DNS.Resolvers,
|
||||
Domain: hostBootstrap.AdminCreationParams.Domain,
|
||||
IP: hostBootstrap.ThisHost().Nebula.IP,
|
||||
IP: hostBootstrap.ThisHost().IP().String(),
|
||||
Hosts: hostsSlice,
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import (
|
||||
"cryptic-net/garage"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
@ -24,7 +23,7 @@ func newGarageAdminClient(
|
||||
|
||||
return garage.NewAdminClient(
|
||||
net.JoinHostPort(
|
||||
thisHost.Nebula.IP,
|
||||
thisHost.IP().String(),
|
||||
strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort),
|
||||
),
|
||||
hostBootstrap.GarageAdminToken,
|
||||
@ -48,7 +47,7 @@ func waitForGarageAndNebula(
|
||||
for _, alloc := range allocs {
|
||||
|
||||
adminAddr := net.JoinHostPort(
|
||||
hostBootstrap.ThisHost().Nebula.IP,
|
||||
hostBootstrap.ThisHost().IP().String(),
|
||||
strconv.Itoa(alloc.AdminPort),
|
||||
)
|
||||
|
||||
@ -66,6 +65,25 @@ func waitForGarageAndNebula(
|
||||
|
||||
}
|
||||
|
||||
// bootstrapGarageHostForAlloc returns the bootstrap.GarageHostInstance which
|
||||
// corresponds with the given alloc from the daemon config. This will panic if
|
||||
// no associated instance can be found.
|
||||
//
|
||||
// This assumes that mergeDaemonConfigIntoBootstrap has already been called.
|
||||
func bootstrapGarageHostForAlloc(
|
||||
host bootstrap.Host,
|
||||
alloc daemon.ConfigStorageAllocation,
|
||||
) bootstrap.GarageHostInstance {
|
||||
|
||||
for _, inst := range host.Garage.Instances {
|
||||
if inst.RPCPort == alloc.RPCPort {
|
||||
return inst
|
||||
}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("could not find alloc %+v in the bootstrap data", alloc))
|
||||
}
|
||||
|
||||
func garageWriteChildConfig(
|
||||
hostBootstrap bootstrap.Bootstrap,
|
||||
alloc daemon.ConfigStorageAllocation,
|
||||
@ -73,28 +91,17 @@ func garageWriteChildConfig(
|
||||
string, error,
|
||||
) {
|
||||
|
||||
if err := os.MkdirAll(alloc.MetaPath, 0750); err != nil {
|
||||
return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err)
|
||||
}
|
||||
|
||||
thisHost := hostBootstrap.ThisHost()
|
||||
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID
|
||||
|
||||
peer := garage.Peer{
|
||||
IP: thisHost.Nebula.IP,
|
||||
RPCPort: alloc.RPCPort,
|
||||
S3APIPort: alloc.S3APIPort,
|
||||
}
|
||||
|
||||
pubKey, privKey := peer.RPCPeerKey()
|
||||
|
||||
nodeKeyPath := filepath.Join(alloc.MetaPath, "node_key")
|
||||
nodeKeyPubPath := filepath.Join(alloc.MetaPath, "node_keypub")
|
||||
|
||||
if err := os.WriteFile(nodeKeyPath, privKey, 0400); err != nil {
|
||||
return "", fmt.Errorf("writing private key to %q: %w", nodeKeyPath, err)
|
||||
|
||||
} else if err := os.WriteFile(nodeKeyPubPath, pubKey, 0440); err != nil {
|
||||
return "", fmt.Errorf("writing public key to %q: %w", nodeKeyPubPath, err)
|
||||
peer := garage.LocalPeer{
|
||||
RemotePeer: garage.RemotePeer{
|
||||
ID: id,
|
||||
IP: thisHost.IP().String(),
|
||||
RPCPort: alloc.RPCPort,
|
||||
S3APIPort: alloc.S3APIPort,
|
||||
},
|
||||
AdminPort: alloc.AdminPort,
|
||||
}
|
||||
|
||||
garageTomlPath := filepath.Join(
|
||||
@ -108,9 +115,9 @@ func garageWriteChildConfig(
|
||||
RPCSecret: hostBootstrap.GarageRPCSecret,
|
||||
AdminToken: hostBootstrap.GarageAdminToken,
|
||||
|
||||
RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
|
||||
APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)),
|
||||
AdminAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.AdminPort)),
|
||||
RPCAddr: peer.RPCAddr(),
|
||||
S3APIAddr: peer.S3APIAddr(),
|
||||
AdminAddr: peer.AdminAddr(),
|
||||
|
||||
BootstrapPeers: hostBootstrap.GarageRPCPeerAddrs(),
|
||||
})
|
||||
@ -224,7 +231,6 @@ func garageApplyLayout(
|
||||
adminClient = newGarageAdminClient(hostBootstrap, daemonConfig)
|
||||
thisHost = hostBootstrap.ThisHost()
|
||||
hostName = thisHost.Name
|
||||
ip = thisHost.Nebula.IP
|
||||
allocs = daemonConfig.Storage.Allocations
|
||||
)
|
||||
|
||||
@ -239,13 +245,9 @@ func garageApplyLayout(
|
||||
|
||||
for _, alloc := range allocs {
|
||||
|
||||
peer := garage.Peer{
|
||||
IP: ip,
|
||||
RPCPort: alloc.RPCPort,
|
||||
S3APIPort: alloc.S3APIPort,
|
||||
}
|
||||
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID
|
||||
|
||||
clusterLayout[peer.RPCPeerID()] = peerLayout{
|
||||
clusterLayout[id] = peerLayout{
|
||||
Capacity: alloc.Capacity / 100,
|
||||
Zone: hostName,
|
||||
Tags: []string{},
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"cryptic-net/bootstrap"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
@ -23,60 +22,6 @@ func validateHostName(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var subCmdHostsAdd = subCmd{
|
||||
name: "add",
|
||||
descr: "Adds a host to the network",
|
||||
checkLock: true,
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
|
||||
flags := subCmdCtx.flagSet(false)
|
||||
|
||||
name := flags.StringP(
|
||||
"name", "n", "",
|
||||
"Name of the new host",
|
||||
)
|
||||
|
||||
ip := flags.StringP(
|
||||
"ip", "i", "",
|
||||
"IP of the new host",
|
||||
)
|
||||
|
||||
if err := flags.Parse(subCmdCtx.args); err != nil {
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
|
||||
if *name == "" || *ip == "" {
|
||||
return errors.New("--name and --ip are required")
|
||||
}
|
||||
|
||||
if err := validateHostName(*name); err != nil {
|
||||
return fmt.Errorf("invalid hostname %q: %w", *name, err)
|
||||
}
|
||||
|
||||
if net.ParseIP(*ip) == nil {
|
||||
return fmt.Errorf("invalid ip %q", *ip)
|
||||
}
|
||||
|
||||
// TODO validate that the IP is in the correct CIDR
|
||||
|
||||
hostBootstrap, err := loadHostBootstrap()
|
||||
if err != nil {
|
||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||
}
|
||||
|
||||
client := hostBootstrap.GlobalBucketS3APIClient()
|
||||
|
||||
host := bootstrap.Host{
|
||||
Name: *name,
|
||||
Nebula: bootstrap.NebulaHost{
|
||||
IP: *ip,
|
||||
},
|
||||
}
|
||||
|
||||
return bootstrap.PutGarageBoostrapHost(subCmdCtx.ctx, client, host)
|
||||
},
|
||||
}
|
||||
|
||||
var subCmdHostsList = subCmd{
|
||||
name: "list",
|
||||
descr: "Lists all hosts in the network, and their IPs",
|
||||
@ -88,9 +33,7 @@ var subCmdHostsList = subCmd{
|
||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||
}
|
||||
|
||||
client := hostBootstrap.GlobalBucketS3APIClient()
|
||||
|
||||
hostsMap, err := bootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx, client)
|
||||
hostsMap, err := hostBootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving hosts from garage: %w", err)
|
||||
}
|
||||
@ -143,7 +86,6 @@ var subCmdHosts = subCmd{
|
||||
descr: "Sub-commands having to do with configuration of hosts in the network",
|
||||
do: func(subCmdCtx subCmdCtx) error {
|
||||
return subCmdCtx.doSubCmd(
|
||||
subCmdHostsAdd,
|
||||
subCmdHostsDelete,
|
||||
subCmdHostsList,
|
||||
)
|
||||
|
@ -18,8 +18,7 @@ import (
|
||||
// interface has been initialized.
|
||||
func waitForNebula(ctx context.Context, hostBootstrap bootstrap.Bootstrap) error {
|
||||
|
||||
ipStr := hostBootstrap.ThisHost().Nebula.IP
|
||||
ip := net.ParseIP(ipStr)
|
||||
ip := hostBootstrap.ThisHost().IP()
|
||||
|
||||
lUdpAddr := &net.UDPAddr{IP: ip, Port: 0}
|
||||
rUdpAddr := &net.UDPAddr{IP: ip, Port: 45535}
|
||||
@ -52,15 +51,16 @@ func nebulaPmuxProcConfig(
|
||||
continue
|
||||
}
|
||||
|
||||
lighthouseHostIPs = append(lighthouseHostIPs, host.Nebula.IP)
|
||||
staticHostMap[host.Nebula.IP] = []string{host.Nebula.PublicAddr}
|
||||
ip := host.IP().String()
|
||||
lighthouseHostIPs = append(lighthouseHostIPs, ip)
|
||||
staticHostMap[ip] = []string{host.Nebula.PublicAddr}
|
||||
}
|
||||
|
||||
config := map[string]interface{}{
|
||||
"pki": map[string]string{
|
||||
"ca": hostBootstrap.NebulaHostCert.CACert,
|
||||
"cert": hostBootstrap.NebulaHostCert.HostCert,
|
||||
"key": hostBootstrap.NebulaHostCert.HostKey,
|
||||
"ca": hostBootstrap.NebulaHostCredentials.CACertPEM,
|
||||
"cert": hostBootstrap.NebulaHostCredentials.HostCertPEM,
|
||||
"key": hostBootstrap.NebulaHostCredentials.HostKeyPEM,
|
||||
},
|
||||
"static_host_map": staticHostMap,
|
||||
"punchy": map[string]bool{
|
||||
|
@ -1,18 +1,23 @@
|
||||
package dnsmasq
|
||||
|
||||
import (
|
||||
"cryptic-net/bootstrap"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// ConfDataHost describes a host which can be resolved by dnsmasq.
|
||||
type ConfDataHost struct {
|
||||
Name string
|
||||
IP string
|
||||
}
|
||||
|
||||
// ConfData describes all the data needed to populate a dnsmasq.conf file.
|
||||
type ConfData struct {
|
||||
Resolvers []string
|
||||
Domain string
|
||||
IP string
|
||||
Hosts []bootstrap.Host
|
||||
Hosts []ConfDataHost
|
||||
}
|
||||
|
||||
var confTpl = template.Must(template.New("").Parse(`
|
||||
|
@ -2,6 +2,15 @@
|
||||
// setting up garage configs, processes, and deployments.
|
||||
package garage
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
// Region is the region which garage is configured with.
|
||||
@ -15,3 +24,81 @@ const (
|
||||
// cluster. We currently only support a factor of 3.
|
||||
ReplicationFactor = 3
|
||||
)
|
||||
|
||||
func nodeKeyPath(metaDirPath string) string {
|
||||
return filepath.Join(metaDirPath, "node_key")
|
||||
}
|
||||
|
||||
func nodeKeyPubPath(metaDirPath string) string {
|
||||
return filepath.Join(metaDirPath, "node_key.pub")
|
||||
}
|
||||
|
||||
// LoadAllocID returns the peer ID (ie the public key) of the node at the given
|
||||
// meta directory.
|
||||
func LoadAllocID(metaDirPath string) (string, error) {
|
||||
nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
|
||||
|
||||
pubKey, err := os.ReadFile(nodeKeyPubPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading %q: %w", nodeKeyPubPath, err)
|
||||
}
|
||||
|
||||
return hex.EncodeToString(pubKey), nil
|
||||
}
|
||||
|
||||
// InitAlloc initializes the meta directory and keys for a particular
|
||||
// allocation, if it hasn't been done so already. It returns the peer ID (ie the
|
||||
// public key) in any case.
|
||||
func InitAlloc(metaDirPath string) (string, error) {
|
||||
|
||||
var err error
|
||||
|
||||
exists := func(path string) bool {
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
} else if _, err = os.Stat(path); errors.Is(err, fs.ErrNotExist) {
|
||||
return false
|
||||
|
||||
} else if err != nil {
|
||||
err = fmt.Errorf("checking if %q exists: %w", path, err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
nodeKeyPath := nodeKeyPath(metaDirPath)
|
||||
nodeKeyPubPath := nodeKeyPubPath(metaDirPath)
|
||||
|
||||
nodeKeyPathExists := exists(nodeKeyPath)
|
||||
nodeKeyPubPathExists := exists(nodeKeyPubPath)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
} else if nodeKeyPubPathExists != nodeKeyPathExists {
|
||||
return "", fmt.Errorf("%q or %q exist without the other existing", nodeKeyPath, nodeKeyPubPath)
|
||||
|
||||
} else if nodeKeyPathExists {
|
||||
return LoadAllocID(metaDirPath)
|
||||
}
|
||||
|
||||
// node key hasn't been written, write it
|
||||
|
||||
if err := os.MkdirAll(metaDirPath, 0750); err != nil {
|
||||
return "", fmt.Errorf("making directory %q: %w", metaDirPath, err)
|
||||
}
|
||||
|
||||
pubKey, privKey := GeneratePeerKey()
|
||||
|
||||
if err := os.WriteFile(nodeKeyPath, privKey, 0400); err != nil {
|
||||
return "", fmt.Errorf("writing private key to %q: %w", nodeKeyPath, err)
|
||||
|
||||
} else if err := os.WriteFile(nodeKeyPubPath, pubKey, 0440); err != nil {
|
||||
return "", fmt.Errorf("writing public key to %q: %w", nodeKeyPubPath, err)
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
package garage
|
||||
|
||||
import "io"
|
||||
|
||||
type infiniteReader struct {
|
||||
b []byte
|
||||
i int
|
||||
}
|
||||
|
||||
// NewInfiniteReader returns a reader which will produce the given bytes in
|
||||
// repetition. len(b) must be greater than 0.
|
||||
func NewInfiniteReader(b []byte) io.Reader {
|
||||
|
||||
if len(b) == 0 {
|
||||
panic("len(b) must be greater than 0")
|
||||
}
|
||||
|
||||
return &infiniteReader{b: b}
|
||||
}
|
||||
|
||||
func (r *infiniteReader) Read(b []byte) (int, error) {
|
||||
|
||||
// here, have a puzzle
|
||||
|
||||
var n int
|
||||
|
||||
for {
|
||||
|
||||
n += copy(b[n:], r.b[r.i:])
|
||||
|
||||
if r.i > 0 {
|
||||
n += copy(b[n:], r.b[:r.i])
|
||||
}
|
||||
|
||||
r.i = (r.i + n) % len(r.b)
|
||||
|
||||
if n >= len(b) {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
package garage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInfiniteReader(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
in []byte
|
||||
size int
|
||||
exp []string
|
||||
}{
|
||||
{
|
||||
in: []byte("a"),
|
||||
size: 1,
|
||||
exp: []string{"a"},
|
||||
},
|
||||
{
|
||||
in: []byte("ab"),
|
||||
size: 1,
|
||||
exp: []string{"a", "b"},
|
||||
},
|
||||
{
|
||||
in: []byte("ab"),
|
||||
size: 2,
|
||||
exp: []string{"ab"},
|
||||
},
|
||||
{
|
||||
in: []byte("ab"),
|
||||
size: 3,
|
||||
exp: []string{"aba", "bab"},
|
||||
},
|
||||
{
|
||||
in: []byte("ab"),
|
||||
size: 4,
|
||||
exp: []string{"abab"},
|
||||
},
|
||||
{
|
||||
in: []byte("ab"),
|
||||
size: 5,
|
||||
exp: []string{"ababa", "babab"},
|
||||
},
|
||||
{
|
||||
in: []byte("abc"),
|
||||
size: 1,
|
||||
exp: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
in: []byte("abc"),
|
||||
size: 2,
|
||||
exp: []string{"ab", "ca", "bc"},
|
||||
},
|
||||
{
|
||||
in: []byte("abc"),
|
||||
size: 3,
|
||||
exp: []string{"abc"},
|
||||
},
|
||||
{
|
||||
in: []byte("abc"),
|
||||
size: 4,
|
||||
exp: []string{"abca", "bcab", "cabc"},
|
||||
},
|
||||
{
|
||||
in: []byte("abc"),
|
||||
size: 5,
|
||||
exp: []string{"abcab", "cabca", "bcabc"},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(strconv.Itoa(i), func(t *testing.T) {
|
||||
|
||||
r := NewInfiniteReader(test.in)
|
||||
buf := make([]byte, test.size)
|
||||
|
||||
assertRead := func(expBuf []byte) {
|
||||
|
||||
n, err := r.Read(buf)
|
||||
|
||||
if !bytes.Equal(buf, expBuf) {
|
||||
t.Fatalf("expected bytes %q, got %q", expBuf, buf)
|
||||
|
||||
} else if n != len(buf) {
|
||||
t.Fatalf("expected n %d, got %d", len(buf), n)
|
||||
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
for _, expStr := range test.exp {
|
||||
assertRead([]byte(expStr))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -2,35 +2,32 @@ package garage
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Peer describes all information necessary to connect to a given garage node.
|
||||
type Peer struct {
|
||||
// RemotePeer describes all information necessary to connect to a given garage
|
||||
// node.
|
||||
type RemotePeer struct {
|
||||
ID string
|
||||
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)))
|
||||
// LocalPeer describes the configuration of a local garage instance.
|
||||
type LocalPeer struct {
|
||||
RemotePeer
|
||||
|
||||
// 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)
|
||||
AdminPort int
|
||||
}
|
||||
|
||||
pubKey, privKey, err := ed25519.GenerateKey(NewInfiniteReader(input))
|
||||
// GeneratePeerKey generates and returns a public/private key pair for a garage
|
||||
// instance.
|
||||
func GeneratePeerKey() (pubKey, privKey []byte) {
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -38,29 +35,23 @@ func (p Peer) RPCPeerKey() (pubKey, privKey []byte) {
|
||||
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 {
|
||||
func (p RemotePeer) RPCAddr() string {
|
||||
return net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort))
|
||||
}
|
||||
|
||||
// RPCPeerAddr returns the full peer address (e.g. "id@ip:port") of the garage
|
||||
// node for use in communicating over RPC.
|
||||
//
|
||||
// DANGER: See warning on RPCPeerKey.
|
||||
func (p Peer) RPCPeerAddr() string {
|
||||
return fmt.Sprintf("%s@%s", p.RPCPeerID(), p.RPCAddr())
|
||||
func (p RemotePeer) RPCPeerAddr() string {
|
||||
return fmt.Sprintf("%s@%s", p.ID, p.RPCAddr())
|
||||
}
|
||||
|
||||
// S3APIAddr returns the address of the peer's S3 API port.
|
||||
func (p Peer) S3APIAddr() string {
|
||||
func (p RemotePeer) S3APIAddr() string {
|
||||
return net.JoinHostPort(p.IP, strconv.Itoa(p.S3APIPort))
|
||||
}
|
||||
|
||||
// AdminAddr returns the address of the peer's S3 API port.
|
||||
func (p LocalPeer) AdminAddr() string {
|
||||
return net.JoinHostPort(p.IP, strconv.Itoa(p.AdminPort))
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ type GarageTomlData struct {
|
||||
AdminToken string
|
||||
|
||||
RPCAddr string
|
||||
APIAddr string
|
||||
S3APIAddr string
|
||||
AdminAddr string
|
||||
|
||||
BootstrapPeers []string
|
||||
@ -39,7 +39,7 @@ bootstrap_peers = [{{- range .BootstrapPeers }}
|
||||
{{ end -}}]
|
||||
|
||||
[s3_api]
|
||||
api_bind_addr = "{{ .APIAddr }}"
|
||||
api_bind_addr = "{{ .S3APIAddr }}"
|
||||
s3_region = "garage"
|
||||
|
||||
[admin]
|
||||
|
@ -3,8 +3,11 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@ -14,65 +17,69 @@ import (
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
// HostCert contains the certificate and private key files which will need to
|
||||
// be present on a particular host. Each file is PEM encoded.
|
||||
type HostCert struct {
|
||||
CACert string
|
||||
HostKey string
|
||||
HostCert string
|
||||
// ErrInvalidSignature is returned from functions when a signature validation
|
||||
// fails.
|
||||
var ErrInvalidSignature = errors.New("invalid signature")
|
||||
|
||||
// HostCredentials contains the certificate and private key files which will
|
||||
// need to be present on a particular host. Each file is PEM encoded.
|
||||
type HostCredentials struct {
|
||||
CACertPEM string
|
||||
HostKeyPEM string
|
||||
HostCertPEM string
|
||||
}
|
||||
|
||||
// CACert contains the certificate and private files which can be used to create
|
||||
// HostCerts. Each file is PEM encoded.
|
||||
type CACert struct {
|
||||
CACert string
|
||||
CAKey string
|
||||
// CACredentials contains the certificate and private files which can be used to
|
||||
// create and validate HostCredentials. Each file is PEM encoded.
|
||||
type CACredentials struct {
|
||||
CACertPEM string
|
||||
CAKeyPEM string
|
||||
}
|
||||
|
||||
// NewHostCert generates a new key/cert for a nebula host using the CA key
|
||||
// which will be found in the adminFS.
|
||||
func NewHostCert(
|
||||
caCert CACert, hostName string, ip net.IP,
|
||||
// NewHostCredentials generates a new key/cert for a nebula host using the CA
|
||||
// key which will be found in the adminFS.
|
||||
func NewHostCredentials(
|
||||
caCreds CACredentials, hostName string, ip net.IP,
|
||||
) (
|
||||
HostCert, error,
|
||||
HostCredentials, error,
|
||||
) {
|
||||
|
||||
// The logic here is largely based on
|
||||
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
|
||||
|
||||
caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCert.CAKey))
|
||||
caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCreds.CAKeyPEM))
|
||||
if err != nil {
|
||||
return HostCert{}, fmt.Errorf("unmarshaling ca.key: %w", err)
|
||||
return HostCredentials{}, fmt.Errorf("unmarshaling ca.key: %w", err)
|
||||
}
|
||||
|
||||
caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCert.CACert))
|
||||
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.CACertPEM))
|
||||
if err != nil {
|
||||
return HostCert{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
|
||||
return HostCredentials{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
|
||||
}
|
||||
|
||||
issuer, err := caCrt.Sha256Sum()
|
||||
issuer, err := caCert.Sha256Sum()
|
||||
if err != nil {
|
||||
return HostCert{}, fmt.Errorf("getting ca.crt issuer: %w", err)
|
||||
return HostCredentials{}, fmt.Errorf("getting ca.crt issuer: %w", err)
|
||||
}
|
||||
|
||||
expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second)
|
||||
expireAt := caCert.Details.NotAfter.Add(-1 * time.Second)
|
||||
|
||||
subnet := caCrt.Details.Subnets[0]
|
||||
subnet := caCert.Details.Subnets[0]
|
||||
if !subnet.Contains(ip) {
|
||||
return HostCert{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet)
|
||||
return HostCredentials{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet)
|
||||
}
|
||||
|
||||
var hostPub, hostKey []byte
|
||||
{
|
||||
var pubkey, privkey [32]byte
|
||||
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
|
||||
return HostCert{}, fmt.Errorf("reading random bytes to form private key: %w", err)
|
||||
return HostCredentials{}, fmt.Errorf("reading random bytes to form private key: %w", err)
|
||||
}
|
||||
curve25519.ScalarBaseMult(&pubkey, &privkey)
|
||||
hostPub, hostKey = pubkey[:], privkey[:]
|
||||
}
|
||||
|
||||
hostCrt := cert.NebulaCertificate{
|
||||
hostCert := cert.NebulaCertificate{
|
||||
Details: cert.NebulaCertificateDetails{
|
||||
Name: hostName,
|
||||
Ips: []*net.IPNet{{
|
||||
@ -87,31 +94,31 @@ func NewHostCert(
|
||||
},
|
||||
}
|
||||
|
||||
if err := hostCrt.CheckRootConstrains(caCrt); err != nil {
|
||||
return HostCert{}, fmt.Errorf("validating certificate constraints: %w", err)
|
||||
if err := hostCert.CheckRootConstrains(caCert); err != nil {
|
||||
return HostCredentials{}, 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)
|
||||
if err := hostCert.Sign(caKey); err != nil {
|
||||
return HostCredentials{}, fmt.Errorf("signing host cert with ca.key: %w", err)
|
||||
}
|
||||
|
||||
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
|
||||
|
||||
hostCrtPEM, err := hostCrt.MarshalToPEM()
|
||||
hostCertPEM, err := hostCert.MarshalToPEM()
|
||||
if err != nil {
|
||||
return HostCert{}, fmt.Errorf("marshalling host.crt: %w", err)
|
||||
return HostCredentials{}, fmt.Errorf("marshalling host.crt: %w", err)
|
||||
}
|
||||
|
||||
return HostCert{
|
||||
CACert: caCert.CACert,
|
||||
HostKey: string(hostKeyPEM),
|
||||
HostCert: string(hostCrtPEM),
|
||||
return HostCredentials{
|
||||
CACertPEM: caCreds.CACertPEM,
|
||||
HostKeyPEM: string(hostKeyPEM),
|
||||
HostCertPEM: string(hostCertPEM),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewCACert generates a CACert. The domain should be the network's root domain,
|
||||
// NewCACredentials generates a CACredentials. The domain should be the network's root domain,
|
||||
// and is included in the signing certificate's Name field.
|
||||
func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
|
||||
func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
|
||||
|
||||
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
@ -121,7 +128,7 @@ func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
|
||||
now := time.Now()
|
||||
expireAt := now.Add(2 * 365 * 24 * time.Hour)
|
||||
|
||||
caCrt := cert.NebulaCertificate{
|
||||
caCert := cert.NebulaCertificate{
|
||||
Details: cert.NebulaCertificateDetails{
|
||||
Name: fmt.Sprintf("%s cryptic-net root cert", domain),
|
||||
Subnets: []*net.IPNet{subnet},
|
||||
@ -132,19 +139,134 @@ func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
|
||||
},
|
||||
}
|
||||
|
||||
if err := caCrt.Sign(privKey); err != nil {
|
||||
return CACert{}, fmt.Errorf("signing caCrt: %w", err)
|
||||
if err := caCert.Sign(privKey); err != nil {
|
||||
return CACredentials{}, fmt.Errorf("signing caCert: %w", err)
|
||||
}
|
||||
|
||||
caKeyPEM := cert.MarshalEd25519PrivateKey(privKey)
|
||||
|
||||
caCrtPem, err := caCrt.MarshalToPEM()
|
||||
caCertPEM, err := caCert.MarshalToPEM()
|
||||
if err != nil {
|
||||
return CACert{}, fmt.Errorf("marshaling caCrt: %w", err)
|
||||
return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err)
|
||||
}
|
||||
|
||||
return CACert{
|
||||
CACert: string(caCrtPem),
|
||||
CAKey: string(caKeyPEM),
|
||||
return CACredentials{
|
||||
CACertPEM: string(caCertPEM),
|
||||
CAKeyPEM: string(caKeyPEM),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ValidateHostCertPEM checks if the given host certificate was signed by the
|
||||
// given CA certificate, and returns ErrInvalidSignature if validation fails.
|
||||
func ValidateHostCertPEM(caCertPEM, hostCertPEM string) error {
|
||||
|
||||
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCertPEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshaling CA certificate as PEM: %w", err)
|
||||
}
|
||||
|
||||
hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(hostCertPEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshaling host certificate as PEM: %w", err)
|
||||
}
|
||||
|
||||
caPubKey := ed25519.PublicKey(caCert.Details.PublicKey)
|
||||
|
||||
if !hostCert.CheckSignature(caPubKey) {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IPFromHostCertPEM is a convenience function for parsing the IP of a host out
|
||||
// of its nebula cert.
|
||||
func IPFromHostCertPEM(hostCertPEM string) (net.IP, error) {
|
||||
|
||||
hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(hostCertPEM))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshaling host certificate as PEM: %w", err)
|
||||
}
|
||||
|
||||
ips := hostCert.Details.Ips
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("malformed nebula host cert: no IPs")
|
||||
}
|
||||
|
||||
return ips[0].IP, nil
|
||||
}
|
||||
|
||||
// SignAndWrap signs the given bytes using the keyPEM, and writes an
|
||||
// encoded, versioned structure containing the signature and the given bytes.
|
||||
func SignAndWrap(into io.Writer, keyPEM string, b []byte) error {
|
||||
|
||||
key, _, err := cert.UnmarshalEd25519PrivateKey([]byte(keyPEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshaling private key: %w", err)
|
||||
}
|
||||
|
||||
sig, err := key.Sign(rand.Reader, b, crypto.Hash(0))
|
||||
if err != nil {
|
||||
return fmt.Errorf("generating signature: %w", err)
|
||||
}
|
||||
|
||||
if _, err := into.Write([]byte("0")); err != nil {
|
||||
return fmt.Errorf("writing version byte: %w", err)
|
||||
}
|
||||
|
||||
err = pem.Encode(into, &pem.Block{
|
||||
Type: "SIGNATURE",
|
||||
Bytes: sig,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing PEM encoding of signature: %w", err)
|
||||
}
|
||||
|
||||
if _, err := into.Write(b); err != nil {
|
||||
return fmt.Errorf("writing input bytes: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unwrap reads a stream of bytes which was produced by SignAndWrap, and returns
|
||||
// the original inpute to SignAndWrap as well as the signature which was
|
||||
// created. ValidateSignature can be used to validate the signature.
|
||||
func Unwrap(from io.Reader) (b, sig []byte, err error) {
|
||||
|
||||
full, err := io.ReadAll(from)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("reading full input: %w", err)
|
||||
} else if len(full) < 3 {
|
||||
return nil, nil, fmt.Errorf("input too small")
|
||||
} else if full[0] != '0' {
|
||||
return nil, nil, fmt.Errorf("unexpected version byte: %d", full[0])
|
||||
}
|
||||
|
||||
full = full[1:]
|
||||
|
||||
pemBlock, rest := pem.Decode(full)
|
||||
if pemBlock == nil {
|
||||
return nil, nil, fmt.Errorf("PEM-encoded signature could not be decoded")
|
||||
}
|
||||
|
||||
return rest, pemBlock.Bytes, nil
|
||||
}
|
||||
|
||||
// ValidateSignature can be used to validate a signature produced by Unwrap.
|
||||
func ValidateSignature(certPEM string, b, sig []byte) error {
|
||||
|
||||
cert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(certPEM))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshaling certificate as PEM: %w", err)
|
||||
}
|
||||
|
||||
pubKey := ed25519.PublicKey(cert.Details.PublicKey)
|
||||
|
||||
if !ed25519.Verify(pubKey, b, sig) {
|
||||
return ErrInvalidSignature
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
77
entrypoint/src/nebula/nebula_test.go
Normal file
77
entrypoint/src/nebula/nebula_test.go
Normal file
@ -0,0 +1,77 @@
|
||||
package nebula
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
ip net.IP
|
||||
ipNet *net.IPNet
|
||||
caCredsA, caCredsB CACredentials
|
||||
)
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
|
||||
ip, ipNet, err = net.ParseCIDR("192.168.0.1/24")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
caCredsA, err = NewCACredentials("a.example.com", ipNet)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
caCredsB, err = NewCACredentials("b.example.com", ipNet)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateHostCredentials(t *testing.T) {
|
||||
|
||||
hostCreds, err := NewHostCredentials(caCredsA, "foo", ip)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ValidateHostCertPEM(hostCreds.CACertPEM, hostCreds.HostCertPEM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = ValidateHostCertPEM(caCredsB.CACertPEM, hostCreds.HostCertPEM)
|
||||
if !errors.Is(err, ErrInvalidSignature) {
|
||||
t.Fatalf("expected ErrInvalidSignature, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignAndWrap(t *testing.T) {
|
||||
|
||||
b := []byte("foo bar baz")
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
if err := SignAndWrap(buf, caCredsA.CAKeyPEM, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gotB, gotSig, err := Unwrap(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
} else if !bytes.Equal(b, gotB) {
|
||||
t.Fatalf("got %q but expected %q", gotB, b)
|
||||
}
|
||||
|
||||
if err := ValidateSignature(caCredsA.CACertPEM, b, gotSig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := ValidateSignature(caCredsB.CACertPEM, b, gotSig); !errors.Is(err, ErrInvalidSignature) {
|
||||
t.Fatalf("expected ErrInvalidSignature but got %v", err)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user