use tracing::warn; use crate::error::{ProxyError, Result}; const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[ "general", "logging", "network", "server", "web", "timeouts", "censorship", "access", "upstreams", "show_link", "dc_overrides", "default_dc", "beobachten", "beobachten_minutes", "beobachten_flush_secs", "beobachten_file", "include", ]; const GENERAL_CONFIG_KEYS: &[&str] = &[ "data_path", "quota_state_path", "config_strict", "modes", "prefer_ipv6", "fast_mode", "use_middle_proxy", "proxy_secret_path", "proxy_secret_url", "proxy_config_v4_cache_path", "proxy_config_v4_url", "proxy_config_v6_cache_path", "proxy_config_v6_url", "ad_tag", "middle_proxy_nat_ip", "middle_proxy_nat_probe", "middle_proxy_nat_stun", "middle_proxy_nat_stun_servers", "stun_nat_probe_concurrency", "middle_proxy_pool_size", "middle_proxy_warm_standby", "me_init_retry_attempts", "me2dc_fallback", "me2dc_fast", "me_keepalive_enabled", "me_keepalive_interval_secs", "me_keepalive_jitter_secs", "me_keepalive_payload_random", "rpc_proxy_req_every", "me_writer_cmd_channel_capacity", "me_writer_byte_budget_bytes", "me_route_channel_capacity", "me_c2me_channel_capacity", "me_c2me_send_timeout_ms", "me_reader_route_data_wait_ms", "me_d2c_flush_batch_max_frames", "me_d2c_flush_batch_max_bytes", "me_d2c_flush_batch_max_delay_us", "me_d2c_ack_flush_immediate", "me_quota_soft_overshoot_bytes", "me_d2c_frame_buf_shrink_threshold_bytes", "direct_relay_copy_buf_c2s_bytes", "direct_relay_copy_buf_s2c_bytes", "direct_relay_buffer_budget_max_bytes", "crypto_pending_buffer", "max_client_frame", "desync_all_full", "beobachten", "beobachten_minutes", "beobachten_flush_secs", "beobachten_file", "hardswap", "me_warmup_stagger_enabled", "me_warmup_step_delay_ms", "me_warmup_step_jitter_ms", "me_reconnect_max_concurrent_per_dc", "me_reconnect_backoff_base_ms", "me_reconnect_backoff_cap_ms", "me_reconnect_fast_retry_count", "me_single_endpoint_shadow_writers", "me_single_endpoint_outage_mode_enabled", "me_single_endpoint_outage_disable_quarantine", "me_single_endpoint_outage_backoff_min_ms", "me_single_endpoint_outage_backoff_max_ms", "me_single_endpoint_shadow_rotate_every_secs", "me_floor_mode", "me_adaptive_floor_idle_secs", "me_adaptive_floor_min_writers_single_endpoint", "me_adaptive_floor_min_writers_multi_endpoint", "me_adaptive_floor_recover_grace_secs", "me_adaptive_floor_writers_per_core_total", "me_adaptive_floor_cpu_cores_override", "me_adaptive_floor_max_extra_writers_single_per_core", "me_adaptive_floor_max_extra_writers_multi_per_core", "me_adaptive_floor_max_active_writers_per_core", "me_adaptive_floor_max_warm_writers_per_core", "me_adaptive_floor_max_active_writers_global", "me_adaptive_floor_max_warm_writers_global", "upstream_connect_retry_attempts", "upstream_connect_retry_backoff_ms", "upstream_connect_budget_ms", "tg_connect", "upstream_unhealthy_fail_threshold", "upstream_connect_failfast_hard_errors", "stun_iface_mismatch_ignore", "unknown_dc_log_path", "unknown_dc_file_log_enabled", "log_level", "disable_colors", "telemetry", "me_socks_kdf_policy", "me_route_backpressure_enabled", "me_route_fairshare_enabled", "me_route_backpressure_base_timeout_ms", "me_route_backpressure_high_timeout_ms", "me_route_backpressure_high_watermark_pct", "me_health_interval_ms_unhealthy", "me_health_interval_ms_healthy", "me_admission_poll_ms", "me_warn_rate_limit_ms", "me_route_no_writer_mode", "me_route_no_writer_wait_ms", "me_route_hybrid_max_wait_ms", "me_route_blocking_send_timeout_ms", "me_route_inline_recovery_attempts", "me_route_inline_recovery_wait_ms", "links", "fast_mode_min_tls_record", "update_every", "me_reinit_every_secs", "me_hardswap_warmup_delay_min_ms", "me_hardswap_warmup_delay_max_ms", "me_hardswap_warmup_extra_passes", "me_hardswap_warmup_pass_backoff_base_ms", "me_config_stable_snapshots", "me_config_apply_cooldown_secs", "me_snapshot_require_http_2xx", "me_snapshot_reject_empty_map", "me_snapshot_min_proxy_for_lines", "proxy_secret_stable_snapshots", "proxy_secret_rotate_runtime", "me_secret_atomic_snapshot", "proxy_secret_len_max", "me_pool_drain_ttl_secs", "me_instadrain", "me_pool_drain_threshold", "me_pool_drain_soft_evict_enabled", "me_pool_drain_soft_evict_grace_secs", "me_pool_drain_soft_evict_per_writer", "me_pool_drain_soft_evict_budget_per_core", "me_pool_drain_soft_evict_cooldown_ms", "me_bind_stale_mode", "me_bind_stale_ttl_secs", "me_pool_min_fresh_ratio", "me_reinit_drain_timeout_secs", "proxy_secret_auto_reload_secs", "proxy_config_auto_reload_secs", "me_reinit_singleflight", "me_reinit_trigger_channel", "me_reinit_coalesce_window_ms", "me_deterministic_writer_sort", "me_writer_pick_mode", "me_writer_pick_sample_size", "ntp_check", "ntp_servers", "auto_degradation_enabled", "degradation_min_unavailable_dc_groups", "rst_on_close", ]; const NETWORK_CONFIG_KEYS: &[&str] = &[ "ipv4", "ipv6", "prefer", "multipath", "stun_use", "stun_servers", "stun_tcp_fallback", "http_ip_detect_urls", "cache_public_ip_path", "dns_overrides", ]; const SERVER_CONFIG_KEYS: &[&str] = &[ "port", "listen_addr_ipv4", "listen_addr_ipv6", "listen_unix_sock", "listen_unix_sock_perm", "listen_tcp", "client_mss", "client_mss_bulk", "proxy_protocol", "proxy_protocol_header_timeout_ms", "proxy_protocol_trusted_cidrs", "metrics_port", "metrics_listen", "metrics_whitelist", "api", "admin_api", "listeners", "listen_backlog", "max_connections", "accept_permit_timeout_ms", "conntrack_control", ]; const API_CONFIG_KEYS: &[&str] = &[ "enabled", "listen", "whitelist", "gray_action", "auth_header", "request_body_limit_bytes", "minimal_runtime_enabled", "minimal_runtime_cache_ttl_ms", "runtime_edge_enabled", "runtime_edge_cache_ttl_ms", "runtime_edge_top_n", "runtime_edge_events_capacity", "read_only", ]; const CONNTRACK_CONTROL_CONFIG_KEYS: &[&str] = &[ "inline_conntrack_control", "mode", "backend", "profile", "hybrid_listener_ips", "pressure_high_watermark_pct", "pressure_low_watermark_pct", "delete_budget_per_sec", ]; const LISTENER_CONFIG_KEYS: &[&str] = &[ "ip", "transport", "port", "client_mss", "synlimit", "synlimit_seconds", "synlimit_hitcount", "synlimit_burst", "synlimit_ios_seconds", "synlimit_ios_hitcount", "synlimit_ios_burst", "synlimit_hashlimit_expire_ms", "synlimit_hashlimit_size", "announce", "announce_ip", "proxy_protocol", "reuse_allow", "web_client_ip_source", "web_trusted_proxy_cidrs", ]; const WEB_CONFIG_KEYS: &[&str] = &[ "enabled", "carrier", "carriers", "carrier_learning", "carrier_negotiation_aggressiveness", "debug", "limits", "timeouts", "vhosts", ]; const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[ "max_header_bytes", "max_body_bytes", "max_frame_payload_bytes", "carrier_batch_bytes", "max_frames_per_body", "max_http_connections", "max_http_handlers", "max_lane_open_waits_per_session", "pending_bytes_per_lane", "pending_items_per_lane", "websocket_bytes_global", "websocket_admission_watermark_pct", "websocket_eviction_watermark_pct", "websocket_http_connection_reserve", "max_websocket_evictions_in_flight", "max_carrier_learning_entries", "max_body_readers", "max_body_bytes_global", "max_sessions_global", "max_sessions_per_ip", "max_streams_per_session", "max_streams_global", "max_stream_handshakes", "max_tombstones_per_session", "pending_bytes_per_session", "pending_bytes_global", "pending_items_per_session", "pending_items_global", "control_bytes_per_session", "control_bytes_global", "max_bootstraps_global", "max_bootstraps_per_ip", "max_vhosts", "max_profiles", "max_static_files", "max_static_file_bytes", "max_static_bytes", "debug_records_capacity", "debug_bytes_global", "memory_envelope_bytes", "new_bootstraps_per_minute", "new_bootstraps_burst", "new_sessions_per_minute", "new_sessions_burst", "new_streams_per_minute", "new_streams_burst", ]; const WEB_DEBUG_CONFIG_KEYS: &[&str] = &[ "enabled", "capture_lifecycle", "capture_headers", "capture_timings", "capture_frames", "body_capture", "body_prefix_bytes", "decoy_body_prefix_bytes", "default_window_secs", "max_window_secs", ]; const WEB_TIMEOUTS_CONFIG_KEYS: &[&str] = &[ "header_secs", "body_secs", "stream_handshake_secs", "stream_first_byte_secs", "long_poll_secs", "lane_open_wait_secs", "carrier_health_secs", "websocket_upgrade_secs", "websocket_open_secs", "websocket_write_secs", "websocket_backpressure_secs", "websocket_eviction_secs", "carrier_negotiation_deadlines_secs", "carrier_learning_secs", "bootstrap_lifetime_secs", "reconnect_grace_secs", "http_idle_secs", "shutdown_secs", "decoy_header_secs", ]; const WEB_VHOST_CONFIG_KEYS: &[&str] = &["host", "public_addr", "decoy", "profiles"]; const WEB_DECOY_CONFIG_KEYS: &[&str] = &["mode", "upstream", "directory", "index"]; const WEB_PROFILE_CONFIG_KEYS: &[&str] = &[ "user", "secret_mode", "max_sessions", "max_streams", "max_streams_per_session", ]; const TIMEOUTS_CONFIG_KEYS: &[&str] = &[ "client_first_byte_idle_secs", "client_handshake", "relay_idle_policy_v2_enabled", "relay_client_idle_soft_secs", "relay_client_idle_hard_secs", "relay_idle_grace_after_downstream_activity_secs", "client_keepalive", "client_ack", "me_one_retry", "me_one_timeout_ms", ]; const CENSORSHIP_CONFIG_KEYS: &[&str] = &[ "tls_domain", "tls_domains", "unknown_sni_action", "tls_fetch_scope", "tls_fetch", "mask", "mask_dynamic", "mask_host", "mask_port", "exclusive_mask", "mask_unix_sock", "fake_cert_len", "tls_emulation", "tls_front_dir", "server_hello_delay_min_ms", "server_hello_delay_max_ms", "tls_new_session_tickets", "serverhello_compact", "tls_full_cert_ttl_secs", "alpn_enforce", "mask_proxy_protocol", "mask_shape_hardening", "mask_shape_hardening_aggressive_mode", "mask_shape_bucket_floor_bytes", "mask_shape_bucket_cap_bytes", "mask_shape_above_cap_blur", "mask_shape_above_cap_blur_max_bytes", "mask_relay_max_bytes", "mask_relay_timeout_ms", "mask_relay_idle_timeout_ms", "mask_classifier_prefetch_timeout_ms", "mask_timing_normalization_enabled", "mask_timing_normalization_floor_ms", "mask_timing_normalization_ceiling_ms", ]; const TLS_FETCH_CONFIG_KEYS: &[&str] = &[ "profiles", "strict_route", "attempt_timeout_ms", "total_budget_ms", "grease_enabled", "deterministic", "profile_cache_ttl_secs", ]; const ACCESS_CONFIG_KEYS: &[&str] = &[ "users", "user_enabled", "user_ad_tags", "user_max_tcp_conns", "user_max_tcp_conns_global_each", "user_expirations", "user_data_quota", "user_rate_limits", "cidr_rate_limits", "user_max_unique_ips", "user_max_unique_ips_global_each", "user_max_unique_ips_mode", "user_max_unique_ips_window_secs", "replay_check_len", "replay_window_secs", "ignore_time_skew", ]; const RATE_LIMIT_BPS_CONFIG_KEYS: &[&str] = &["up_bps", "down_bps"]; const UPSTREAM_CONFIG_KEYS: &[&str] = &[ "type", "interface", "bind_addresses", "bindtodevice", "force_bind", "address", "user_id", "username", "password", "url", "weight", "enabled", "scopes", "ipv4", "ipv6", ]; const PROXY_MODES_CONFIG_KEYS: &[&str] = &["classic", "secure", "tls"]; const TELEMETRY_CONFIG_KEYS: &[&str] = &["core_enabled", "user_enabled", "me_level"]; const LINKS_CONFIG_KEYS: &[&str] = &["show", "public_host", "public_port"]; const LOGGING_CONFIG_KEYS: &[&str] = &[ "destination", "path", "rotation", "max_size_bytes", "max_files", "max_age_secs", ]; // Recursive table traversal and key suggestion logic. mod check; /// Rejects or reports unknown configuration keys according to strict mode. pub(super) fn handle_unknown_config_keys(parsed_toml: &toml::Value) -> Result<()> { let unknown = check::collect_unknown_config_keys(parsed_toml); if unknown.is_empty() { return Ok(()); } for item in &unknown { if let Some(suggestion) = item.suggestion.as_deref() { warn!( key = %item.path, suggestion = %suggestion, "Unknown config key ignored; did you mean the suggested key?" ); } else { warn!(key = %item.path, "Unknown config key ignored"); } } if check::is_strict_config(parsed_toml) { let mut paths = Vec::with_capacity(unknown.len()); for item in unknown { if let Some(suggestion) = item.suggestion { paths.push(format!("{} (did you mean `{}`?)", item.path, suggestion)); } else { paths.push(item.path); } } return Err(ProxyError::Config(format!( "unknown config keys are not allowed when general.config_strict=true: {}", paths.join(", ") ))); } Ok(()) }