isle/go/bootstrap/bootstrap.go
Brian Picciano 8c3e6a2845 Separate Daemon and Network logic into separate packages
In a world where the daemon can manage more than one network, the Daemon
is really responsible only for knowing which networks are currently
joined, creating/joining/leaving networks, and routing incoming RPC
requests to the correct network handler as needed.

The new network package, with its Network interface, inherits most of
the logic that Daemon used to have, leaving Daemon only the parts needed
for the functionality just described. There's a lot of cleanup done here
in order to really nail down the separation of concerns between the two,
especially around directory creation.
2024-09-09 16:34:00 +02:00

165 lines
4.2 KiB
Go

// Package bootstrap deals with the parsing and creation of bootstrap.json
// files. It also contains some helpers which rely on bootstrap data.
package bootstrap
import (
"crypto/sha512"
"encoding/json"
"fmt"
"isle/nebula"
"isle/toolkit"
"maps"
"net/netip"
"path/filepath"
"sort"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
)
// StateDirPath returns the path within the user's state directory where the
// bootstrap file is stored.
func StateDirPath(dataDirPath string) string {
return filepath.Join(dataDirPath, "bootstrap.json")
}
// AppDirPath returns the path within the AppDir where an embedded bootstrap
// file might be found.
func AppDirPath(appDirPath string) string {
return filepath.Join(appDirPath, "share/bootstrap.json")
}
// CreationParams are general parameters used when creating a new network. These
// are available to all hosts within the network via their bootstrap files.
type CreationParams struct {
ID string
Name string
Domain string
}
// NewCreationParams instantiates and returns a CreationParams.
func NewCreationParams(name, domain string) CreationParams {
return CreationParams{
ID: toolkit.RandStr(32),
Name: name,
Domain: domain,
}
}
// Annotate implements the mctx.Annotator interface.
func (p CreationParams) Annotate(aa mctx.Annotations) {
aa["networkID"] = p.ID
aa["networkName"] = p.Name
aa["networkDomain"] = p.Domain
}
// Bootstrap contains all information which is needed by a host daemon to join a
// network on boot.
type Bootstrap struct {
NetworkCreationParams CreationParams
CAPublicCredentials nebula.CAPublicCredentials
PrivateCredentials nebula.HostPrivateCredentials
HostAssigned `json:"-"`
SignedHostAssigned nebula.Signed[HostAssigned] // signed by CA
Hosts map[nebula.HostName]Host
}
// New initializes and returns a new Bootstrap file for a new host.
//
// TODO in the resulting bootstrap only include this host and hosts which are
// necessary for connecting to nebula/garage. Remember to immediately re-poll
// garage for the full hosts list during network joining.
func New(
caCreds nebula.CACredentials,
adminCreationParams CreationParams,
existingHosts map[nebula.HostName]Host,
name nebula.HostName,
ip netip.Addr,
) (
Bootstrap, error,
) {
hostPubCreds, hostPrivCreds, err := nebula.NewHostCredentials(
caCreds, name, ip,
)
if err != nil {
return Bootstrap{}, fmt.Errorf("generating host credentials: %w", err)
}
assigned := HostAssigned{
Name: name,
PublicCredentials: hostPubCreds,
}
signedAssigned, err := nebula.Sign(assigned, caCreds.SigningPrivateKey)
if err != nil {
return Bootstrap{}, fmt.Errorf("signing assigned fields: %w", err)
}
existingHosts = maps.Clone(existingHosts)
existingHosts[name] = Host{
HostAssigned: assigned,
}
return Bootstrap{
NetworkCreationParams: adminCreationParams,
CAPublicCredentials: caCreds.Public,
PrivateCredentials: hostPrivCreds,
HostAssigned: assigned,
SignedHostAssigned: signedAssigned,
Hosts: existingHosts,
}, nil
}
// UnmarshalJSON implements the json.Unmarshaler interface. It will
// automatically populate the HostAssigned field by unwrapping the
// SignedHostAssigned field.
func (b *Bootstrap) UnmarshalJSON(data []byte) error {
type inner Bootstrap
err := json.Unmarshal(data, (*inner)(b))
if err != nil {
return err
}
b.HostAssigned, err = b.SignedHostAssigned.Unwrap(
b.CAPublicCredentials.SigningKey,
)
if err != nil {
return fmt.Errorf("unwrapping HostAssigned: %w", err)
}
return nil
}
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
// HostName isn't found in the Hosts map.
func (b Bootstrap) ThisHost() Host {
host, ok := b.Hosts[b.Name]
if !ok {
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.Name))
}
return host
}
// Hash returns a deterministic hash of the given hosts map.
func HostsHash(hostsMap map[nebula.HostName]Host) ([]byte, error) {
hosts := make([]Host, 0, len(hostsMap))
for _, host := range hostsMap {
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool {
return hosts[i].Name < hosts[j].Name
})
h := sha512.New()
if err := json.NewEncoder(h).Encode(hosts); err != nil {
return nil, err
}
return h.Sum(nil), nil
}