From fb151865b4e868791933af3795f7877ad2b58db5 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sun, 16 Oct 2022 17:51:51 +0200 Subject: [PATCH] WIP --- go-workspace/src/admin/admin.go | 4 +- go-workspace/src/cmd/entrypoint/admin.go | 164 ++++++++++++++++++++++ go-workspace/src/cmd/entrypoint/daemon.go | 33 ++--- go-workspace/src/nebula/nebula.go | 32 ++--- 4 files changed, 187 insertions(+), 46 deletions(-) diff --git a/go-workspace/src/admin/admin.go b/go-workspace/src/admin/admin.go index 0dbac5a..ad8bdc6 100644 --- a/go-workspace/src/admin/admin.go +++ b/go-workspace/src/admin/admin.go @@ -27,8 +27,8 @@ const ( // 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 { - Domain string `yaml:"domain"` - CIDRs []string `yaml:"cidrs"` + ID string `yaml:"id"` + Domain string `yaml:"domain"` } // Admin is used for accessing all information contained within an admin.tgz. diff --git a/go-workspace/src/cmd/entrypoint/admin.go b/go-workspace/src/cmd/entrypoint/admin.go index 8247777..36b895f 100644 --- a/go-workspace/src/cmd/entrypoint/admin.go +++ b/go-workspace/src/cmd/entrypoint/admin.go @@ -1,14 +1,29 @@ package entrypoint import ( + "context" "cryptic-net/admin" "cryptic-net/bootstrap" "cryptic-net/nebula" + "crypto/rand" + "encoding/hex" "errors" "fmt" + "net" "os" + "strings" + + "github.com/cryptic-io/pmux/pmuxlib" ) +func randStr(l int) string { + b := make([]byte, l) + if _, err := rand.Read(b); err != nil { + panic(err) + } + return hex.EncodeToString(b) +} + func readAdmin(path string) (admin.Admin, error) { if path == "-" { @@ -30,6 +45,155 @@ func readAdmin(path string) (admin.Admin, error) { return admin.FromReader(f) } +var subCmdAdminCreateNetwork = subCmd{ + name: "create-network", + descr: "Creates a new cryptic-net network, outputting the resulting admin.tgz to stdout", + do: func(subCmdCtx subCmdCtx) error { + + flags := subCmdCtx.flagSet(false) + + daemonYmlPath := flags.StringP( + "config-path", "c", "", + "Optional path to a daemon.yml file to load configuration from.", + ) + + dumpConfig := flags.Bool( + "dump-config", false, + "Write the default configuration file to stdout and exit.", + ) + + domain := flags.StringP( + "domain", "d", "", + "Domain name that should be used as the root domain in the network.", + ) + + subnetStr := flags.StringP( + "subnet", "s", "", + "CIDR which denotes the subnet that IPs hosts on the network can be assigned.", + ) + + if err := flags.Parse(subCmdCtx.args); err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + + env := subCmdCtx.env + + if *dumpConfig { + return writeBuiltinDaemonYml(env, os.Stdout) + } + + if *domain == "" || *subnetStr == "" { + return errors.New("--domain and --subnet are required") + } + + *domain = strings.TrimRight(strings.TrimLeft(*domain, "."), ".") + + ip, subnet, err := net.ParseCIDR(*subnetStr) + if err != nil { + return fmt.Errorf("parsing %q as a CIDR: %w", *subnetStr, err) + } + + hostName := "genesis" + + adminCreationParams := admin.CreationParams{ + ID: randStr(32), + Domain: *domain, + } + + garageRPCSecret := randStr(32) + + { + runtimeDirPath := env.RuntimeDirPath + + fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath) + + if err := os.MkdirAll(runtimeDirPath, 0700); err != nil { + return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err) + } + + defer func() { + fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", runtimeDirPath) + if err := os.RemoveAll(runtimeDirPath); err != nil { + fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", runtimeDirPath, err) + } + }() + } + + if err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil { + return fmt.Errorf("merging and writing daemon.yml file: %w", err) + } + + daemon := env.ThisDaemon() + + if len(daemon.Storage.Allocations) < 3 { + return fmt.Errorf("daemon.yml with at least 3 allocations was not provided") + } + + nebulaCACert, err := nebula.NewCACert(*domain, subnet) + if err != nil { + return fmt.Errorf("creating nebula CA cert: %w", err) + } + + nebulaHostCert, err := nebula.NewHostCert(nebulaCACert, hostName, ip) + if err != nil { + return fmt.Errorf("creating nebula cert for host: %w", err) + } + + host := bootstrap.Host{ + Name: hostName, + Nebula: bootstrap.NebulaHost{ + IP: ip.String(), + }, + } + + env.Bootstrap = bootstrap.Bootstrap{ + AdminCreationParams: adminCreationParams, + Hosts: map[string]bootstrap.Host{ + hostName: host, + }, + HostName: hostName, + NebulaHostCert: nebulaHostCert, + GarageRPCSecret: garageRPCSecret, + } + + // this will also write the bootstrap file + if err := mergeDaemonIntoBootstrap(env); err != nil { + return fmt.Errorf("merging daemon.yml into bootstrap data: %w", err) + } + + for key, val := range env.ToMap() { + if err := os.Setenv(key, val); err != nil { + return fmt.Errorf("failed to set %q to %q: %w", key, val, err) + } + } + + garageChildrenPmuxProcConfigs, err := garageChildrenPmuxProcConfigs(env) + if err != nil { + return fmt.Errorf("generating garage children configs: %w", err) + } + + pmuxConfig := pmuxlib.Config{ + Processes: append( + []pmuxlib.ProcessConfig{ + nebulaEntrypointPmuxProcConfig(), + garageApplyLayoutDiffPmuxProcConfig(env), + }, + garageChildrenPmuxProcConfigs..., + ), + } + + ctx, cancel := context.WithCancel(env.Context) + defer cancel() + pmuxDoneCh := make(chan struct{}) + + go func() { + pmuxlib.Run(ctx, pmuxConfig) + close(pmuxDoneCh) + }() + + }, +} + var subCmdAdminMakeBootstrap = subCmd{ name: "make-bootstrap", descr: "Creates a new bootstrap.tgz file for a particular host and writes it to stdout", diff --git a/go-workspace/src/cmd/entrypoint/daemon.go b/go-workspace/src/cmd/entrypoint/daemon.go index b25af6a..128c282 100644 --- a/go-workspace/src/cmd/entrypoint/daemon.go +++ b/go-workspace/src/cmd/entrypoint/daemon.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "os" - "path/filepath" "sync" "time" @@ -179,28 +178,8 @@ var subCmdDaemon = subCmd{ env := subCmdCtx.env - s3Client, err := env.Bootstrap.GlobalBucketS3APIClient() - if err != nil { - return fmt.Errorf("creating client for global bucket: %w", err) - } - - appDirPath := env.AppDirPath - - builtinDaemonYmlPath := filepath.Join(appDirPath, "etc", "daemon.yml") - if *dumpConfig { - - builtinDaemonYml, err := os.ReadFile(builtinDaemonYmlPath) - - if err != nil { - return fmt.Errorf("reading default daemon.yml at %q: %w", builtinDaemonYmlPath, err) - } - - if _, err := os.Stdout.Write(builtinDaemonYml); err != nil { - return fmt.Errorf("writing default daemon.yml to stdout: %w", err) - } - - return nil + return writeBuiltinDaemonYml(env, os.Stdout) } runtimeDirPath := env.RuntimeDirPath @@ -256,7 +235,7 @@ var subCmdDaemon = subCmd{ } if err := writeMergedDaemonYml(env, *daemonYmlPath); err != nil { - return fmt.Errorf("generating daemon.yml file: %w", err) + return fmt.Errorf("merging and writing daemon.yml file: %w", err) } // we update this Host's data using whatever configuration has been @@ -276,6 +255,14 @@ var subCmdDaemon = subCmd{ for { + // create s3Client anew on every loop, in case the topology has + // changed and we should be connecting to a different garage + // endpoint. + s3Client, err := env.Bootstrap.GlobalBucketS3APIClient() + if err != nil { + return fmt.Errorf("creating client for global bucket: %w", err) + } + if err := runDaemonPmuxOnce(env, s3Client); errors.Is(err, context.Canceled) { return nil diff --git a/go-workspace/src/nebula/nebula.go b/go-workspace/src/nebula/nebula.go index eab83e9..3d39ab3 100644 --- a/go-workspace/src/nebula/nebula.go +++ b/go-workspace/src/nebula/nebula.go @@ -14,15 +14,6 @@ import ( "golang.org/x/crypto/curve25519" ) -// TODO this should one day not be hardcoded -var ipCIDRMask = func() net.IPMask { - _, ipNet, err := net.ParseCIDR("10.10.0.0/16") - if err != nil { - panic(err) - } - return ipNet.Mask -}() - // HostCert contains the certificate and private key files which will need to // be present on a particular host. Each file is PEM encoded. type HostCert struct { @@ -41,7 +32,7 @@ type CACert struct { // NewHostCert generates a new key/cert for a nebula host using the CA key // which will be found in the adminFS. func NewHostCert( - caCert CACert, hostName, hostIP string, + caCert CACert, hostName string, ip net.IP, ) ( HostCert, error, ) { @@ -66,14 +57,9 @@ func NewHostCert( expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second) - ip := net.ParseIP(hostIP) - if ip == nil { - return HostCert{}, fmt.Errorf("invalid host ip %q", hostIP) - } - - ipNet := &net.IPNet{ - IP: ip, - Mask: ipCIDRMask, + subnet := caCrt.Details.Subnets[0] + if !subnet.Contains(ip) { + return HostCert{}, fmt.Errorf("invalid ip %q, not contained by network subnet %q", ip, subnet) } var hostPub, hostKey []byte @@ -88,8 +74,11 @@ func NewHostCert( hostCrt := cert.NebulaCertificate{ Details: cert.NebulaCertificateDetails{ - Name: hostName, - Ips: []*net.IPNet{ipNet}, + Name: hostName, + Ips: []*net.IPNet{{ + IP: ip, + Mask: subnet.Mask, + }}, NotBefore: time.Now(), NotAfter: expireAt, PublicKey: hostPub, @@ -122,7 +111,7 @@ func NewHostCert( // NewCACert generates a CACert. The domain should be the network's root domain, // and is included in the signing certificate's Name field. -func NewCACert(domain string) (CACert, error) { +func NewCACert(domain string, subnet *net.IPNet) (CACert, error) { pubKey, privKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { @@ -135,6 +124,7 @@ func NewCACert(domain string) (CACert, error) { caCrt := cert.NebulaCertificate{ Details: cert.NebulaCertificateDetails{ Name: fmt.Sprintf("%s cryptic-net root cert", domain), + Subnets: []*net.IPNet{subnet}, NotBefore: now, NotAfter: expireAt, PublicKey: pubKey,