package main import ( "fmt" "net" "os" "strconv" "strings" "time" "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" ) // RateLimitConfig holds rate limiting configuration type RateLimitConfig struct { PerPeerIntervalMs int `yaml:"per_peer_interval_ms"` // Milliseconds between requests per peer PerPeerBurst int `yaml:"per_peer_burst"` // Number of requests allowed in a burst } // Config holds proxy configuration type Config struct { AllowedSourceSubnets []string `yaml:"allowed_source_subnets"` MetricsAddress string `yaml:"metrics_address"` LogLevel string `yaml:"log_level"` AllowedNodes []string `yaml:"allowed_nodes"` StrictNodeValidation bool `yaml:"strict_node_validation"` ReadTimeout time.Duration `yaml:"read_timeout"` WriteTimeout time.Duration `yaml:"write_timeout"` MaxSSHOutputBytes int64 `yaml:"max_ssh_output_bytes"` RequireProxmoxHostkeys bool `yaml:"require_proxmox_hostkeys"` AllowedPeers []PeerConfig `yaml:"allowed_peers"` AllowIDMappedRoot bool `yaml:"allow_idmapped_root"` AllowedPeerUIDs []uint32 `yaml:"allowed_peer_uids"` AllowedPeerGIDs []uint32 `yaml:"allowed_peer_gids"` AllowedIDMapUsers []string `yaml:"allowed_idmap_users"` RateLimit *RateLimitConfig `yaml:"rate_limit,omitempty"` // HTTP mode configuration HTTPEnabled bool `yaml:"http_enabled"` // Enable HTTP server mode HTTPListenAddr string `yaml:"http_listen_addr"` // Address to listen on (e.g., ":8443") HTTPTLSCertFile string `yaml:"http_tls_cert"` // Path to TLS certificate HTTPTLSKeyFile string `yaml:"http_tls_key"` // Path to TLS private key HTTPAuthToken string `yaml:"http_auth_token"` // Bearer token for authentication } // PeerConfig represents a peer entry with capabilities. type PeerConfig struct { UID uint32 `yaml:"uid"` Capabilities []string `yaml:"capabilities"` } // loadConfig loads configuration from file and environment variables func loadConfig(configPath string) (*Config, error) { cfg := &Config{ AllowIDMappedRoot: true, AllowedIDMapUsers: []string{"root"}, LogLevel: "info", // Default log level ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, MaxSSHOutputBytes: 1 * 1024 * 1024, // 1 MiB } // Try to load config file if it exists if configPath != "" { if _, err := os.Stat(configPath); err == nil { data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } if err := yaml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("failed to parse config file: %w", err) } log.Info(). Str("config_file", configPath). Int("subnet_count", len(cfg.AllowedSourceSubnets)). Msg("Loaded configuration from file") } } // Read timeout override if envReadTimeout := os.Getenv("PULSE_SENSOR_PROXY_READ_TIMEOUT"); envReadTimeout != "" { if parsed, err := time.ParseDuration(strings.TrimSpace(envReadTimeout)); err != nil { log.Warn().Str("value", envReadTimeout).Err(err).Msg("Invalid PULSE_SENSOR_PROXY_READ_TIMEOUT value, ignoring") } else { cfg.ReadTimeout = parsed log.Info().Dur("read_timeout", cfg.ReadTimeout).Msg("Configured read timeout from environment") } } // Write timeout override if envWriteTimeout := os.Getenv("PULSE_SENSOR_PROXY_WRITE_TIMEOUT"); envWriteTimeout != "" { if parsed, err := time.ParseDuration(strings.TrimSpace(envWriteTimeout)); err != nil { log.Warn().Str("value", envWriteTimeout).Err(err).Msg("Invalid PULSE_SENSOR_PROXY_WRITE_TIMEOUT value, ignoring") } else { cfg.WriteTimeout = parsed log.Info().Dur("write_timeout", cfg.WriteTimeout).Msg("Configured write timeout from environment") } } // Append from environment variable if set if envSubnets := os.Getenv("PULSE_SENSOR_PROXY_ALLOWED_SUBNETS"); envSubnets != "" { envList := strings.Split(envSubnets, ",") cfg.AllowedSourceSubnets = append(cfg.AllowedSourceSubnets, envList...) log.Info(). Int("env_subnet_count", len(envList)). Msg("Appended subnets from environment variable") } // Ensure timeouts have sane defaults if cfg.ReadTimeout <= 0 { log.Warn().Dur("configured_value", cfg.ReadTimeout).Msg("Read timeout must be positive; using default 5s") cfg.ReadTimeout = 5 * time.Second } if cfg.WriteTimeout <= 0 { log.Warn().Dur("configured_value", cfg.WriteTimeout).Msg("Write timeout must be positive; using default 10s") cfg.WriteTimeout = 10 * time.Second } if cfg.MaxSSHOutputBytes <= 0 { log.Warn().Int64("configured_value", cfg.MaxSSHOutputBytes).Msg("max_ssh_output_bytes must be positive; using default 1MiB") cfg.MaxSSHOutputBytes = 1 * 1024 * 1024 } // Allow ID-mapped root override if envAllowIDMap := os.Getenv("PULSE_SENSOR_PROXY_ALLOW_IDMAPPED_ROOT"); envAllowIDMap != "" { parsed, err := parseBool(envAllowIDMap) if err != nil { log.Warn(). Str("value", envAllowIDMap). Err(err). Msg("Invalid PULSE_SENSOR_PROXY_ALLOW_IDMAPPED_ROOT value, ignoring") } else { cfg.AllowIDMappedRoot = parsed log.Info(). Bool("allow_idmapped_root", parsed). Msg("Configured allow_idmapped_root from environment variable") } } // Allowed ID map users override if envIDMapUsers := os.Getenv("PULSE_SENSOR_PROXY_ALLOWED_IDMAP_USERS"); envIDMapUsers != "" { envList := splitAndTrim(envIDMapUsers) if len(envList) > 0 { cfg.AllowedIDMapUsers = envList log.Info(). Strs("allowed_idmap_users", cfg.AllowedIDMapUsers). Msg("Configured allowed ID map users from environment") } } // Allowed peer UID overrides if envUIDs := os.Getenv("PULSE_SENSOR_PROXY_ALLOWED_PEER_UIDS"); envUIDs != "" { parsed, err := parseUint32List(envUIDs) if err != nil { log.Warn(). Str("value", envUIDs). Err(err). Msg("Invalid PULSE_SENSOR_PROXY_ALLOWED_PEER_UIDS value, ignoring") } else { cfg.AllowedPeerUIDs = append(cfg.AllowedPeerUIDs, parsed...) log.Info(). Int("env_uid_count", len(parsed)). Msg("Appended allowed peer UIDs from environment") } } // Allowed peer GID overrides if envGIDs := os.Getenv("PULSE_SENSOR_PROXY_ALLOWED_PEER_GIDS"); envGIDs != "" { parsed, err := parseUint32List(envGIDs) if err != nil { log.Warn(). Str("value", envGIDs). Err(err). Msg("Invalid PULSE_SENSOR_PROXY_ALLOWED_PEER_GIDS value, ignoring") } else { cfg.AllowedPeerGIDs = append(cfg.AllowedPeerGIDs, parsed...) log.Info(). Int("env_gid_count", len(parsed)). Msg("Appended allowed peer GIDs from environment") } } // Allowed node overrides if envNodes := os.Getenv("PULSE_SENSOR_PROXY_ALLOWED_NODES"); envNodes != "" { envList := splitAndTrim(envNodes) if len(envList) > 0 { cfg.AllowedNodes = append(cfg.AllowedNodes, envList...) log.Info(). Int("env_allowed_nodes", len(envList)). Msg("Appended allowed nodes from environment") } } // Strict node validation override if envStrict := os.Getenv("PULSE_SENSOR_PROXY_STRICT_NODE_VALIDATION"); envStrict != "" { parsed, err := parseBool(envStrict) if err != nil { log.Warn(). Str("value", envStrict). Err(err). Msg("Invalid PULSE_SENSOR_PROXY_STRICT_NODE_VALIDATION value, ignoring") } else { cfg.StrictNodeValidation = parsed log.Info(). Bool("strict_node_validation", parsed). Msg("Configured strict node validation from environment") } } // SSH output limit override if envMaxSSH := os.Getenv("PULSE_SENSOR_PROXY_MAX_SSH_OUTPUT_BYTES"); envMaxSSH != "" { if parsed, err := strconv.ParseInt(strings.TrimSpace(envMaxSSH), 10, 64); err != nil { log.Warn().Str("value", envMaxSSH).Err(err).Msg("Invalid PULSE_SENSOR_PROXY_MAX_SSH_OUTPUT_BYTES value, ignoring") } else { cfg.MaxSSHOutputBytes = parsed log.Info().Int64("max_ssh_output_bytes", cfg.MaxSSHOutputBytes).Msg("Configured max SSH output bytes from environment") } } // Require Proxmox host keys override if envReq := os.Getenv("PULSE_SENSOR_PROXY_REQUIRE_PROXMOX_HOSTKEYS"); envReq != "" { if parsed, err := parseBool(envReq); err != nil { log.Warn().Str("value", envReq).Err(err).Msg("Invalid PULSE_SENSOR_PROXY_REQUIRE_PROXMOX_HOSTKEYS value, ignoring") } else { cfg.RequireProxmoxHostkeys = parsed log.Info().Bool("require_proxmox_hostkeys", parsed).Msg("Configured Proxmox host key requirement from environment") } } // Metrics address from environment variable if envMetrics := os.Getenv("PULSE_SENSOR_PROXY_METRICS_ADDR"); envMetrics != "" { cfg.MetricsAddress = envMetrics log.Info().Str("metrics_addr", envMetrics).Msg("Metrics address set from environment") } // Default metrics address if not configured if cfg.MetricsAddress == "" { cfg.MetricsAddress = "default" // Will use defaultMetricsAddr } // Parse and validate all subnets if len(cfg.AllowedSourceSubnets) > 0 { normalized, err := parseAllowedSubnets(cfg.AllowedSourceSubnets) if err != nil { return nil, fmt.Errorf("invalid subnet configuration: %w", err) } cfg.AllowedSourceSubnets = normalized log.Info(). Strs("allowed_subnets", cfg.AllowedSourceSubnets). Msg("Validated and normalized subnet configuration") } else { // Auto-detect if no configuration provided detected := detectHostCIDRs() if len(detected) == 0 { log.Warn().Msg("No allowed_source_subnets configured and no host addresses detected") } else { cfg.AllowedSourceSubnets = detected log.Warn(). Strs("auto_detected_subnets", detected). Msg("No allowed_source_subnets configured; using detected host addresses (recommended to configure explicitly)") } } // Log rate limit configuration if provided if cfg.RateLimit != nil { log.Info(). Int("per_peer_interval_ms", cfg.RateLimit.PerPeerIntervalMs). Int("per_peer_burst", cfg.RateLimit.PerPeerBurst). Msg("Rate limit configuration loaded from config file") } // Log level from environment variable if envLogLevel := os.Getenv("PULSE_SENSOR_PROXY_LOG_LEVEL"); envLogLevel != "" { cfg.LogLevel = strings.ToLower(strings.TrimSpace(envLogLevel)) log.Info().Str("log_level", cfg.LogLevel).Msg("Log level set from environment") } // HTTP mode configuration from environment variables if envHTTPEnabled := os.Getenv("PULSE_SENSOR_PROXY_HTTP_ENABLED"); envHTTPEnabled != "" { if parsed, err := parseBool(envHTTPEnabled); err != nil { log.Warn().Str("value", envHTTPEnabled).Err(err).Msg("Invalid PULSE_SENSOR_PROXY_HTTP_ENABLED value, ignoring") } else { cfg.HTTPEnabled = parsed log.Info().Bool("http_enabled", parsed).Msg("HTTP mode enabled from environment") } } if envHTTPAddr := os.Getenv("PULSE_SENSOR_PROXY_HTTP_ADDR"); envHTTPAddr != "" { cfg.HTTPListenAddr = strings.TrimSpace(envHTTPAddr) log.Info().Str("http_addr", cfg.HTTPListenAddr).Msg("HTTP listen address set from environment") } if envHTTPCert := os.Getenv("PULSE_SENSOR_PROXY_HTTP_TLS_CERT"); envHTTPCert != "" { cfg.HTTPTLSCertFile = strings.TrimSpace(envHTTPCert) log.Info().Str("tls_cert", cfg.HTTPTLSCertFile).Msg("HTTP TLS cert path set from environment") } if envHTTPKey := os.Getenv("PULSE_SENSOR_PROXY_HTTP_TLS_KEY"); envHTTPKey != "" { cfg.HTTPTLSKeyFile = strings.TrimSpace(envHTTPKey) log.Info().Str("tls_key", cfg.HTTPTLSKeyFile).Msg("HTTP TLS key path set from environment") } if envHTTPToken := os.Getenv("PULSE_SENSOR_PROXY_HTTP_AUTH_TOKEN"); envHTTPToken != "" { cfg.HTTPAuthToken = strings.TrimSpace(envHTTPToken) log.Info().Msg("HTTP auth token set from environment") } // Validate HTTP configuration if enabled if cfg.HTTPEnabled { if cfg.HTTPListenAddr == "" { cfg.HTTPListenAddr = ":8443" // Default port log.Info().Str("http_addr", cfg.HTTPListenAddr).Msg("Using default HTTP listen address") } if cfg.HTTPAuthToken == "" { return nil, fmt.Errorf("http_enabled=true requires http_auth_token to be configured") } if cfg.HTTPTLSCertFile == "" || cfg.HTTPTLSKeyFile == "" { return nil, fmt.Errorf("http_enabled=true requires both http_tls_cert and http_tls_key") } } return cfg, nil } // parseBool returns boolean value for various truthy/falsy strings func parseBool(raw string) (bool, error) { switch strings.ToLower(strings.TrimSpace(raw)) { case "1", "true", "yes", "on": return true, nil case "0", "false", "no", "off": return false, nil default: return false, fmt.Errorf("invalid boolean value: %s", raw) } } // parseUint32List parses comma-separated uint32 list func parseUint32List(raw string) ([]uint32, error) { var parsed []uint32 parts := splitAndTrim(raw) for _, part := range parts { if part == "" { continue } val, err := strconv.ParseUint(part, 10, 32) if err != nil { return nil, fmt.Errorf("invalid uint32 %q: %w", part, err) } parsed = append(parsed, uint32(val)) } return parsed, nil } // splitAndTrim splits a comma-separated string and trims whitespace func splitAndTrim(raw string) []string { parts := strings.Split(raw, ",") var result []string for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed != "" { result = append(result, trimmed) } } return result } // detectHostCIDRs detects local host IP addresses as /32 (IPv4) or /128 (IPv6) CIDRs func detectHostCIDRs() []string { var cidrs []string ifaces, err := net.Interfaces() if err != nil { log.Warn().Err(err).Msg("Failed to enumerate network interfaces") return cidrs } for _, iface := range ifaces { // Skip down or loopback interfaces if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { continue } addrs, err := iface.Addrs() if err != nil { log.Warn().Str("iface", iface.Name).Err(err).Msg("Address lookup failed") continue } for _, addr := range addrs { ipNet, ok := addr.(*net.IPNet) if !ok { continue } ip := ipNet.IP // Skip loopback and link-local addresses if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { continue } // Add as /32 for IPv4, /128 for IPv6 if ip.To4() != nil { cidrs = append(cidrs, ip.String()+"/32") } else if ip.To16() != nil { cidrs = append(cidrs, ip.String()+"/128") } } } return cidrs } // parseAllowedSubnets validates and normalizes subnet specifications func parseAllowedSubnets(cfg []string) ([]string, error) { seen := make(map[string]struct{}) var normalized []string for _, raw := range cfg { entry := strings.TrimSpace(raw) if entry == "" { continue } // Try parsing as CIDR if _, _, err := net.ParseCIDR(entry); err == nil { if _, exists := seen[entry]; !exists { seen[entry] = struct{}{} normalized = append(normalized, entry) } continue } // Try parsing as single IP if ip := net.ParseIP(entry); ip != nil { norm := entry + "/32" if ip.To4() == nil { norm = entry + "/128" } if _, exists := seen[norm]; !exists { seen[norm] = struct{}{} normalized = append(normalized, norm) } continue } // Invalid format return nil, fmt.Errorf("invalid subnet or address: %s", entry) } return normalized, nil }