package config import ( "encoding/json" "errors" "flag" "os" "path/filepath" "strconv" "strings" "time" ) const MaxMeshProductionObservationSinkCapacity = 10000 type Config struct { BackendURL string ClusterID string ClusterAuthorityPublicKey string ClusterAuthorityFingerprint string JoinToken string NodeName string StateDir string WorkloadSupervisionEnabled bool WebIngressRuntimeEnabled bool WebIngressSigningPrivateKey string WebIngressSigningKeyID string WebIngressTrustedKeysJSON string WebIngressRuntimeServiceClasses string HeartbeatInterval time.Duration EnrollmentPollInterval time.Duration EnrollmentPollTimeout time.Duration MeshSyntheticRuntimeEnabled bool MeshProductionForwardingEnabled bool VPNFabricSessionTransportEnabled bool MeshQUICFabricEnabled bool MeshQUICFabricListenAddr string VPNFabricSessionStreamShards int VPNFabricQUICMaxStreamsPerConn int VPNFabricQUICIdleTTL time.Duration MeshProductionObservationSinkCapacity int MeshListenAddr string MeshListenPortMode string MeshListenAutoPortStart int MeshListenAutoPortEnd int MeshAdvertiseEndpoint string MeshAdvertiseEndpointsJSON string FabricRegistryRecordsJSON string MeshAdvertiseTransport string MeshConnectivityMode string MeshNATType string MeshLocalSegmentID string MeshNATGroupID string MeshSTUNReflexiveEndpoint string MeshSTUNServer string MeshRelayNodeID string MeshRelayEndpoint string MeshRegion string MeshSyntheticConfigPath string MeshPeerEndpointsJSON string MeshSyntheticRoutesJSON string RemoteWorkspaceRealAdapterEnabled bool RemoteWorkspaceRealAdapterCommand string RemoteWorkspaceRealAdapterArgsJSON string RemoteWorkspaceRealAdapterWorkDir string } func Load(args []string, env map[string]string) (Config, error) { if env == nil { env = readEnv() } defaultStateDir := filepath.Join(".", ".rap-node-agent") fs := flag.NewFlagSet("rap-node-agent", flag.ContinueOnError) cfg := Config{} fs.StringVar(&cfg.BackendURL, "backend-url", getEnv(env, "RAP_BACKEND_URL", "http://127.0.0.1:8080/api/v1"), "Backend API base URL.") fs.StringVar(&cfg.ClusterID, "cluster-id", getEnv(env, "RAP_CLUSTER_ID", ""), "Cluster ID.") fs.StringVar(&cfg.ClusterAuthorityPublicKey, "cluster-authority-public-key", getEnv(env, "RAP_CLUSTER_AUTHORITY_PUBLIC_KEY", ""), "Pinned cluster authority Ed25519 public key.") fs.StringVar(&cfg.ClusterAuthorityFingerprint, "cluster-authority-fingerprint", getEnv(env, "RAP_CLUSTER_AUTHORITY_FINGERPRINT", ""), "Pinned cluster authority key fingerprint.") fs.StringVar(&cfg.JoinToken, "join-token", getEnv(env, "RAP_JOIN_TOKEN", ""), "Short-lived node join token.") fs.StringVar(&cfg.NodeName, "node-name", getEnv(env, "RAP_NODE_NAME", hostnameOrDefault()), "Node display name.") fs.StringVar(&cfg.StateDir, "state-dir", getEnv(env, "RAP_NODE_STATE_DIR", defaultStateDir), "Local node-agent state directory.") fs.BoolVar(&cfg.WorkloadSupervisionEnabled, "workload-supervision-enabled", getEnvBool(env, "RAP_WORKLOAD_SUPERVISION_ENABLED", false), "Enable desired workload polling and status reporting. Disabled by default while service runtime is not implemented.") fs.BoolVar(&cfg.WebIngressRuntimeEnabled, "web-ingress-runtime-enabled", getEnvBool(env, "RAP_WEB_INGRESS_RUNTIME_ENABLED", false), "Enable the future real 80/443 web ingress listener runtime. Disabled by default; contract probe remains safe without it.") fs.StringVar(&cfg.WebIngressSigningPrivateKey, "web-ingress-signing-private-key", getEnv(env, "RAP_WEB_INGRESS_SIGNING_PRIVATE_KEY", ""), "Base64 Ed25519 private key used to sign web ingress fabric envelopes. Empty keeps signing disabled.") fs.StringVar(&cfg.WebIngressSigningKeyID, "web-ingress-signing-key-id", getEnv(env, "RAP_WEB_INGRESS_SIGNING_KEY_ID", ""), "Optional key id for web ingress envelope signatures.") fs.StringVar(&cfg.WebIngressTrustedKeysJSON, "web-ingress-trusted-keys-json", getEnv(env, "RAP_WEB_INGRESS_TRUSTED_KEYS_JSON", ""), "JSON map or array of trusted Ed25519 public keys for web ingress runtime receiver.") fs.StringVar(&cfg.WebIngressRuntimeServiceClasses, "web-ingress-runtime-service-classes", getEnv(env, "RAP_WEB_INGRESS_RUNTIME_SERVICE_CLASSES", ""), "Optional comma-separated allow-list of web ingress runtime service classes accepted by this node.") fs.BoolVar(&cfg.MeshSyntheticRuntimeEnabled, "mesh-synthetic-runtime-enabled", getEnvBool(env, "RAP_MESH_SYNTHETIC_RUNTIME_ENABLED", false), "Enable C17A synthetic fabric probe runtime. Disabled by default.") fs.BoolVar(&cfg.MeshProductionForwardingEnabled, "mesh-production-forwarding-enabled", getEnvBool(env, "RAP_MESH_PRODUCTION_FORWARDING_ENABLED", false), "Enable production fabric-control direct next-hop forwarding gate. Disabled by default.") fs.BoolVar(&cfg.VPNFabricSessionTransportEnabled, "vpn-fabric-session-transport-enabled", getEnvBool(env, "RAP_VPN_FABRIC_SESSION_TRANSPORT_ENABLED", false), "Route VPN packet transport over persistent fabric session when explicitly enabled. Disabled by default.") fs.BoolVar(&cfg.MeshQUICFabricEnabled, "mesh-quic-fabric-enabled", getEnvBool(env, "RAP_MESH_QUIC_FABRIC_ENABLED", false), "Enable QUIC/UDP fabric listener. Disabled by default.") fs.StringVar(&cfg.MeshQUICFabricListenAddr, "mesh-quic-fabric-listen-addr", getEnv(env, "RAP_MESH_QUIC_FABRIC_LISTEN_ADDR", ""), "Listen address for QUIC/UDP fabric endpoint, for example :19443.") fs.IntVar(&cfg.VPNFabricSessionStreamShards, "vpn-fabric-session-stream-shards", getEnvInt(env, "RAP_VPN_FABRIC_SESSION_STREAM_SHARDS", 4), "VPN fabric-session stream shards per traffic class.") fs.IntVar(&cfg.VPNFabricQUICMaxStreamsPerConn, "vpn-fabric-quic-max-streams-per-conn", getEnvInt(env, "RAP_VPN_FABRIC_QUIC_MAX_STREAMS_PER_CONN", 64), "Maximum logical fabric-session streams per cached VPN QUIC carrier connection.") fs.DurationVar(&cfg.VPNFabricQUICIdleTTL, "vpn-fabric-quic-idle-ttl", time.Duration(getEnvInt(env, "RAP_VPN_FABRIC_QUIC_IDLE_TTL_SECONDS", 300))*time.Second, "Idle TTL for cached VPN QUIC carrier connections.") fs.IntVar(&cfg.MeshProductionObservationSinkCapacity, "mesh-production-observation-sink-capacity", getEnvSignedInt(env, "RAP_MESH_PRODUCTION_OBSERVATION_SINK_CAPACITY", 0), "Bounded local metadata-only production envelope observation sink capacity. Disabled when 0.") fs.StringVar(&cfg.MeshListenAddr, "mesh-listen-addr", getEnv(env, "RAP_MESH_LISTEN_ADDR", ""), "Listen address for disabled-by-default historical synthetic mesh HTTP endpoint.") fs.StringVar(&cfg.MeshListenPortMode, "mesh-listen-port-mode", getEnv(env, "RAP_MESH_LISTEN_PORT_MODE", "manual"), "Mesh listen port behavior: manual, auto, or disabled.") fs.IntVar(&cfg.MeshListenAutoPortStart, "mesh-listen-auto-port-start", getEnvInt(env, "RAP_MESH_LISTEN_AUTO_PORT_START", 19131), "First port used when mesh listen port mode is auto.") fs.IntVar(&cfg.MeshListenAutoPortEnd, "mesh-listen-auto-port-end", getEnvInt(env, "RAP_MESH_LISTEN_AUTO_PORT_END", 19231), "Last port used when mesh listen port mode is auto.") fs.StringVar(&cfg.MeshAdvertiseEndpoint, "mesh-advertise-endpoint", getEnv(env, "RAP_MESH_ADVERTISE_ENDPOINT", ""), "Advertised mesh endpoint reported to the Control Plane. Empty disables endpoint reporting.") fs.StringVar(&cfg.MeshAdvertiseEndpointsJSON, "mesh-advertise-endpoints-json", getEnv(env, "RAP_MESH_ADVERTISE_ENDPOINTS_JSON", ""), "JSON array of advertised mesh endpoint candidates, including private/corporate endpoints.") fs.StringVar(&cfg.FabricRegistryRecordsJSON, "fabric-registry-records-json", getEnv(env, "RAP_FABRIC_REGISTRY_RECORDS_JSON", ""), "JSON array of signed QUIC-only fabric registry gossip records used as bootstrap discovery seeds.") fs.StringVar(&cfg.MeshAdvertiseTransport, "mesh-advertise-transport", getEnv(env, "RAP_MESH_ADVERTISE_TRANSPORT", "quic"), "Transport label for the advertised mesh endpoint.") fs.StringVar(&cfg.MeshConnectivityMode, "mesh-connectivity-mode", getEnv(env, "RAP_MESH_CONNECTIVITY_MODE", "direct"), "Connectivity mode reported with the advertised mesh endpoint.") fs.StringVar(&cfg.MeshNATType, "mesh-nat-type", getEnv(env, "RAP_MESH_NAT_TYPE", "unknown"), "NAT type hint reported with the advertised mesh endpoint.") fs.StringVar(&cfg.MeshLocalSegmentID, "mesh-local-segment-id", getEnv(env, "RAP_MESH_LOCAL_SEGMENT_ID", ""), "Optional local LAN/site segment ID advertised with QUIC endpoint candidates.") fs.StringVar(&cfg.MeshNATGroupID, "mesh-nat-group-id", getEnv(env, "RAP_MESH_NAT_GROUP_ID", ""), "Optional NAT group ID advertised with QUIC endpoint candidates.") fs.StringVar(&cfg.MeshSTUNReflexiveEndpoint, "mesh-stun-reflexive-endpoint", getEnv(env, "RAP_MESH_STUN_REFLEXIVE_ENDPOINT", ""), "Optional STUN-discovered reflexive QUIC endpoint, for example quic://203.0.113.10:19443.") fs.StringVar(&cfg.MeshSTUNServer, "mesh-stun-server", getEnv(env, "RAP_MESH_STUN_SERVER", ""), "Optional STUN server name used to discover the reflexive endpoint.") fs.StringVar(&cfg.MeshRelayNodeID, "mesh-relay-node-id", getEnv(env, "RAP_MESH_RELAY_NODE_ID", ""), "Optional relay node ID for relay-required QUIC fallback candidates.") fs.StringVar(&cfg.MeshRelayEndpoint, "mesh-relay-endpoint", getEnv(env, "RAP_MESH_RELAY_ENDPOINT", ""), "Optional relay QUIC endpoint for relay-required fallback candidates.") fs.StringVar(&cfg.MeshRegion, "mesh-region", getEnv(env, "RAP_MESH_REGION", ""), "Optional region/site hint for the advertised mesh endpoint.") fs.StringVar(&cfg.MeshSyntheticConfigPath, "mesh-synthetic-config", getEnv(env, "RAP_MESH_SYNTHETIC_CONFIG", ""), "Path to scoped synthetic mesh config snapshot. Preferred over debug JSON env.") fs.StringVar(&cfg.MeshPeerEndpointsJSON, "mesh-peer-endpoints-json", getEnv(env, "RAP_MESH_PEER_ENDPOINTS_JSON", ""), "JSON object mapping peer node_id to synthetic mesh endpoint URL.") fs.StringVar(&cfg.MeshSyntheticRoutesJSON, "mesh-synthetic-routes-json", getEnv(env, "RAP_MESH_SYNTHETIC_ROUTES_JSON", ""), "JSON array of synthetic mesh routes for test-only runtime.") fs.BoolVar(&cfg.RemoteWorkspaceRealAdapterEnabled, "remote-workspace-real-adapter-enabled", getEnvBool(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_ENABLED", false), "Request future real remote workspace adapter supervision. Disabled until the real runtime stage is implemented.") fs.StringVar(&cfg.RemoteWorkspaceRealAdapterCommand, "remote-workspace-real-adapter-command", getEnv(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_COMMAND", ""), "Future real remote workspace adapter command path. Redacted from status payloads.") fs.StringVar(&cfg.RemoteWorkspaceRealAdapterArgsJSON, "remote-workspace-real-adapter-args-json", getEnv(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_ARGS_JSON", ""), "Future real remote workspace adapter args JSON. Redacted from status payloads.") fs.StringVar(&cfg.RemoteWorkspaceRealAdapterWorkDir, "remote-workspace-real-adapter-workdir", getEnv(env, "RAP_REMOTE_WORKSPACE_REAL_ADAPTER_WORKDIR", ""), "Future real remote workspace adapter working directory. Redacted from status payloads.") heartbeatSeconds := getEnvInt(env, "RAP_HEARTBEAT_INTERVAL_SECONDS", 15) fs.DurationVar(&cfg.HeartbeatInterval, "heartbeat-interval", time.Duration(heartbeatSeconds)*time.Second, "Heartbeat interval.") enrollmentPollIntervalSeconds := getEnvInt(env, "RAP_ENROLLMENT_POLL_INTERVAL_SECONDS", 5) enrollmentPollTimeoutSeconds := getEnvSignedInt(env, "RAP_ENROLLMENT_POLL_TIMEOUT_SECONDS", 0) fs.DurationVar(&cfg.EnrollmentPollInterval, "enrollment-poll-interval", time.Duration(enrollmentPollIntervalSeconds)*time.Second, "Enrollment approval polling interval.") fs.DurationVar(&cfg.EnrollmentPollTimeout, "enrollment-poll-timeout", time.Duration(enrollmentPollTimeoutSeconds)*time.Second, "Enrollment approval polling timeout.") if err := fs.Parse(args); err != nil { return Config{}, err } cfg.BackendURL = strings.TrimRight(strings.TrimSpace(cfg.BackendURL), "/") cfg.ClusterID = strings.TrimSpace(cfg.ClusterID) cfg.ClusterAuthorityPublicKey = strings.TrimSpace(cfg.ClusterAuthorityPublicKey) cfg.ClusterAuthorityFingerprint = strings.TrimSpace(cfg.ClusterAuthorityFingerprint) cfg.JoinToken = strings.TrimSpace(cfg.JoinToken) cfg.NodeName = strings.TrimSpace(cfg.NodeName) cfg.StateDir = strings.TrimSpace(cfg.StateDir) cfg.MeshListenAddr = strings.TrimSpace(cfg.MeshListenAddr) cfg.MeshQUICFabricListenAddr = strings.TrimSpace(cfg.MeshQUICFabricListenAddr) cfg.MeshListenPortMode = strings.ToLower(strings.TrimSpace(cfg.MeshListenPortMode)) if cfg.VPNFabricSessionStreamShards <= 0 { cfg.VPNFabricSessionStreamShards = 4 } if cfg.VPNFabricSessionStreamShards > 64 { cfg.VPNFabricSessionStreamShards = 64 } if cfg.VPNFabricQUICMaxStreamsPerConn <= 0 { cfg.VPNFabricQUICMaxStreamsPerConn = 64 } if cfg.VPNFabricQUICIdleTTL <= 0 { cfg.VPNFabricQUICIdleTTL = 5 * time.Minute } cfg.MeshAdvertiseEndpoint = strings.TrimRight(strings.TrimSpace(cfg.MeshAdvertiseEndpoint), "/") cfg.MeshAdvertiseEndpointsJSON = strings.TrimSpace(cfg.MeshAdvertiseEndpointsJSON) cfg.FabricRegistryRecordsJSON = strings.TrimSpace(cfg.FabricRegistryRecordsJSON) cfg.MeshAdvertiseTransport = strings.TrimSpace(cfg.MeshAdvertiseTransport) if cfg.MeshAdvertiseTransport == "" { cfg.MeshAdvertiseTransport = "quic" } cfg.MeshAdvertiseTransport = normalizeLegacyAdvertiseTransport(cfg.MeshAdvertiseTransport) cfg.MeshAdvertiseEndpoint = normalizeLegacyEndpointSchemeToQUIC(cfg.MeshAdvertiseEndpoint) cfg.MeshConnectivityMode = strings.TrimSpace(cfg.MeshConnectivityMode) cfg.MeshNATType = strings.TrimSpace(cfg.MeshNATType) cfg.MeshLocalSegmentID = strings.TrimSpace(cfg.MeshLocalSegmentID) cfg.MeshNATGroupID = strings.TrimSpace(cfg.MeshNATGroupID) cfg.MeshSTUNReflexiveEndpoint = normalizeLegacyEndpointSchemeToQUIC(strings.TrimRight(strings.TrimSpace(cfg.MeshSTUNReflexiveEndpoint), "/")) cfg.MeshSTUNServer = strings.TrimSpace(cfg.MeshSTUNServer) cfg.MeshRelayNodeID = strings.TrimSpace(cfg.MeshRelayNodeID) cfg.MeshRelayEndpoint = normalizeLegacyEndpointSchemeToQUIC(strings.TrimRight(strings.TrimSpace(cfg.MeshRelayEndpoint), "/")) cfg.MeshRegion = strings.TrimSpace(cfg.MeshRegion) cfg.MeshSyntheticConfigPath = strings.TrimSpace(cfg.MeshSyntheticConfigPath) cfg.MeshPeerEndpointsJSON = strings.TrimSpace(cfg.MeshPeerEndpointsJSON) cfg.MeshSyntheticRoutesJSON = strings.TrimSpace(cfg.MeshSyntheticRoutesJSON) cfg.WebIngressSigningPrivateKey = strings.TrimSpace(cfg.WebIngressSigningPrivateKey) cfg.WebIngressSigningKeyID = strings.TrimSpace(cfg.WebIngressSigningKeyID) cfg.WebIngressTrustedKeysJSON = strings.TrimSpace(cfg.WebIngressTrustedKeysJSON) cfg.WebIngressRuntimeServiceClasses = strings.TrimSpace(cfg.WebIngressRuntimeServiceClasses) cfg.RemoteWorkspaceRealAdapterCommand = strings.TrimSpace(cfg.RemoteWorkspaceRealAdapterCommand) cfg.RemoteWorkspaceRealAdapterArgsJSON = strings.TrimSpace(cfg.RemoteWorkspaceRealAdapterArgsJSON) cfg.RemoteWorkspaceRealAdapterWorkDir = strings.TrimSpace(cfg.RemoteWorkspaceRealAdapterWorkDir) if cfg.BackendURL == "" { return Config{}, errors.New("backend URL is required") } if cfg.NodeName == "" { return Config{}, errors.New("node name is required") } if cfg.StateDir == "" { return Config{}, errors.New("state dir is required") } if cfg.HeartbeatInterval <= 0 { return Config{}, errors.New("heartbeat interval must be positive") } if cfg.EnrollmentPollInterval <= 0 { return Config{}, errors.New("enrollment poll interval must be positive") } if cfg.EnrollmentPollTimeout < 0 { return Config{}, errors.New("enrollment poll timeout must not be negative") } if cfg.MeshProductionObservationSinkCapacity < 0 { return Config{}, errors.New("mesh production observation sink capacity must not be negative") } if cfg.MeshProductionObservationSinkCapacity > MaxMeshProductionObservationSinkCapacity { return Config{}, errors.New("mesh production observation sink capacity exceeds maximum") } if cfg.FabricRegistryRecordsJSON != "" && !isJSONArray(cfg.FabricRegistryRecordsJSON) { return Config{}, errors.New("fabric registry records must be a JSON array") } switch cfg.MeshListenPortMode { case "", "manual", "auto", "disabled": if cfg.MeshListenPortMode == "" { cfg.MeshListenPortMode = "manual" } default: return Config{}, errors.New("mesh listen port mode must be manual, auto, or disabled") } if cfg.MeshListenAutoPortStart <= 0 || cfg.MeshListenAutoPortEnd <= 0 { return Config{}, errors.New("mesh listen auto port range must be positive") } if cfg.MeshListenAutoPortStart > cfg.MeshListenAutoPortEnd { return Config{}, errors.New("mesh listen auto port start must be less than or equal to end") } if !isQUICAdvertiseTransport(cfg.MeshAdvertiseTransport) { return Config{}, errors.New("mesh advertise transport must be a QUIC transport label") } if hasLegacyEndpointScheme(cfg.MeshAdvertiseEndpoint) { return Config{}, errors.New("mesh advertise endpoint must be a QUIC endpoint") } if cfg.MeshSTUNReflexiveEndpoint != "" && hasLegacyEndpointScheme(cfg.MeshSTUNReflexiveEndpoint) { return Config{}, errors.New("mesh STUN reflexive endpoint must be a QUIC endpoint") } if cfg.MeshRelayEndpoint != "" && hasLegacyEndpointScheme(cfg.MeshRelayEndpoint) { return Config{}, errors.New("mesh relay endpoint must be a QUIC endpoint") } return cfg, nil } func isQUICAdvertiseTransport(label string) bool { switch strings.ToLower(strings.TrimSpace(label)) { case "quic", "direct_quic", "udp_quic", "quic_udp", "lan_quic", "reverse_quic", "relay_quic", "ice_quic": return true default: return false } } func normalizeLegacyAdvertiseTransport(label string) string { switch strings.ToLower(strings.TrimSpace(label)) { case "direct_http", "direct_https", "direct_tcp_tls", "http", "https", "ws", "wss", "websocket": return "direct_quic" case "outbound_reverse", "reverse", "reverse_outbound": return "reverse_quic" case "relay", "relay_control": return "relay_quic" default: return strings.TrimSpace(label) } } func normalizeLegacyEndpointSchemeToQUIC(endpoint string) string { endpoint = strings.TrimRight(strings.TrimSpace(endpoint), "/") lower := strings.ToLower(endpoint) for _, prefix := range []string{"http://", "https://", "ws://", "wss://"} { if strings.HasPrefix(lower, prefix) { return "quic://" + endpoint[len(prefix):] } } return endpoint } func hasLegacyEndpointScheme(endpoint string) bool { endpoint = strings.ToLower(strings.TrimSpace(endpoint)) return strings.HasPrefix(endpoint, "http://") || strings.HasPrefix(endpoint, "https://") || strings.HasPrefix(endpoint, "ws://") || strings.HasPrefix(endpoint, "wss://") } func isJSONArray(value string) bool { var items []json.RawMessage return json.Unmarshal([]byte(strings.TrimSpace(value)), &items) == nil } func readEnv() map[string]string { out := map[string]string{} for _, pair := range os.Environ() { key, value, ok := strings.Cut(pair, "=") if ok { out[key] = value } } return out } func getEnv(env map[string]string, key, fallback string) string { if value := strings.TrimSpace(env[key]); value != "" { return value } return fallback } func getEnvInt(env map[string]string, key string, fallback int) int { value := strings.TrimSpace(env[key]) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil || parsed <= 0 { return fallback } return parsed } func getEnvSignedInt(env map[string]string, key string, fallback int) int { value := strings.TrimSpace(env[key]) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil { return fallback } return parsed } func getEnvBool(env map[string]string, key string, fallback bool) bool { value := strings.ToLower(strings.TrimSpace(env[key])) switch value { case "1", "true", "yes", "y", "on": return true case "0", "false", "no", "n", "off": return false default: return fallback } } func hostnameOrDefault() string { host, err := os.Hostname() if err != nil || strings.TrimSpace(host) == "" { return "rap-node" } return host }