Compare commits

..

No commits in common. "7dceb659ef94ce9edbb93fc015e9e12e9696d525" and "b26f4bdd6ac0c0b3b2ca713562b0c72c3648499c" have entirely different histories.

20 changed files with 449 additions and 558 deletions

View File

@ -35,7 +35,7 @@ type CreationParams struct {
type Admin struct { type Admin struct {
CreationParams CreationParams CreationParams CreationParams
NebulaCACredentials nebula.CACredentials NebulaCACert nebula.CACert
GarageRPCSecret string GarageRPCSecret string
GarageGlobalBucketS3APICredentials garage.S3APICredentials GarageGlobalBucketS3APICredentials garage.S3APICredentials
@ -67,8 +67,8 @@ func FromFS(adminFS fs.FS) (Admin, error) {
into *string into *string
path string path string
}{ }{
{&a.NebulaCACredentials.CACertPEM, nebulaCertsCACertPath}, {&a.NebulaCACert.CACert, nebulaCertsCACertPath},
{&a.NebulaCACredentials.CAKeyPEM, nebulaCertsCAKeyPath}, {&a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
{&a.GarageRPCSecret, garageRPCSecretPath}, {&a.GarageRPCSecret, garageRPCSecretPath},
} }
@ -122,8 +122,8 @@ func (a Admin) WriteTo(into io.Writer) error {
value string value string
path string path string
}{ }{
{a.NebulaCACredentials.CACertPEM, nebulaCertsCACertPath}, {a.NebulaCACert.CACert, nebulaCertsCACertPath},
{a.NebulaCACredentials.CAKeyPEM, nebulaCertsCAKeyPath}, {a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
{a.GarageRPCSecret, garageRPCSecretPath}, {a.GarageRPCSecret, garageRPCSecretPath},
} }

View File

@ -45,7 +45,7 @@ type Bootstrap struct {
Hosts map[string]Host Hosts map[string]Host
HostName string HostName string
NebulaHostCredentials nebula.HostCredentials NebulaHostCert nebula.HostCert
GarageRPCSecret string GarageRPCSecret string
GarageAdminToken string GarageAdminToken string
@ -84,9 +84,9 @@ func FromFS(bootstrapFS fs.FS) (Bootstrap, error) {
path string path string
}{ }{
{&b.HostName, hostNamePath}, {&b.HostName, hostNamePath},
{&b.NebulaHostCredentials.CACertPEM, nebulaCertsCACertPath}, {&b.NebulaHostCert.CACert, nebulaCertsCACertPath},
{&b.NebulaHostCredentials.HostCertPEM, nebulaCertsHostCertPath}, {&b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
{&b.NebulaHostCredentials.HostKeyPEM, nebulaCertsHostKeyPath}, {&b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
{&b.GarageRPCSecret, garageRPCSecretPath}, {&b.GarageRPCSecret, garageRPCSecretPath},
{&b.GarageAdminToken, garageAdminTokenPath}, {&b.GarageAdminToken, garageAdminTokenPath},
} }
@ -165,9 +165,9 @@ func (b Bootstrap) WriteTo(into io.Writer) error {
path string path string
}{ }{
{b.HostName, hostNamePath}, {b.HostName, hostNamePath},
{b.NebulaHostCredentials.CACertPEM, nebulaCertsCACertPath}, {b.NebulaHostCert.CACert, nebulaCertsCACertPath},
{b.NebulaHostCredentials.HostCertPEM, nebulaCertsHostCertPath}, {b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
{b.NebulaHostCredentials.HostKeyPEM, nebulaCertsHostKeyPath}, {b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
{b.GarageRPCSecret, garageRPCSecretPath}, {b.GarageRPCSecret, garageRPCSecretPath},
{b.GarageAdminToken, garageAdminTokenPath}, {b.GarageAdminToken, garageAdminTokenPath},
} }
@ -209,3 +209,19 @@ func HostsHash(hostsMap map[string]Host) ([]byte, error) {
return h.Sum(nil), nil 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
}

View File

@ -12,9 +12,9 @@ const (
) )
// GaragePeers returns a Peer for each known garage instance in the network. // GaragePeers returns a Peer for each known garage instance in the network.
func (b Bootstrap) GaragePeers() []garage.RemotePeer { func (b Bootstrap) GaragePeers() []garage.Peer {
var peers []garage.RemotePeer var peers []garage.Peer
for _, host := range b.Hosts { for _, host := range b.Hosts {
@ -24,9 +24,8 @@ func (b Bootstrap) GaragePeers() []garage.RemotePeer {
for _, instance := range host.Garage.Instances { for _, instance := range host.Garage.Instances {
peer := garage.RemotePeer{ peer := garage.Peer{
ID: instance.ID, IP: host.Nebula.IP,
IP: host.IP().String(),
RPCPort: instance.RPCPort, RPCPort: instance.RPCPort,
S3APIPort: instance.S3APIPort, S3APIPort: instance.S3APIPort,
} }
@ -51,16 +50,14 @@ func (b Bootstrap) GarageRPCPeerAddrs() []string {
// ChooseGaragePeer returns a Peer for a garage instance from the network. It // 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 prefer a garage instance on this particular host, if there is one, but
// will otherwise return a random endpoint. // will otherwise return a random endpoint.
func (b Bootstrap) ChooseGaragePeer() garage.RemotePeer { func (b Bootstrap) ChooseGaragePeer() garage.Peer {
thisHost := b.ThisHost() thisHost := b.ThisHost()
if thisHost.Garage != nil && len(thisHost.Garage.Instances) > 0 { if thisHost.Garage != nil && len(thisHost.Garage.Instances) > 0 {
inst := thisHost.Garage.Instances[0] inst := thisHost.Garage.Instances[0]
return garage.RemotePeer{ return garage.Peer{
ID: inst.ID, IP: thisHost.Nebula.IP,
IP: thisHost.IP().String(),
RPCPort: inst.RPCPort, RPCPort: inst.RPCPort,
S3APIPort: inst.S3APIPort, S3APIPort: inst.S3APIPort,
} }

View File

@ -4,9 +4,8 @@ import (
"bytes" "bytes"
"context" "context"
"cryptic-net/garage" "cryptic-net/garage"
"cryptic-net/nebula"
"fmt" "fmt"
"os" "log"
"path/filepath" "path/filepath"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
@ -18,32 +17,23 @@ const (
garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts" garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts"
) )
// PutGarageBoostrapHost places the <hostname>.yml.signed file for this host // PutGarageBoostrapHost places the <hostname>.yml file for the given host into
// into garage so that other hosts are able to see relevant configuration for // garage so that other hosts are able to see relevant configuration for it.
// it. //
func (b Bootstrap) PutGarageBoostrapHost(ctx context.Context) error { // The given client should be for the global bucket.
func PutGarageBoostrapHost(
host := b.ThisHost() ctx context.Context, client garage.S3APIClient, host Host,
client := b.GlobalBucketS3APIClient() ) error {
hostB, err := yaml.Marshal(host)
if err != nil {
return fmt.Errorf("yaml encoding host data: %w", err)
}
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
err = nebula.SignAndWrap(buf, b.NebulaHostCredentials.HostKeyPEM, hostB) if err := yaml.NewEncoder(buf).Encode(host); err != nil {
if err != nil { log.Fatalf("yaml encoding host data: %v", err)
return fmt.Errorf("signing encoded host data: %w", err)
} }
filePath := filepath.Join( filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, host.Name+".yml")
garageGlobalBucketBootstrapHostsDirPath,
host.Name+".yml.signed",
)
_, err = client.PutObject( _, err := client.PutObject(
ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()), ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()),
minio.PutObjectOptions{}, minio.PutObjectOptions{},
) )
@ -55,18 +45,15 @@ func (b Bootstrap) PutGarageBoostrapHost(ctx context.Context) error {
return nil return nil
} }
// RemoveGarageBootstrapHost removes the <hostname>.yml.signed for the given // RemoveGarageBootstrapHost removes the <hostname>.yml for the given host from
// host from garage. // garage.
// //
// The given client should be for the global bucket. // The given client should be for the global bucket.
func RemoveGarageBootstrapHost( func RemoveGarageBootstrapHost(
ctx context.Context, client garage.S3APIClient, hostName string, ctx context.Context, client garage.S3APIClient, hostName string,
) error { ) error {
filePath := filepath.Join( filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, hostName+".yml")
garageGlobalBucketBootstrapHostsDirPath,
hostName+".yml.signed",
)
return client.RemoveObject( return client.RemoveObject(
ctx, garage.GlobalBucket, filePath, ctx, garage.GlobalBucket, filePath,
@ -74,17 +61,16 @@ func RemoveGarageBootstrapHost(
) )
} }
// GetGarageBootstrapHosts loads the <hostname>.yml.signed file for all hosts // GetGarageBootstrapHosts loads the <hostname>.yml file for all hosts stored in
// stored in garage. // garage.
func (b Bootstrap) GetGarageBootstrapHosts( //
ctx context.Context, // The given client should be for the global bucket.
func GetGarageBootstrapHosts(
ctx context.Context, client garage.S3APIClient,
) ( ) (
map[string]Host, error, map[string]Host, error,
) { ) {
caCertPEM := b.NebulaHostCredentials.CACertPEM
client := b.GlobalBucketS3APIClient()
hosts := map[string]Host{} hosts := map[string]Host{}
objInfoCh := client.ListObjects( objInfoCh := client.ListObjects(
@ -109,30 +95,15 @@ func (b Bootstrap) GetGarageBootstrapHosts(
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err) return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
} }
hostB, sig, err := nebula.Unwrap(obj) var host Host
err = yaml.NewDecoder(obj).Decode(&host)
obj.Close() obj.Close()
if err != nil { 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) 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 hosts[host.Name] = host
} }

View File

@ -1,10 +1,8 @@
package bootstrap package bootstrap
import ( import (
"cryptic-net/nebula"
"fmt" "fmt"
"io/fs" "io/fs"
"net"
"path/filepath" "path/filepath"
"strings" "strings"
@ -18,13 +16,12 @@ const (
// NebulaHost describes the nebula configuration of a Host which is relevant for // NebulaHost describes the nebula configuration of a Host which is relevant for
// other hosts to know. // other hosts to know.
type NebulaHost struct { type NebulaHost struct {
CertPEM string `yaml:"crt"` IP string `yaml:"ip"`
PublicAddr string `yaml:"public_addr,omitempty"` PublicAddr string `yaml:"public_addr,omitempty"`
} }
// GarageHost describes a single garage instance in the GarageHost. // GarageHost describes a single garage instance in the GarageHost.
type GarageHostInstance struct { type GarageHostInstance struct {
ID string `yaml:"id"`
RPCPort int `yaml:"rpc_port"` RPCPort int `yaml:"rpc_port"`
S3APIPort int `yaml:"s3_api_port"` S3APIPort int `yaml:"s3_api_port"`
} }
@ -43,18 +40,6 @@ type Host struct {
Garage *GarageHost `yaml:"garage,omitempty"` 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) { func loadHosts(bootstrapFS fs.FS) (map[string]Host, error) {
hosts := map[string]Host{} hosts := map[string]Host{}

View File

@ -117,12 +117,12 @@ var subCmdAdminCreateNetwork = subCmd{
return fmt.Errorf("daemon config with at least 3 allocations was not provided") return fmt.Errorf("daemon config with at least 3 allocations was not provided")
} }
nebulaCACreds, err := nebula.NewCACredentials(*domain, subnet) nebulaCACert, err := nebula.NewCACert(*domain, subnet)
if err != nil { if err != nil {
return fmt.Errorf("creating nebula CA cert: %w", err) return fmt.Errorf("creating nebula CA cert: %w", err)
} }
nebulaHostCreds, err := nebula.NewHostCredentials(nebulaCACreds, *hostName, ip) nebulaHostCert, err := nebula.NewHostCert(nebulaCACert, *hostName, ip)
if err != nil { if err != nil {
return fmt.Errorf("creating nebula cert for host: %w", err) return fmt.Errorf("creating nebula cert for host: %w", err)
} }
@ -138,12 +138,12 @@ var subCmdAdminCreateNetwork = subCmd{
*hostName: bootstrap.Host{ *hostName: bootstrap.Host{
Name: *hostName, Name: *hostName,
Nebula: bootstrap.NebulaHost{ Nebula: bootstrap.NebulaHost{
CertPEM: nebulaHostCreds.HostCertPEM, IP: ip.String(),
}, },
}, },
}, },
HostName: *hostName, HostName: *hostName,
NebulaHostCredentials: nebulaHostCreds, NebulaHostCert: nebulaHostCert,
GarageRPCSecret: randStr(32), GarageRPCSecret: randStr(32),
GarageAdminToken: randStr(32), GarageAdminToken: randStr(32),
GarageGlobalBucketS3APICredentials: garage.NewS3APICredentials(), GarageGlobalBucketS3APICredentials: garage.NewS3APICredentials(),
@ -213,7 +213,7 @@ var subCmdAdminCreateNetwork = subCmd{
err = admin.Admin{ err = admin.Admin{
CreationParams: adminCreationParams, CreationParams: adminCreationParams,
NebulaCACredentials: nebulaCACreds, NebulaCACert: nebulaCACert,
GarageRPCSecret: hostBootstrap.GarageRPCSecret, GarageRPCSecret: hostBootstrap.GarageRPCSecret,
GarageGlobalBucketS3APICredentials: hostBootstrap.GarageGlobalBucketS3APICredentials, GarageGlobalBucketS3APICredentials: hostBootstrap.GarageGlobalBucketS3APICredentials,
GarageAdminBucketS3APICredentials: garage.NewS3APICredentials(), GarageAdminBucketS3APICredentials: garage.NewS3APICredentials(),
@ -240,11 +240,6 @@ var subCmdAdminMakeBootstrap = subCmd{
"Name of the host to generate bootstrap.tgz for", "Name of the host to generate bootstrap.tgz for",
) )
ipStr := flags.StringP(
"ip", "i", "",
"IP of the new host",
)
adminPath := flags.StringP( adminPath := flags.StringP(
"admin-path", "a", "", "admin-path", "a", "",
`Path to admin.tgz file. If the given path is "-" then stdin is used.`, `Path to admin.tgz file. If the given path is "-" then stdin is used.`,
@ -254,23 +249,8 @@ var subCmdAdminMakeBootstrap = subCmd{
return fmt.Errorf("parsing flags: %w", err) return fmt.Errorf("parsing flags: %w", err)
} }
if *name == "" || *ipStr == "" || *adminPath == "" { if *name == "" || *adminPath == "" {
return errors.New("--name, --ip, and --admin-path are required") return errors.New("--name and --admin-path are required")
}
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)
if err != nil {
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
} }
hostBootstrap, err := loadHostBootstrap() hostBootstrap, err := loadHostBootstrap()
@ -278,7 +258,33 @@ var subCmdAdminMakeBootstrap = subCmd{
return fmt.Errorf("loading host bootstrap: %w", err) return fmt.Errorf("loading host bootstrap: %w", err)
} }
nebulaHostCreds, err := nebula.NewHostCredentials(adm.NebulaCACredentials, *name, ip) adm, err := readAdmin(*adminPath)
if err != nil {
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)
if err != nil {
return fmt.Errorf("retrieving host info from garage: %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)
if err != nil { if err != nil {
return fmt.Errorf("creating new nebula host key/cert: %w", err) return fmt.Errorf("creating new nebula host key/cert: %w", err)
} }
@ -286,10 +292,10 @@ var subCmdAdminMakeBootstrap = subCmd{
newHostBootstrap := bootstrap.Bootstrap{ newHostBootstrap := bootstrap.Bootstrap{
AdminCreationParams: adm.CreationParams, AdminCreationParams: adm.CreationParams,
Hosts: hostBootstrap.Hosts, Hosts: hosts,
HostName: *name, HostName: *name,
NebulaHostCredentials: nebulaHostCreds, NebulaHostCert: nebulaHostCert,
GarageRPCSecret: adm.GarageRPCSecret, GarageRPCSecret: adm.GarageRPCSecret,
GarageAdminToken: randStr(32), GarageAdminToken: randStr(32),

View File

@ -12,6 +12,7 @@ import (
"cryptic-net/bootstrap" "cryptic-net/bootstrap"
"cryptic-net/daemon" "cryptic-net/daemon"
"cryptic-net/garage"
"code.betamike.com/cryptic-io/pmux/pmuxlib" "code.betamike.com/cryptic-io/pmux/pmuxlib"
) )
@ -41,21 +42,16 @@ import (
func reloadBootstrap( func reloadBootstrap(
ctx context.Context, ctx context.Context,
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
s3Client garage.S3APIClient,
) ( ) (
bootstrap.Bootstrap, bool, error, bootstrap.Bootstrap, bool, error,
) { ) {
thisHost := hostBootstrap.ThisHost() newHosts, err := bootstrap.GetGarageBootstrapHosts(ctx, s3Client)
newHosts, err := hostBootstrap.GetGarageBootstrapHosts(ctx)
if err != nil { if err != nil {
return bootstrap.Bootstrap{}, false, fmt.Errorf("getting hosts from garage: %w", err) 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) newHostsHash, err := bootstrap.HostsHash(newHosts)
if err != nil { if err != nil {
return bootstrap.Bootstrap{}, false, fmt.Errorf("calculating hash of new hosts: %w", err) return bootstrap.Bootstrap{}, false, fmt.Errorf("calculating hash of new hosts: %w", err)
@ -70,8 +66,13 @@ func reloadBootstrap(
return hostBootstrap, false, nil return hostBootstrap, false, nil
} }
hostBootstrap.Hosts = newHosts newHostBootstrap := hostBootstrap.WithHosts(newHosts)
return hostBootstrap, true, nil
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
} }
// runs a single pmux process of daemon, returning only once the env.Context has // runs a single pmux process of daemon, returning only once the env.Context has
@ -86,6 +87,14 @@ func runDaemonPmuxOnce(
bootstrap.Bootstrap, error, 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) nebulaPmuxProcConfig, err := nebulaPmuxProcConfig(hostBootstrap, daemonConfig)
if err != nil { if err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf("generating nebula config: %w", err) return bootstrap.Bootstrap{}, fmt.Errorf("generating nebula config: %w", err)
@ -134,9 +143,11 @@ func runDaemonPmuxOnce(
return return
} }
thisHost := hostBootstrap.ThisHost()
err := doOnce(ctx, func(ctx context.Context) error { err := doOnce(ctx, func(ctx context.Context) error {
fmt.Fprintln(os.Stderr, "updating host info in garage") fmt.Fprintln(os.Stderr, "updating host info in garage")
return hostBootstrap.PutGarageBoostrapHost(ctx) return bootstrap.PutGarageBoostrapHost(ctx, s3Client, thisHost)
}) })
if err != nil { if err != nil {
@ -183,7 +194,7 @@ func runDaemonPmuxOnce(
err error err error
) )
if hostBootstrap, changed, err = reloadBootstrap(ctx, hostBootstrap); err != nil { if hostBootstrap, changed, err = reloadBootstrap(ctx, hostBootstrap, s3Client); err != nil {
return bootstrap.Bootstrap{}, fmt.Errorf("reloading bootstrap: %w", err) return bootstrap.Bootstrap{}, fmt.Errorf("reloading bootstrap: %w", err)
} else if changed { } else if changed {

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"cryptic-net/bootstrap" "cryptic-net/bootstrap"
"cryptic-net/daemon" "cryptic-net/daemon"
"cryptic-net/garage"
"fmt" "fmt"
"time" "time"
) )
@ -26,14 +25,7 @@ func mergeDaemonConfigIntoBootstrap(
host.Garage = new(bootstrap.GarageHost) host.Garage = new(bootstrap.GarageHost)
for _, alloc := range allocs { 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{ host.Garage.Instances = append(host.Garage.Instances, bootstrap.GarageHostInstance{
ID: id,
RPCPort: alloc.RPCPort, RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort, S3APIPort: alloc.S3APIPort,
}) })

View File

@ -20,22 +20,19 @@ func dnsmasqPmuxProcConfig(
confPath := filepath.Join(envRuntimeDirPath, "dnsmasq.conf") confPath := filepath.Join(envRuntimeDirPath, "dnsmasq.conf")
hostsSlice := make([]dnsmasq.ConfDataHost, 0, len(hostBootstrap.Hosts)) hostsSlice := make([]bootstrap.Host, 0, len(hostBootstrap.Hosts))
for _, host := range hostBootstrap.Hosts { for _, host := range hostBootstrap.Hosts {
hostsSlice = append(hostsSlice, dnsmasq.ConfDataHost{ hostsSlice = append(hostsSlice, host)
Name: host.Name,
IP: host.IP().String(),
})
} }
sort.Slice(hostsSlice, func(i, j int) bool { sort.Slice(hostsSlice, func(i, j int) bool {
return hostsSlice[i].IP < hostsSlice[j].IP return hostsSlice[i].Nebula.IP < hostsSlice[j].Nebula.IP
}) })
confData := dnsmasq.ConfData{ confData := dnsmasq.ConfData{
Resolvers: daemonConfig.DNS.Resolvers, Resolvers: daemonConfig.DNS.Resolvers,
Domain: hostBootstrap.AdminCreationParams.Domain, Domain: hostBootstrap.AdminCreationParams.Domain,
IP: hostBootstrap.ThisHost().IP().String(), IP: hostBootstrap.ThisHost().Nebula.IP,
Hosts: hostsSlice, Hosts: hostsSlice,
} }

View File

@ -7,6 +7,7 @@ import (
"cryptic-net/garage" "cryptic-net/garage"
"fmt" "fmt"
"net" "net"
"os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -23,7 +24,7 @@ func newGarageAdminClient(
return garage.NewAdminClient( return garage.NewAdminClient(
net.JoinHostPort( net.JoinHostPort(
thisHost.IP().String(), thisHost.Nebula.IP,
strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort), strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort),
), ),
hostBootstrap.GarageAdminToken, hostBootstrap.GarageAdminToken,
@ -47,7 +48,7 @@ func waitForGarageAndNebula(
for _, alloc := range allocs { for _, alloc := range allocs {
adminAddr := net.JoinHostPort( adminAddr := net.JoinHostPort(
hostBootstrap.ThisHost().IP().String(), hostBootstrap.ThisHost().Nebula.IP,
strconv.Itoa(alloc.AdminPort), strconv.Itoa(alloc.AdminPort),
) )
@ -65,25 +66,6 @@ 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( func garageWriteChildConfig(
hostBootstrap bootstrap.Bootstrap, hostBootstrap bootstrap.Bootstrap,
alloc daemon.ConfigStorageAllocation, alloc daemon.ConfigStorageAllocation,
@ -91,17 +73,28 @@ func garageWriteChildConfig(
string, error, string, error,
) { ) {
thisHost := hostBootstrap.ThisHost() if err := os.MkdirAll(alloc.MetaPath, 0750); err != nil {
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err)
}
peer := garage.LocalPeer{ thisHost := hostBootstrap.ThisHost()
RemotePeer: garage.RemotePeer{
ID: id, peer := garage.Peer{
IP: thisHost.IP().String(), IP: thisHost.Nebula.IP,
RPCPort: alloc.RPCPort, RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort, S3APIPort: alloc.S3APIPort,
}, }
AdminPort: alloc.AdminPort,
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)
} }
garageTomlPath := filepath.Join( garageTomlPath := filepath.Join(
@ -115,9 +108,9 @@ func garageWriteChildConfig(
RPCSecret: hostBootstrap.GarageRPCSecret, RPCSecret: hostBootstrap.GarageRPCSecret,
AdminToken: hostBootstrap.GarageAdminToken, AdminToken: hostBootstrap.GarageAdminToken,
RPCAddr: peer.RPCAddr(), RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
S3APIAddr: peer.S3APIAddr(), APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)),
AdminAddr: peer.AdminAddr(), AdminAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.AdminPort)),
BootstrapPeers: hostBootstrap.GarageRPCPeerAddrs(), BootstrapPeers: hostBootstrap.GarageRPCPeerAddrs(),
}) })
@ -231,6 +224,7 @@ func garageApplyLayout(
adminClient = newGarageAdminClient(hostBootstrap, daemonConfig) adminClient = newGarageAdminClient(hostBootstrap, daemonConfig)
thisHost = hostBootstrap.ThisHost() thisHost = hostBootstrap.ThisHost()
hostName = thisHost.Name hostName = thisHost.Name
ip = thisHost.Nebula.IP
allocs = daemonConfig.Storage.Allocations allocs = daemonConfig.Storage.Allocations
) )
@ -245,9 +239,13 @@ func garageApplyLayout(
for _, alloc := range allocs { for _, alloc := range allocs {
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID peer := garage.Peer{
IP: ip,
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
}
clusterLayout[id] = peerLayout{ clusterLayout[peer.RPCPeerID()] = peerLayout{
Capacity: alloc.Capacity / 100, Capacity: alloc.Capacity / 100,
Zone: hostName, Zone: hostName,
Tags: []string{}, Tags: []string{},

View File

@ -4,6 +4,7 @@ import (
"cryptic-net/bootstrap" "cryptic-net/bootstrap"
"errors" "errors"
"fmt" "fmt"
"net"
"os" "os"
"regexp" "regexp"
"sort" "sort"
@ -22,6 +23,60 @@ func validateHostName(name string) error {
return nil 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{ var subCmdHostsList = subCmd{
name: "list", name: "list",
descr: "Lists all hosts in the network, and their IPs", descr: "Lists all hosts in the network, and their IPs",
@ -33,7 +88,9 @@ var subCmdHostsList = subCmd{
return fmt.Errorf("loading host bootstrap: %w", err) return fmt.Errorf("loading host bootstrap: %w", err)
} }
hostsMap, err := hostBootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx) client := hostBootstrap.GlobalBucketS3APIClient()
hostsMap, err := bootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx, client)
if err != nil { if err != nil {
return fmt.Errorf("retrieving hosts from garage: %w", err) return fmt.Errorf("retrieving hosts from garage: %w", err)
} }
@ -86,6 +143,7 @@ var subCmdHosts = subCmd{
descr: "Sub-commands having to do with configuration of hosts in the network", descr: "Sub-commands having to do with configuration of hosts in the network",
do: func(subCmdCtx subCmdCtx) error { do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd( return subCmdCtx.doSubCmd(
subCmdHostsAdd,
subCmdHostsDelete, subCmdHostsDelete,
subCmdHostsList, subCmdHostsList,
) )

View File

@ -18,7 +18,8 @@ import (
// interface has been initialized. // interface has been initialized.
func waitForNebula(ctx context.Context, hostBootstrap bootstrap.Bootstrap) error { func waitForNebula(ctx context.Context, hostBootstrap bootstrap.Bootstrap) error {
ip := hostBootstrap.ThisHost().IP() ipStr := hostBootstrap.ThisHost().Nebula.IP
ip := net.ParseIP(ipStr)
lUdpAddr := &net.UDPAddr{IP: ip, Port: 0} lUdpAddr := &net.UDPAddr{IP: ip, Port: 0}
rUdpAddr := &net.UDPAddr{IP: ip, Port: 45535} rUdpAddr := &net.UDPAddr{IP: ip, Port: 45535}
@ -51,16 +52,15 @@ func nebulaPmuxProcConfig(
continue continue
} }
ip := host.IP().String() lighthouseHostIPs = append(lighthouseHostIPs, host.Nebula.IP)
lighthouseHostIPs = append(lighthouseHostIPs, ip) staticHostMap[host.Nebula.IP] = []string{host.Nebula.PublicAddr}
staticHostMap[ip] = []string{host.Nebula.PublicAddr}
} }
config := map[string]interface{}{ config := map[string]interface{}{
"pki": map[string]string{ "pki": map[string]string{
"ca": hostBootstrap.NebulaHostCredentials.CACertPEM, "ca": hostBootstrap.NebulaHostCert.CACert,
"cert": hostBootstrap.NebulaHostCredentials.HostCertPEM, "cert": hostBootstrap.NebulaHostCert.HostCert,
"key": hostBootstrap.NebulaHostCredentials.HostKeyPEM, "key": hostBootstrap.NebulaHostCert.HostKey,
}, },
"static_host_map": staticHostMap, "static_host_map": staticHostMap,
"punchy": map[string]bool{ "punchy": map[string]bool{

View File

@ -1,23 +1,18 @@
package dnsmasq package dnsmasq
import ( import (
"cryptic-net/bootstrap"
"fmt" "fmt"
"os" "os"
"text/template" "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. // ConfData describes all the data needed to populate a dnsmasq.conf file.
type ConfData struct { type ConfData struct {
Resolvers []string Resolvers []string
Domain string Domain string
IP string IP string
Hosts []ConfDataHost Hosts []bootstrap.Host
} }
var confTpl = template.Must(template.New("").Parse(` var confTpl = template.Must(template.New("").Parse(`

View File

@ -2,15 +2,6 @@
// setting up garage configs, processes, and deployments. // setting up garage configs, processes, and deployments.
package garage package garage
import (
"encoding/hex"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
)
const ( const (
// Region is the region which garage is configured with. // Region is the region which garage is configured with.
@ -24,81 +15,3 @@ const (
// cluster. We currently only support a factor of 3. // cluster. We currently only support a factor of 3.
ReplicationFactor = 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
}

View File

@ -0,0 +1,41 @@
package garage
import "io"
type infiniteReader struct {
b []byte
i int
}
// NewInfiniteReader returns a reader which will produce the given bytes in
// repetition. len(b) must be greater than 0.
func NewInfiniteReader(b []byte) io.Reader {
if len(b) == 0 {
panic("len(b) must be greater than 0")
}
return &infiniteReader{b: b}
}
func (r *infiniteReader) Read(b []byte) (int, error) {
// here, have a puzzle
var n int
for {
n += copy(b[n:], r.b[r.i:])
if r.i > 0 {
n += copy(b[n:], r.b[:r.i])
}
r.i = (r.i + n) % len(r.b)
if n >= len(b) {
return n, nil
}
}
}

View File

@ -0,0 +1,101 @@
package garage
import (
"bytes"
"strconv"
"testing"
)
func TestInfiniteReader(t *testing.T) {
tests := []struct {
in []byte
size int
exp []string
}{
{
in: []byte("a"),
size: 1,
exp: []string{"a"},
},
{
in: []byte("ab"),
size: 1,
exp: []string{"a", "b"},
},
{
in: []byte("ab"),
size: 2,
exp: []string{"ab"},
},
{
in: []byte("ab"),
size: 3,
exp: []string{"aba", "bab"},
},
{
in: []byte("ab"),
size: 4,
exp: []string{"abab"},
},
{
in: []byte("ab"),
size: 5,
exp: []string{"ababa", "babab"},
},
{
in: []byte("abc"),
size: 1,
exp: []string{"a", "b", "c"},
},
{
in: []byte("abc"),
size: 2,
exp: []string{"ab", "ca", "bc"},
},
{
in: []byte("abc"),
size: 3,
exp: []string{"abc"},
},
{
in: []byte("abc"),
size: 4,
exp: []string{"abca", "bcab", "cabc"},
},
{
in: []byte("abc"),
size: 5,
exp: []string{"abcab", "cabca", "bcabc"},
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
r := NewInfiniteReader(test.in)
buf := make([]byte, test.size)
assertRead := func(expBuf []byte) {
n, err := r.Read(buf)
if !bytes.Equal(buf, expBuf) {
t.Fatalf("expected bytes %q, got %q", expBuf, buf)
} else if n != len(buf) {
t.Fatalf("expected n %d, got %d", len(buf), n)
} else if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
for i := 0; i < 3; i++ {
for _, expStr := range test.exp {
assertRead([]byte(expStr))
}
}
})
}
}

View File

@ -2,32 +2,35 @@ package garage
import ( import (
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "encoding/hex"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
) )
// RemotePeer describes all information necessary to connect to a given garage // Peer describes all information necessary to connect to a given garage node.
// node. type Peer struct {
type RemotePeer struct {
ID string
IP string IP string
RPCPort int RPCPort int
S3APIPort int S3APIPort int
} }
// LocalPeer describes the configuration of a local garage instance. // RPCPeerKey deterministically generates a public/private keys which can
type LocalPeer struct { // be used as a garage node key.
RemotePeer //
// 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)))
AdminPort int // 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)
// GeneratePeerKey generates and returns a public/private key pair for a garage pubKey, privKey, err := ed25519.GenerateKey(NewInfiniteReader(input))
// instance.
func GeneratePeerKey() (pubKey, privKey []byte) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -35,23 +38,29 @@ func GeneratePeerKey() (pubKey, privKey []byte) {
return pubKey, privKey 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. // RPCAddr returns the address of the peer's RPC port.
func (p RemotePeer) RPCAddr() string { func (p Peer) RPCAddr() string {
return net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort)) return net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort))
} }
// RPCPeerAddr returns the full peer address (e.g. "id@ip:port") of the garage // RPCPeerAddr returns the full peer address (e.g. "id@ip:port") of the garage
// node for use in communicating over RPC. // node for use in communicating over RPC.
func (p RemotePeer) RPCPeerAddr() string { //
return fmt.Sprintf("%s@%s", p.ID, p.RPCAddr()) // 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. // S3APIAddr returns the address of the peer's S3 API port.
func (p RemotePeer) S3APIAddr() string { func (p Peer) S3APIAddr() string {
return net.JoinHostPort(p.IP, strconv.Itoa(p.S3APIPort)) 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))
}

View File

@ -17,7 +17,7 @@ type GarageTomlData struct {
AdminToken string AdminToken string
RPCAddr string RPCAddr string
S3APIAddr string APIAddr string
AdminAddr string AdminAddr string
BootstrapPeers []string BootstrapPeers []string
@ -39,7 +39,7 @@ bootstrap_peers = [{{- range .BootstrapPeers }}
{{ end -}}] {{ end -}}]
[s3_api] [s3_api]
api_bind_addr = "{{ .S3APIAddr }}" api_bind_addr = "{{ .APIAddr }}"
s3_region = "garage" s3_region = "garage"
[admin] [admin]

View File

@ -3,11 +3,8 @@
package nebula package nebula
import ( import (
"crypto"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"encoding/pem"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -17,69 +14,65 @@ import (
"golang.org/x/crypto/curve25519" "golang.org/x/crypto/curve25519"
) )
// ErrInvalidSignature is returned from functions when a signature validation // HostCert contains the certificate and private key files which will need to
// fails. // be present on a particular host. Each file is PEM encoded.
var ErrInvalidSignature = errors.New("invalid signature") type HostCert struct {
CACert string
// HostCredentials contains the certificate and private key files which will HostKey string
// need to be present on a particular host. Each file is PEM encoded. HostCert string
type HostCredentials struct {
CACertPEM string
HostKeyPEM string
HostCertPEM string
} }
// CACredentials contains the certificate and private files which can be used to // CACert contains the certificate and private files which can be used to create
// create and validate HostCredentials. Each file is PEM encoded. // HostCerts. Each file is PEM encoded.
type CACredentials struct { type CACert struct {
CACertPEM string CACert string
CAKeyPEM string CAKey string
} }
// NewHostCredentials generates a new key/cert for a nebula host using the CA // NewHostCert generates a new key/cert for a nebula host using the CA key
// key which will be found in the adminFS. // which will be found in the adminFS.
func NewHostCredentials( func NewHostCert(
caCreds CACredentials, hostName string, ip net.IP, caCert CACert, hostName string, ip net.IP,
) ( ) (
HostCredentials, error, HostCert, error,
) { ) {
// The logic here is largely based on // The logic here is largely based on
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go // https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCreds.CAKeyPEM)) caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCert.CAKey))
if err != nil { if err != nil {
return HostCredentials{}, fmt.Errorf("unmarshaling ca.key: %w", err) return HostCert{}, fmt.Errorf("unmarshaling ca.key: %w", err)
} }
caCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCreds.CACertPEM)) caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCert.CACert))
if err != nil { if err != nil {
return HostCredentials{}, fmt.Errorf("unmarshaling ca.crt: %w", err) return HostCert{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
} }
issuer, err := caCert.Sha256Sum() issuer, err := caCrt.Sha256Sum()
if err != nil { if err != nil {
return HostCredentials{}, fmt.Errorf("getting ca.crt issuer: %w", err) return HostCert{}, fmt.Errorf("getting ca.crt issuer: %w", err)
} }
expireAt := caCert.Details.NotAfter.Add(-1 * time.Second) expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second)
subnet := caCert.Details.Subnets[0] subnet := caCrt.Details.Subnets[0]
if !subnet.Contains(ip) { if !subnet.Contains(ip) {
return HostCredentials{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet) return HostCert{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet)
} }
var hostPub, hostKey []byte var hostPub, hostKey []byte
{ {
var pubkey, privkey [32]byte var pubkey, privkey [32]byte
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil { if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
return HostCredentials{}, fmt.Errorf("reading random bytes to form private key: %w", err) return HostCert{}, fmt.Errorf("reading random bytes to form private key: %w", err)
} }
curve25519.ScalarBaseMult(&pubkey, &privkey) curve25519.ScalarBaseMult(&pubkey, &privkey)
hostPub, hostKey = pubkey[:], privkey[:] hostPub, hostKey = pubkey[:], privkey[:]
} }
hostCert := cert.NebulaCertificate{ hostCrt := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{ Details: cert.NebulaCertificateDetails{
Name: hostName, Name: hostName,
Ips: []*net.IPNet{{ Ips: []*net.IPNet{{
@ -94,31 +87,31 @@ func NewHostCredentials(
}, },
} }
if err := hostCert.CheckRootConstrains(caCert); err != nil { if err := hostCrt.CheckRootConstrains(caCrt); err != nil {
return HostCredentials{}, fmt.Errorf("validating certificate constraints: %w", err) return HostCert{}, fmt.Errorf("validating certificate constraints: %w", err)
} }
if err := hostCert.Sign(caKey); err != nil { if err := hostCrt.Sign(caKey); err != nil {
return HostCredentials{}, fmt.Errorf("signing host cert with ca.key: %w", err) return HostCert{}, fmt.Errorf("signing host cert with ca.key: %w", err)
} }
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey) hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
hostCertPEM, err := hostCert.MarshalToPEM() hostCrtPEM, err := hostCrt.MarshalToPEM()
if err != nil { if err != nil {
return HostCredentials{}, fmt.Errorf("marshalling host.crt: %w", err) return HostCert{}, fmt.Errorf("marshalling host.crt: %w", err)
} }
return HostCredentials{ return HostCert{
CACertPEM: caCreds.CACertPEM, CACert: caCert.CACert,
HostKeyPEM: string(hostKeyPEM), HostKey: string(hostKeyPEM),
HostCertPEM: string(hostCertPEM), HostCert: string(hostCrtPEM),
}, nil }, nil
} }
// NewCACredentials generates a CACredentials. The domain should be the network's root domain, // NewCACert generates a CACert. The domain should be the network's root domain,
// and is included in the signing certificate's Name field. // and is included in the signing certificate's Name field.
func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) { func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil { if err != nil {
@ -128,7 +121,7 @@ func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
now := time.Now() now := time.Now()
expireAt := now.Add(2 * 365 * 24 * time.Hour) expireAt := now.Add(2 * 365 * 24 * time.Hour)
caCert := cert.NebulaCertificate{ caCrt := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{ Details: cert.NebulaCertificateDetails{
Name: fmt.Sprintf("%s cryptic-net root cert", domain), Name: fmt.Sprintf("%s cryptic-net root cert", domain),
Subnets: []*net.IPNet{subnet}, Subnets: []*net.IPNet{subnet},
@ -139,134 +132,19 @@ func NewCACredentials(domain string, subnet *net.IPNet) (CACredentials, error) {
}, },
} }
if err := caCert.Sign(privKey); err != nil { if err := caCrt.Sign(privKey); err != nil {
return CACredentials{}, fmt.Errorf("signing caCert: %w", err) return CACert{}, fmt.Errorf("signing caCrt: %w", err)
} }
caKeyPEM := cert.MarshalEd25519PrivateKey(privKey) caKeyPEM := cert.MarshalEd25519PrivateKey(privKey)
caCertPEM, err := caCert.MarshalToPEM() caCrtPem, err := caCrt.MarshalToPEM()
if err != nil { if err != nil {
return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err) return CACert{}, fmt.Errorf("marshaling caCrt: %w", err)
} }
return CACredentials{ return CACert{
CACertPEM: string(caCertPEM), CACert: string(caCrtPem),
CAKeyPEM: string(caKeyPEM), CAKey: string(caKeyPEM),
}, nil }, 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
}

View File

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