package entrypoint import ( "bytes" "context" "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" "sync" "time" crypticnet "cryptic-net" "cryptic-net/bootstrap" "cryptic-net/yamlutil" "github.com/cryptic-io/pmux/pmuxlib" "github.com/imdario/mergo" "gopkg.in/yaml.v3" ) // The daemon sub-command deals with starting an actual cryptic-net daemon // process, which is required to be running for most other cryptic-net // functionality. The sub-command does the following: // // * Creates and locks the runtime directory. // // * Creates the data directory and copies the appdir bootstrap file into there, // if it's not already there. // // * Merges the user-provided daemon.yml file with the default, and writes the // result to the runtime dir. // // * Sets up environment variables that all other sub-processes then use, based // on the runtime dir. // // * Dynamically creates the root pmux config and runs pmux. // // * (On exit) cleans up the runtime directory. func writeDaemonYml(userDaemonYmlPath, builtinDaemonYmlPath, runtimeDirPath string) error { var fullDaemonYml map[string]interface{} if err := yamlutil.LoadYamlFile(&fullDaemonYml, builtinDaemonYmlPath); err != nil { return fmt.Errorf("parsing builtin daemon.yml file: %w", err) } if userDaemonYmlPath != "" { var daemonYml map[string]interface{} if err := yamlutil.LoadYamlFile(&daemonYml, userDaemonYmlPath); err != nil { return fmt.Errorf("parsing %q: %w", userDaemonYmlPath, err) } err := mergo.Merge(&fullDaemonYml, daemonYml, mergo.WithOverride) if err != nil { return fmt.Errorf("merging contents of file %q: %w", userDaemonYmlPath, err) } } fullDaemonYmlB, err := yaml.Marshal(fullDaemonYml) if err != nil { return fmt.Errorf("yaml marshaling daemon config: %w", err) } daemonYmlPath := filepath.Join(runtimeDirPath, "daemon.yml") if err := ioutil.WriteFile(daemonYmlPath, fullDaemonYmlB, 0400); err != nil { return fmt.Errorf("writing daemon.yml file to %q: %w", daemonYmlPath, err) } return nil } func writeBootstrapToDataDir(env *crypticnet.Env, r io.Reader) error { path := env.DataDirBootstrapPath() dirPath := filepath.Dir(path) if err := os.MkdirAll(dirPath, 0700); err != nil { return fmt.Errorf("creating directory %q: %w", dirPath, err) } f, err := os.Create(path) if err != nil { return fmt.Errorf("creating file %q: %w", path, err) } _, err = io.Copy(f, r) f.Close() if err != nil { return fmt.Errorf("writing new bootstrap file to %q: %w", path, err) } if err := env.LoadBootstrap(path); err != nil { return fmt.Errorf("loading bootstrap from %q: %w", path, err) } return nil } // creates a new bootstrap file using available information from the network. If // the new bootstrap file is different than the existing one, the existing one // is overwritten, ReloadBootstrap is called on env, true is returned. func reloadBootstrap(env *crypticnet.Env) (bool, error) { buf := new(bytes.Buffer) if err := bootstrap.NewForThisHost(env, buf); err != nil { return false, fmt.Errorf("generating new bootstrap from env: %w", err) } newHash, err := bootstrap.GetHashFromReader(bytes.NewReader(buf.Bytes())) if err != nil { return false, fmt.Errorf("reading hash from new bootstrap file: %w", err) } currHash, err := bootstrap.GetHashFromFS(env.BootstrapFS) if err != nil { return false, fmt.Errorf("reading hash from existing bootstrap fs: %w", err) } if bytes.Equal(newHash, currHash) { return false, nil } if err := writeBootstrapToDataDir(env, buf); err != nil { return false, fmt.Errorf("writing new bootstrap file: %w", err) } return true, nil } // runs a single pmux process for daemon, returning only once the env.Context // has been canceled or bootstrap info has been changed. This will always block // until the spawned pmux has returned. func runDaemonPmuxOnce(env *crypticnet.Env) error { thisHost := env.ThisHost() fmt.Fprintf(os.Stderr, "host name is %q, ip is %q\n", thisHost.Name, thisHost.Nebula.IP) pmuxProcConfigs := []pmuxlib.ProcessConfig{ { Name: "nebula", Cmd: "cryptic-net-main", Args: []string{ "nebula-entrypoint", }, }, { Name: "dnsmasq", Cmd: "bash", Args: []string{ "wait-for-ip", env.ThisHost().Nebula.IP, "bash", "dnsmasq-entrypoint", }, }, } if len(env.ThisDaemon().Storage.Allocations) > 0 { pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{ Name: "garage", Cmd: "bash", Args: []string{ "wait-for-ip", env.ThisHost().Nebula.IP, "cryptic-net-main", "garage-entrypoint", }, // garage can take a while to clean up SigKillWait: (1 * time.Minute) + (10 * time.Second), }) } pmuxConfig := pmuxlib.Config{Processes: pmuxProcConfigs} doneCh := env.Context.Done() var wg sync.WaitGroup defer wg.Wait() ctx, cancel := context.WithCancel(env.Context) defer cancel() wg.Add(1) go func() { defer wg.Done() pmuxlib.Run(ctx, pmuxConfig) }() ticker := time.NewTicker(3 * time.Minute) defer ticker.Stop() for { select { case <-doneCh: return env.Context.Err() case <-ticker.C: fmt.Fprintln(os.Stderr, "checking for changes to bootstrap") if changed, err := reloadBootstrap(env); err != nil { return fmt.Errorf("reloading bootstrap: %w", err) } else if changed { fmt.Fprintln(os.Stderr, "bootstrap info has changed, restarting all processes") return nil } } } } var subCmdDaemon = subCmd{ name: "daemon", descr: "Runs the cryptic-net daemon (Default if no sub-command given)", 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.", ) bootstrapPath := flags.StringP( "bootstrap-path", "b", "", `Path to a bootstrap.tgz file. This only needs to be provided the first time the daemon is started, after that it is ignored. If the cryptic-net binary has a bootstrap built into it then this argument is always optional.`, ) if err := flags.Parse(subCmdCtx.args); err != nil { return fmt.Errorf("parsing flags: %w", err) } env := subCmdCtx.env 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 } 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) } else if err := crypticnet.NewProcLock(runtimeDirPath).WriteLock(); err != nil { return err } // do not defer the cleaning of the runtime directory until the lock has // been obtained, otherwise we might delete the directory out from under // the feet of an already running daemon 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 the bootstrap file is not being stored in the data dir, move it // there and reload the bootstrap info if env.BootstrapPath != env.DataDirBootstrapPath() { path := env.BootstrapPath // If there's no BootstrapPath then no bootstrap file could be // found. In this case we require the user to provide one on the // command-line. if path == "" { if *bootstrapPath == "" { return errors.New("No bootstrap.tgz file could be found, and one is not provided with --bootstrap-path") } path = *bootstrapPath } f, err := os.Open(path) if err != nil { return fmt.Errorf("opening file %q: %w", env.BootstrapPath, err) } err = writeBootstrapToDataDir(env, f) f.Close() if err != nil { return fmt.Errorf("copying bootstrap file from %q: %w", path, err) } } if err := writeDaemonYml(*daemonYmlPath, builtinDaemonYmlPath, runtimeDirPath); err != nil { return fmt.Errorf("generating daemon.yml file: %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) } } for { if err := runDaemonPmuxOnce(env); errors.Is(err, context.Canceled) { return nil } else if err != nil { return fmt.Errorf("running pmux for daemon: %w", err) } } }, }