package config import ( "crypto/ed25519" "crypto/rand" "crypto/x509" "encoding/pem" "os" "path/filepath" "strings" "time" toml "github.com/pelletier/go-toml" "golang.org/x/crypto/ssh" "banger/internal/model" "banger/internal/paths" "banger/internal/system" ) type fileConfig struct { LogLevel string `toml:"log_level"` WebListenAddr *string `toml:"web_listen_addr"` FirecrackerBin string `toml:"firecracker_bin"` SSHKeyPath string `toml:"ssh_key_path"` DefaultImageName string `toml:"default_image_name"` AutoStopStaleAfter string `toml:"auto_stop_stale_after"` StatsPollInterval string `toml:"stats_poll_interval"` MetricsPoll string `toml:"metrics_poll_interval"` BridgeName string `toml:"bridge_name"` BridgeIP string `toml:"bridge_ip"` CIDR string `toml:"cidr"` TapPoolSize int `toml:"tap_pool_size"` DefaultDNS string `toml:"default_dns"` } func Load(layout paths.Layout) (model.DaemonConfig, error) { cfg := model.DaemonConfig{ LogLevel: "info", WebListenAddr: "127.0.0.1:7777", AutoStopStaleAfter: 0, StatsPollInterval: model.DefaultStatsPollInterval, MetricsPollInterval: model.DefaultMetricsPollInterval, BridgeName: model.DefaultBridgeName, BridgeIP: model.DefaultBridgeIP, CIDR: model.DefaultCIDR, TapPoolSize: 4, DefaultDNS: model.DefaultDNS, DefaultImageName: "default", } var file fileConfig configPath := filepath.Join(layout.ConfigDir, "config.toml") if info, err := os.Stat(configPath); err == nil && !info.IsDir() { data, err := os.ReadFile(configPath) if err != nil { return cfg, err } if err := toml.Unmarshal(data, &file); err != nil { return cfg, err } } else if err != nil && !os.IsNotExist(err) { return cfg, err } if value := strings.TrimSpace(file.LogLevel); value != "" { cfg.LogLevel = value } if file.WebListenAddr != nil { cfg.WebListenAddr = strings.TrimSpace(*file.WebListenAddr) } if value := strings.TrimSpace(file.FirecrackerBin); value != "" { cfg.FirecrackerBin = value } else if path, err := system.LookupExecutable("firecracker"); err == nil { cfg.FirecrackerBin = path } if value := strings.TrimSpace(file.DefaultImageName); value != "" { cfg.DefaultImageName = value } if value := strings.TrimSpace(file.BridgeName); value != "" { cfg.BridgeName = value } if value := strings.TrimSpace(file.BridgeIP); value != "" { cfg.BridgeIP = value } if value := strings.TrimSpace(file.CIDR); value != "" { cfg.CIDR = value } if file.TapPoolSize > 0 { cfg.TapPoolSize = file.TapPoolSize } if value := strings.TrimSpace(file.DefaultDNS); value != "" { cfg.DefaultDNS = value } if value := strings.TrimSpace(file.AutoStopStaleAfter); value != "" { duration, err := time.ParseDuration(value) if err != nil { return cfg, err } cfg.AutoStopStaleAfter = duration } if value := strings.TrimSpace(file.StatsPollInterval); value != "" { duration, err := time.ParseDuration(value) if err != nil { return cfg, err } cfg.StatsPollInterval = duration } if value := strings.TrimSpace(file.MetricsPoll); value != "" { duration, err := time.ParseDuration(value) if err != nil { return cfg, err } cfg.MetricsPollInterval = duration } if value := strings.TrimSpace(os.Getenv("BANGER_LOG_LEVEL")); value != "" { cfg.LogLevel = value } sshKeyPath, err := resolveSSHKeyPath(layout, file.SSHKeyPath) if err != nil { return cfg, err } cfg.SSHKeyPath = sshKeyPath return cfg, nil } func resolveSSHKeyPath(layout paths.Layout, configured string) (string, error) { configured = strings.TrimSpace(configured) if configured != "" { return configured, nil } return ensureDefaultSSHKey(filepath.Join(layout.ConfigDir, "ssh", "id_ed25519")) } func ensureDefaultSSHKey(path string) (string, error) { if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return "", err } if _, err := os.Stat(path); err == nil { if err := ensurePublicKeyFile(path); err != nil { return "", err } return path, nil } else if !os.IsNotExist(err) { return "", err } _, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return "", err } pkcs8, err := x509.MarshalPKCS8PrivateKey(privateKey) if err != nil { return "", err } privatePEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8}) if err := os.WriteFile(path, privatePEM, 0o600); err != nil { return "", err } if err := ensurePublicKeyFile(path); err != nil { return "", err } return path, nil } func ensurePublicKeyFile(privateKeyPath string) error { data, err := os.ReadFile(privateKeyPath) if err != nil { return err } signer, err := ssh.ParsePrivateKey(data) if err != nil { return err } publicKey := ssh.MarshalAuthorizedKey(signer.PublicKey()) return os.WriteFile(privateKeyPath+".pub", publicKey, 0o644) }