Compare commits
2 Commits
b26f4bdd6a
...
7dceb659ef
Author | SHA1 | Date | |
---|---|---|---|
|
7dceb659ef | ||
|
711d568036 |
@ -35,7 +35,7 @@ type CreationParams struct {
|
|||||||
type Admin struct {
|
type Admin struct {
|
||||||
CreationParams CreationParams
|
CreationParams CreationParams
|
||||||
|
|
||||||
NebulaCACert nebula.CACert
|
NebulaCACredentials nebula.CACredentials
|
||||||
|
|
||||||
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.NebulaCACert.CACert, nebulaCertsCACertPath},
|
{&a.NebulaCACredentials.CACertPEM, nebulaCertsCACertPath},
|
||||||
{&a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
|
{&a.NebulaCACredentials.CAKeyPEM, 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.NebulaCACert.CACert, nebulaCertsCACertPath},
|
{a.NebulaCACredentials.CACertPEM, nebulaCertsCACertPath},
|
||||||
{a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
|
{a.NebulaCACredentials.CAKeyPEM, nebulaCertsCAKeyPath},
|
||||||
{a.GarageRPCSecret, garageRPCSecretPath},
|
{a.GarageRPCSecret, garageRPCSecretPath},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ type Bootstrap struct {
|
|||||||
Hosts map[string]Host
|
Hosts map[string]Host
|
||||||
HostName string
|
HostName string
|
||||||
|
|
||||||
NebulaHostCert nebula.HostCert
|
NebulaHostCredentials nebula.HostCredentials
|
||||||
|
|
||||||
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.NebulaHostCert.CACert, nebulaCertsCACertPath},
|
{&b.NebulaHostCredentials.CACertPEM, nebulaCertsCACertPath},
|
||||||
{&b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
|
{&b.NebulaHostCredentials.HostCertPEM, nebulaCertsHostCertPath},
|
||||||
{&b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
|
{&b.NebulaHostCredentials.HostKeyPEM, 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.NebulaHostCert.CACert, nebulaCertsCACertPath},
|
{b.NebulaHostCredentials.CACertPEM, nebulaCertsCACertPath},
|
||||||
{b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
|
{b.NebulaHostCredentials.HostCertPEM, nebulaCertsHostCertPath},
|
||||||
{b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
|
{b.NebulaHostCredentials.HostKeyPEM, nebulaCertsHostKeyPath},
|
||||||
{b.GarageRPCSecret, garageRPCSecretPath},
|
{b.GarageRPCSecret, garageRPCSecretPath},
|
||||||
{b.GarageAdminToken, garageAdminTokenPath},
|
{b.GarageAdminToken, garageAdminTokenPath},
|
||||||
}
|
}
|
||||||
@ -209,19 +209,3 @@ 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
|
|
||||||
}
|
|
||||||
|
@ -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.Peer {
|
func (b Bootstrap) GaragePeers() []garage.RemotePeer {
|
||||||
|
|
||||||
var peers []garage.Peer
|
var peers []garage.RemotePeer
|
||||||
|
|
||||||
for _, host := range b.Hosts {
|
for _, host := range b.Hosts {
|
||||||
|
|
||||||
@ -24,8 +24,9 @@ func (b Bootstrap) GaragePeers() []garage.Peer {
|
|||||||
|
|
||||||
for _, instance := range host.Garage.Instances {
|
for _, instance := range host.Garage.Instances {
|
||||||
|
|
||||||
peer := garage.Peer{
|
peer := garage.RemotePeer{
|
||||||
IP: host.Nebula.IP,
|
ID: instance.ID,
|
||||||
|
IP: host.IP().String(),
|
||||||
RPCPort: instance.RPCPort,
|
RPCPort: instance.RPCPort,
|
||||||
S3APIPort: instance.S3APIPort,
|
S3APIPort: instance.S3APIPort,
|
||||||
}
|
}
|
||||||
@ -50,14 +51,16 @@ 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.Peer {
|
func (b Bootstrap) ChooseGaragePeer() garage.RemotePeer {
|
||||||
|
|
||||||
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.Peer{
|
return garage.RemotePeer{
|
||||||
IP: thisHost.Nebula.IP,
|
ID: inst.ID,
|
||||||
|
IP: thisHost.IP().String(),
|
||||||
RPCPort: inst.RPCPort,
|
RPCPort: inst.RPCPort,
|
||||||
S3APIPort: inst.S3APIPort,
|
S3APIPort: inst.S3APIPort,
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,9 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"cryptic-net/garage"
|
"cryptic-net/garage"
|
||||||
|
"cryptic-net/nebula"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
@ -17,23 +18,32 @@ const (
|
|||||||
garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts"
|
garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PutGarageBoostrapHost places the <hostname>.yml file for the given host into
|
// PutGarageBoostrapHost places the <hostname>.yml.signed file for this host
|
||||||
// garage so that other hosts are able to see relevant configuration for it.
|
// into garage so that other hosts are able to see relevant configuration for
|
||||||
//
|
// it.
|
||||||
// The given client should be for the global bucket.
|
func (b Bootstrap) PutGarageBoostrapHost(ctx context.Context) error {
|
||||||
func PutGarageBoostrapHost(
|
|
||||||
ctx context.Context, client garage.S3APIClient, host Host,
|
host := b.ThisHost()
|
||||||
) error {
|
client := b.GlobalBucketS3APIClient()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
if err := yaml.NewEncoder(buf).Encode(host); err != nil {
|
err = nebula.SignAndWrap(buf, b.NebulaHostCredentials.HostKeyPEM, hostB)
|
||||||
log.Fatalf("yaml encoding host data: %v", err)
|
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()),
|
ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()),
|
||||||
minio.PutObjectOptions{},
|
minio.PutObjectOptions{},
|
||||||
)
|
)
|
||||||
@ -45,15 +55,18 @@ func PutGarageBoostrapHost(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveGarageBootstrapHost removes the <hostname>.yml for the given host from
|
// RemoveGarageBootstrapHost removes the <hostname>.yml.signed for the given
|
||||||
// garage.
|
// host from 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(garageGlobalBucketBootstrapHostsDirPath, hostName+".yml")
|
filePath := filepath.Join(
|
||||||
|
garageGlobalBucketBootstrapHostsDirPath,
|
||||||
|
hostName+".yml.signed",
|
||||||
|
)
|
||||||
|
|
||||||
return client.RemoveObject(
|
return client.RemoveObject(
|
||||||
ctx, garage.GlobalBucket, filePath,
|
ctx, garage.GlobalBucket, filePath,
|
||||||
@ -61,16 +74,17 @@ func RemoveGarageBootstrapHost(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGarageBootstrapHosts loads the <hostname>.yml file for all hosts stored in
|
// GetGarageBootstrapHosts loads the <hostname>.yml.signed file for all hosts
|
||||||
// garage.
|
// stored in garage.
|
||||||
//
|
func (b Bootstrap) GetGarageBootstrapHosts(
|
||||||
// The given client should be for the global bucket.
|
ctx context.Context,
|
||||||
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(
|
||||||
@ -95,15 +109,30 @@ func GetGarageBootstrapHosts(
|
|||||||
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
|
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var host Host
|
hostB, sig, err := nebula.Unwrap(obj)
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package bootstrap
|
package bootstrap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cryptic-net/nebula"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"net"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -16,12 +18,13 @@ 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 {
|
||||||
IP string `yaml:"ip"`
|
CertPEM string `yaml:"crt"`
|
||||||
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"`
|
||||||
}
|
}
|
||||||
@ -40,6 +43,18 @@ 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{}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
nebulaCACert, err := nebula.NewCACert(*domain, subnet)
|
nebulaCACreds, err := nebula.NewCACredentials(*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)
|
||||||
}
|
}
|
||||||
|
|
||||||
nebulaHostCert, err := nebula.NewHostCert(nebulaCACert, *hostName, ip)
|
nebulaHostCreds, err := nebula.NewHostCredentials(nebulaCACreds, *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{
|
||||||
IP: ip.String(),
|
CertPEM: nebulaHostCreds.HostCertPEM,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
HostName: *hostName,
|
HostName: *hostName,
|
||||||
NebulaHostCert: nebulaHostCert,
|
NebulaHostCredentials: nebulaHostCreds,
|
||||||
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,
|
||||||
NebulaCACert: nebulaCACert,
|
NebulaCACredentials: nebulaCACreds,
|
||||||
GarageRPCSecret: hostBootstrap.GarageRPCSecret,
|
GarageRPCSecret: hostBootstrap.GarageRPCSecret,
|
||||||
GarageGlobalBucketS3APICredentials: hostBootstrap.GarageGlobalBucketS3APICredentials,
|
GarageGlobalBucketS3APICredentials: hostBootstrap.GarageGlobalBucketS3APICredentials,
|
||||||
GarageAdminBucketS3APICredentials: garage.NewS3APICredentials(),
|
GarageAdminBucketS3APICredentials: garage.NewS3APICredentials(),
|
||||||
@ -240,6 +240,11 @@ 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.`,
|
||||||
@ -249,13 +254,18 @@ var subCmdAdminMakeBootstrap = subCmd{
|
|||||||
return fmt.Errorf("parsing flags: %w", err)
|
return fmt.Errorf("parsing flags: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if *name == "" || *adminPath == "" {
|
if *name == "" || *ipStr == "" || *adminPath == "" {
|
||||||
return errors.New("--name and --admin-path are required")
|
return errors.New("--name, --ip, and --admin-path are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
hostBootstrap, err := loadHostBootstrap()
|
if err := validateHostName(*name); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("invalid hostname %q: %w", *name, err)
|
||||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(*ipStr)
|
||||||
|
|
||||||
|
if ip == nil {
|
||||||
|
return fmt.Errorf("invalid ip %q", *ipStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
adm, err := readAdmin(*adminPath)
|
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)
|
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := hostBootstrap.GlobalBucketS3APIClient()
|
hostBootstrap, err := loadHostBootstrap()
|
||||||
|
|
||||||
// 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 {
|
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]
|
nebulaHostCreds, err := nebula.NewHostCredentials(adm.NebulaCACredentials, *name, ip)
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -292,10 +286,10 @@ var subCmdAdminMakeBootstrap = subCmd{
|
|||||||
newHostBootstrap := bootstrap.Bootstrap{
|
newHostBootstrap := bootstrap.Bootstrap{
|
||||||
AdminCreationParams: adm.CreationParams,
|
AdminCreationParams: adm.CreationParams,
|
||||||
|
|
||||||
Hosts: hosts,
|
Hosts: hostBootstrap.Hosts,
|
||||||
HostName: *name,
|
HostName: *name,
|
||||||
|
|
||||||
NebulaHostCert: nebulaHostCert,
|
NebulaHostCredentials: nebulaHostCreds,
|
||||||
|
|
||||||
GarageRPCSecret: adm.GarageRPCSecret,
|
GarageRPCSecret: adm.GarageRPCSecret,
|
||||||
GarageAdminToken: randStr(32),
|
GarageAdminToken: randStr(32),
|
||||||
|
@ -12,7 +12,6 @@ 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"
|
||||||
)
|
)
|
||||||
@ -42,16 +41,21 @@ 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,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
newHosts, err := bootstrap.GetGarageBootstrapHosts(ctx, s3Client)
|
thisHost := hostBootstrap.ThisHost()
|
||||||
|
|
||||||
|
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)
|
||||||
@ -66,13 +70,8 @@ func reloadBootstrap(
|
|||||||
return hostBootstrap, false, nil
|
return hostBootstrap, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
newHostBootstrap := hostBootstrap.WithHosts(newHosts)
|
hostBootstrap.Hosts = 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
|
||||||
@ -87,14 +86,6 @@ 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)
|
||||||
@ -143,11 +134,9 @@ 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 bootstrap.PutGarageBoostrapHost(ctx, s3Client, thisHost)
|
return hostBootstrap.PutGarageBoostrapHost(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -194,7 +183,7 @@ func runDaemonPmuxOnce(
|
|||||||
err error
|
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)
|
return bootstrap.Bootstrap{}, fmt.Errorf("reloading bootstrap: %w", err)
|
||||||
|
|
||||||
} else if changed {
|
} else if changed {
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"cryptic-net/bootstrap"
|
"cryptic-net/bootstrap"
|
||||||
"cryptic-net/daemon"
|
"cryptic-net/daemon"
|
||||||
|
"cryptic-net/garage"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -25,7 +26,14 @@ 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,
|
||||||
})
|
})
|
||||||
|
@ -20,19 +20,22 @@ func dnsmasqPmuxProcConfig(
|
|||||||
|
|
||||||
confPath := filepath.Join(envRuntimeDirPath, "dnsmasq.conf")
|
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 {
|
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 {
|
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{
|
confData := dnsmasq.ConfData{
|
||||||
Resolvers: daemonConfig.DNS.Resolvers,
|
Resolvers: daemonConfig.DNS.Resolvers,
|
||||||
Domain: hostBootstrap.AdminCreationParams.Domain,
|
Domain: hostBootstrap.AdminCreationParams.Domain,
|
||||||
IP: hostBootstrap.ThisHost().Nebula.IP,
|
IP: hostBootstrap.ThisHost().IP().String(),
|
||||||
Hosts: hostsSlice,
|
Hosts: hostsSlice,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"cryptic-net/garage"
|
"cryptic-net/garage"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@ -24,7 +23,7 @@ func newGarageAdminClient(
|
|||||||
|
|
||||||
return garage.NewAdminClient(
|
return garage.NewAdminClient(
|
||||||
net.JoinHostPort(
|
net.JoinHostPort(
|
||||||
thisHost.Nebula.IP,
|
thisHost.IP().String(),
|
||||||
strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort),
|
strconv.Itoa(daemonConfig.Storage.Allocations[0].AdminPort),
|
||||||
),
|
),
|
||||||
hostBootstrap.GarageAdminToken,
|
hostBootstrap.GarageAdminToken,
|
||||||
@ -48,7 +47,7 @@ func waitForGarageAndNebula(
|
|||||||
for _, alloc := range allocs {
|
for _, alloc := range allocs {
|
||||||
|
|
||||||
adminAddr := net.JoinHostPort(
|
adminAddr := net.JoinHostPort(
|
||||||
hostBootstrap.ThisHost().Nebula.IP,
|
hostBootstrap.ThisHost().IP().String(),
|
||||||
strconv.Itoa(alloc.AdminPort),
|
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(
|
func garageWriteChildConfig(
|
||||||
hostBootstrap bootstrap.Bootstrap,
|
hostBootstrap bootstrap.Bootstrap,
|
||||||
alloc daemon.ConfigStorageAllocation,
|
alloc daemon.ConfigStorageAllocation,
|
||||||
@ -73,28 +91,17 @@ func garageWriteChildConfig(
|
|||||||
string, error,
|
string, error,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if err := os.MkdirAll(alloc.MetaPath, 0750); err != nil {
|
|
||||||
return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
thisHost := hostBootstrap.ThisHost()
|
thisHost := hostBootstrap.ThisHost()
|
||||||
|
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID
|
||||||
|
|
||||||
peer := garage.Peer{
|
peer := garage.LocalPeer{
|
||||||
IP: thisHost.Nebula.IP,
|
RemotePeer: garage.RemotePeer{
|
||||||
|
ID: id,
|
||||||
|
IP: thisHost.IP().String(),
|
||||||
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(
|
||||||
@ -108,9 +115,9 @@ func garageWriteChildConfig(
|
|||||||
RPCSecret: hostBootstrap.GarageRPCSecret,
|
RPCSecret: hostBootstrap.GarageRPCSecret,
|
||||||
AdminToken: hostBootstrap.GarageAdminToken,
|
AdminToken: hostBootstrap.GarageAdminToken,
|
||||||
|
|
||||||
RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
|
RPCAddr: peer.RPCAddr(),
|
||||||
APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)),
|
S3APIAddr: peer.S3APIAddr(),
|
||||||
AdminAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.AdminPort)),
|
AdminAddr: peer.AdminAddr(),
|
||||||
|
|
||||||
BootstrapPeers: hostBootstrap.GarageRPCPeerAddrs(),
|
BootstrapPeers: hostBootstrap.GarageRPCPeerAddrs(),
|
||||||
})
|
})
|
||||||
@ -224,7 +231,6 @@ 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
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -239,13 +245,9 @@ func garageApplyLayout(
|
|||||||
|
|
||||||
for _, alloc := range allocs {
|
for _, alloc := range allocs {
|
||||||
|
|
||||||
peer := garage.Peer{
|
id := bootstrapGarageHostForAlloc(thisHost, alloc).ID
|
||||||
IP: ip,
|
|
||||||
RPCPort: alloc.RPCPort,
|
|
||||||
S3APIPort: alloc.S3APIPort,
|
|
||||||
}
|
|
||||||
|
|
||||||
clusterLayout[peer.RPCPeerID()] = peerLayout{
|
clusterLayout[id] = peerLayout{
|
||||||
Capacity: alloc.Capacity / 100,
|
Capacity: alloc.Capacity / 100,
|
||||||
Zone: hostName,
|
Zone: hostName,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
|
@ -4,7 +4,6 @@ import (
|
|||||||
"cryptic-net/bootstrap"
|
"cryptic-net/bootstrap"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
@ -23,60 +22,6 @@ 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",
|
||||||
@ -88,9 +33,7 @@ var subCmdHostsList = subCmd{
|
|||||||
return fmt.Errorf("loading host bootstrap: %w", err)
|
return fmt.Errorf("loading host bootstrap: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := hostBootstrap.GlobalBucketS3APIClient()
|
hostsMap, err := hostBootstrap.GetGarageBootstrapHosts(subCmdCtx.ctx)
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -143,7 +86,6 @@ 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,
|
||||||
)
|
)
|
||||||
|
@ -18,8 +18,7 @@ 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 {
|
||||||
|
|
||||||
ipStr := hostBootstrap.ThisHost().Nebula.IP
|
ip := hostBootstrap.ThisHost().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}
|
||||||
@ -52,15 +51,16 @@ func nebulaPmuxProcConfig(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
lighthouseHostIPs = append(lighthouseHostIPs, host.Nebula.IP)
|
ip := host.IP().String()
|
||||||
staticHostMap[host.Nebula.IP] = []string{host.Nebula.PublicAddr}
|
lighthouseHostIPs = append(lighthouseHostIPs, ip)
|
||||||
|
staticHostMap[ip] = []string{host.Nebula.PublicAddr}
|
||||||
}
|
}
|
||||||
|
|
||||||
config := map[string]interface{}{
|
config := map[string]interface{}{
|
||||||
"pki": map[string]string{
|
"pki": map[string]string{
|
||||||
"ca": hostBootstrap.NebulaHostCert.CACert,
|
"ca": hostBootstrap.NebulaHostCredentials.CACertPEM,
|
||||||
"cert": hostBootstrap.NebulaHostCert.HostCert,
|
"cert": hostBootstrap.NebulaHostCredentials.HostCertPEM,
|
||||||
"key": hostBootstrap.NebulaHostCert.HostKey,
|
"key": hostBootstrap.NebulaHostCredentials.HostKeyPEM,
|
||||||
},
|
},
|
||||||
"static_host_map": staticHostMap,
|
"static_host_map": staticHostMap,
|
||||||
"punchy": map[string]bool{
|
"punchy": map[string]bool{
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
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 []bootstrap.Host
|
Hosts []ConfDataHost
|
||||||
}
|
}
|
||||||
|
|
||||||
var confTpl = template.Must(template.New("").Parse(`
|
var confTpl = template.Must(template.New("").Parse(`
|
||||||
|
@ -2,6 +2,15 @@
|
|||||||
// 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.
|
||||||
@ -15,3 +24,81 @@ 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
|
||||||
|
}
|
||||||
|
@ -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 (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"encoding/hex"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Peer describes all information necessary to connect to a given garage node.
|
// RemotePeer describes all information necessary to connect to a given garage
|
||||||
type Peer struct {
|
// node.
|
||||||
|
type RemotePeer struct {
|
||||||
|
ID string
|
||||||
IP string
|
IP string
|
||||||
RPCPort int
|
RPCPort int
|
||||||
S3APIPort int
|
S3APIPort int
|
||||||
}
|
}
|
||||||
|
|
||||||
// RPCPeerKey deterministically generates a public/private keys which can
|
// LocalPeer describes the configuration of a local garage instance.
|
||||||
// be used as a garage node key.
|
type LocalPeer struct {
|
||||||
//
|
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)))
|
|
||||||
|
|
||||||
// Append the length of the input to the input, so that the input "foo"
|
AdminPort int
|
||||||
// doesn't generate the same key as the input "foofoo".
|
}
|
||||||
input = strconv.AppendInt(input, int64(len(input)), 10)
|
|
||||||
|
|
||||||
pubKey, privKey, err := ed25519.GenerateKey(NewInfiniteReader(input))
|
// 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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -38,29 +35,23 @@ func (p Peer) RPCPeerKey() (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 Peer) RPCAddr() string {
|
func (p RemotePeer) 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 {
|
||||||
// DANGER: See warning on RPCPeerKey.
|
return fmt.Sprintf("%s@%s", p.ID, p.RPCAddr())
|
||||||
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 Peer) S3APIAddr() string {
|
func (p RemotePeer) 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))
|
||||||
|
}
|
||||||
|
@ -17,7 +17,7 @@ type GarageTomlData struct {
|
|||||||
AdminToken string
|
AdminToken string
|
||||||
|
|
||||||
RPCAddr string
|
RPCAddr string
|
||||||
APIAddr string
|
S3APIAddr 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 = "{{ .APIAddr }}"
|
api_bind_addr = "{{ .S3APIAddr }}"
|
||||||
s3_region = "garage"
|
s3_region = "garage"
|
||||||
|
|
||||||
[admin]
|
[admin]
|
||||||
|
@ -3,8 +3,11 @@
|
|||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@ -14,65 +17,69 @@ import (
|
|||||||
"golang.org/x/crypto/curve25519"
|
"golang.org/x/crypto/curve25519"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HostCert contains the certificate and private key files which will need to
|
// ErrInvalidSignature is returned from functions when a signature validation
|
||||||
// be present on a particular host. Each file is PEM encoded.
|
// fails.
|
||||||
type HostCert struct {
|
var ErrInvalidSignature = errors.New("invalid signature")
|
||||||
CACert string
|
|
||||||
HostKey string
|
// HostCredentials contains the certificate and private key files which will
|
||||||
HostCert string
|
// 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
|
// CACredentials contains the certificate and private files which can be used to
|
||||||
// HostCerts. Each file is PEM encoded.
|
// create and validate HostCredentials. Each file is PEM encoded.
|
||||||
type CACert struct {
|
type CACredentials struct {
|
||||||
CACert string
|
CACertPEM string
|
||||||
CAKey string
|
CAKeyPEM string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHostCert generates a new key/cert for a nebula host using the CA key
|
// NewHostCredentials generates a new key/cert for a nebula host using the CA
|
||||||
// which will be found in the adminFS.
|
// key which will be found in the adminFS.
|
||||||
func NewHostCert(
|
func NewHostCredentials(
|
||||||
caCert CACert, hostName string, ip net.IP,
|
caCreds CACredentials, hostName string, ip net.IP,
|
||||||
) (
|
) (
|
||||||
HostCert, error,
|
HostCredentials, 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(caCert.CAKey))
|
caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCreds.CAKeyPEM))
|
||||||
if err != nil {
|
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 {
|
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 {
|
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) {
|
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 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 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)
|
curve25519.ScalarBaseMult(&pubkey, &privkey)
|
||||||
hostPub, hostKey = pubkey[:], privkey[:]
|
hostPub, hostKey = pubkey[:], privkey[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
hostCrt := cert.NebulaCertificate{
|
hostCert := cert.NebulaCertificate{
|
||||||
Details: cert.NebulaCertificateDetails{
|
Details: cert.NebulaCertificateDetails{
|
||||||
Name: hostName,
|
Name: hostName,
|
||||||
Ips: []*net.IPNet{{
|
Ips: []*net.IPNet{{
|
||||||
@ -87,31 +94,31 @@ func NewHostCert(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := hostCrt.CheckRootConstrains(caCrt); err != nil {
|
if err := hostCert.CheckRootConstrains(caCert); err != nil {
|
||||||
return HostCert{}, fmt.Errorf("validating certificate constraints: %w", err)
|
return HostCredentials{}, fmt.Errorf("validating certificate constraints: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := hostCrt.Sign(caKey); err != nil {
|
if err := hostCert.Sign(caKey); err != nil {
|
||||||
return HostCert{}, fmt.Errorf("signing host cert with ca.key: %w", err)
|
return HostCredentials{}, fmt.Errorf("signing host cert with ca.key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
|
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
|
||||||
|
|
||||||
hostCrtPEM, err := hostCrt.MarshalToPEM()
|
hostCertPEM, err := hostCert.MarshalToPEM()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return HostCert{}, fmt.Errorf("marshalling host.crt: %w", err)
|
return HostCredentials{}, fmt.Errorf("marshalling host.crt: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return HostCert{
|
return HostCredentials{
|
||||||
CACert: caCert.CACert,
|
CACertPEM: caCreds.CACertPEM,
|
||||||
HostKey: string(hostKeyPEM),
|
HostKeyPEM: string(hostKeyPEM),
|
||||||
HostCert: string(hostCrtPEM),
|
HostCertPEM: string(hostCertPEM),
|
||||||
}, nil
|
}, 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.
|
// 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)
|
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -121,7 +128,7 @@ func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
expireAt := now.Add(2 * 365 * 24 * time.Hour)
|
expireAt := now.Add(2 * 365 * 24 * time.Hour)
|
||||||
|
|
||||||
caCrt := cert.NebulaCertificate{
|
caCert := 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},
|
||||||
@ -132,19 +139,134 @@ func NewCACert(domain string, subnet *net.IPNet) (CACert, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := caCrt.Sign(privKey); err != nil {
|
if err := caCert.Sign(privKey); err != nil {
|
||||||
return CACert{}, fmt.Errorf("signing caCrt: %w", err)
|
return CACredentials{}, fmt.Errorf("signing caCert: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
caKeyPEM := cert.MarshalEd25519PrivateKey(privKey)
|
caKeyPEM := cert.MarshalEd25519PrivateKey(privKey)
|
||||||
|
|
||||||
caCrtPem, err := caCrt.MarshalToPEM()
|
caCertPEM, err := caCert.MarshalToPEM()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return CACert{}, fmt.Errorf("marshaling caCrt: %w", err)
|
return CACredentials{}, fmt.Errorf("marshaling caCert: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return CACert{
|
return CACredentials{
|
||||||
CACert: string(caCrtPem),
|
CACertPEM: string(caCertPEM),
|
||||||
CAKey: string(caKeyPEM),
|
CAKeyPEM: 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
|
||||||
|
}
|
||||||
|
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