// Copyright 2024 RustFS Team // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Cloud provider detection and metadata fetching. use async_trait::async_trait; use rustfs_utils::get_env_opt_str; use std::str::FromStr; use std::time::Duration; use tracing::{debug, info, warn}; use crate::AppError; /// Supported cloud providers. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum CloudProvider { /// Amazon Web Services Aws, /// Microsoft Azure Azure, /// Google Cloud Platform Gcp, /// DigitalOcean DigitalOcean, /// Cloudflare Cloudflare, /// Unknown or custom provider. Unknown(String), } impl FromStr for CloudProvider { type Err = std::convert::Infallible; fn from_str(s: &str) -> Result { Ok(match s.to_lowercase().as_str() { "aws" | "amazon" => Self::Aws, "azure" | "microsoft" => Self::Azure, "gcp" | "google" => Self::Gcp, "digitalocean" | "do" => Self::DigitalOcean, "cloudflare" | "cf" => Self::Cloudflare, _ => Self::Unknown(s.to_string()), }) } } impl CloudProvider { /// Detects the cloud provider based on environment variables. pub fn detect_from_env() -> Option { let has_env = |key| get_env_opt_str(key).is_some(); // Check for AWS environment variables. if has_env("RUSTFS_AWS_EXECUTION_ENV") || has_env("RUSTFS_AWS_REGION") || has_env("RUSTFS_EC2_INSTANCE_ID") { return Some(Self::Aws); } // Check for Azure environment variables. if has_env("RUSTFS_WEBSITE_SITE_NAME") || has_env("RUSTFS_WEBSITE_INSTANCE_ID") || has_env("RUSTFS_APPSETTING_WEBSITE_SITE_NAME") { return Some(Self::Azure); } // Check for GCP environment variables. if has_env("RUSTFS_GCP_PROJECT") || has_env("RUSTFS_GOOGLE_CLOUD_PROJECT") || has_env("RUSTFS_GAE_INSTANCE") { return Some(Self::Gcp); } // Check for DigitalOcean environment variables. if has_env("RUSTFS_DIGITALOCEAN_REGION") { return Some(Self::DigitalOcean); } // Check for Cloudflare environment variables. if has_env("RUSTFS_CF_PAGES") || has_env("RUSTFS_CF_WORKERS") { return Some(Self::Cloudflare); } None } /// Returns the canonical name of the cloud provider. pub fn name(&self) -> &str { match self { Self::Aws => "aws", Self::Azure => "azure", Self::Gcp => "gcp", Self::DigitalOcean => "digitalocean", Self::Cloudflare => "cloudflare", Self::Unknown(name) => name, } } } #[cfg(test)] mod tests { use super::*; use temp_env::{with_var, with_vars_unset}; #[test] fn test_detect_from_env_prefers_known_provider_markers() { with_var("RUSTFS_AWS_REGION", Some("us-east-1"), || { assert_eq!(CloudProvider::detect_from_env(), Some(CloudProvider::Aws)); }); } #[test] fn test_detect_from_env_returns_none_without_markers() { with_vars_unset( vec![ "RUSTFS_AWS_EXECUTION_ENV", "RUSTFS_AWS_REGION", "RUSTFS_EC2_INSTANCE_ID", "RUSTFS_WEBSITE_SITE_NAME", "RUSTFS_WEBSITE_INSTANCE_ID", "RUSTFS_APPSETTING_WEBSITE_SITE_NAME", "RUSTFS_GCP_PROJECT", "RUSTFS_GOOGLE_CLOUD_PROJECT", "RUSTFS_GAE_INSTANCE", "RUSTFS_DIGITALOCEAN_REGION", "RUSTFS_CF_PAGES", "RUSTFS_CF_WORKERS", ], || { assert_eq!(CloudProvider::detect_from_env(), None); }, ); } } /// Trait for fetching metadata from a specific cloud provider. #[async_trait] pub trait CloudMetadataFetcher: Send + Sync { /// Returns the name of the provider. fn provider_name(&self) -> &str; /// Fetches the network CIDR ranges for the current instance. async fn fetch_network_cidrs(&self) -> Result, AppError>; /// Fetches the public IP ranges for the cloud provider. async fn fetch_public_ip_ranges(&self) -> Result, AppError>; /// Fetches all IP ranges that should be considered trusted proxies. async fn fetch_trusted_proxy_ranges(&self) -> Result, AppError> { let mut ranges = Vec::new(); match self.fetch_network_cidrs().await { Ok(cidrs) => ranges.extend(cidrs), Err(e) => warn!( event = "trusted_proxies.cloud_fetch", component = "trusted_proxies", subsystem = "cloud_detector", provider = self.provider_name(), dataset = "network_cidrs", result = "degraded", error = %e, "trusted proxy cloud metadata fetch degraded" ), } match self.fetch_public_ip_ranges().await { Ok(public_ranges) => ranges.extend(public_ranges), Err(e) => warn!( event = "trusted_proxies.cloud_fetch", component = "trusted_proxies", subsystem = "cloud_detector", provider = self.provider_name(), dataset = "public_ip_ranges", result = "degraded", error = %e, "trusted proxy cloud metadata fetch degraded" ), } Ok(ranges) } } /// Detector for identifying the current cloud environment and fetching relevant metadata. #[derive(Debug, Clone)] pub struct CloudDetector { /// Whether cloud detection is enabled. enabled: bool, /// Timeout for metadata requests. timeout: Duration, /// Optionally force a specific provider. forced_provider: Option, } impl CloudDetector { /// Creates a new `CloudDetector`. pub fn new(enabled: bool, timeout: Duration, forced_provider: Option) -> Self { let forced_provider = forced_provider.and_then(|s| CloudProvider::from_str(&s).ok()); Self { enabled, timeout, forced_provider, } } /// Identifies the current cloud provider. pub fn detect_provider(&self) -> Option { if !self.enabled { return None; } if let Some(provider) = self.forced_provider.as_ref() { return Some(provider.clone()); } CloudProvider::detect_from_env() } /// Fetches trusted IP ranges for the detected cloud provider. pub async fn fetch_trusted_ranges(&self) -> Result, AppError> { if !self.enabled { debug!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", state = "disabled", "trusted proxy cloud detection skipped" ); return Ok(Vec::new()); } let provider = self.detect_provider(); match provider { Some(CloudProvider::Aws) => { info!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", provider = "aws", result = "detected", timeout_ms = self.timeout.as_millis(), "trusted proxy cloud provider detected" ); let fetcher = crate::AwsMetadataFetcher::new(self.timeout); fetcher.fetch_trusted_proxy_ranges().await } Some(CloudProvider::Azure) => { info!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", provider = "azure", result = "detected", timeout_ms = self.timeout.as_millis(), "trusted proxy cloud provider detected" ); let fetcher = crate::AzureMetadataFetcher::new(self.timeout); fetcher.fetch_trusted_proxy_ranges().await } Some(CloudProvider::Gcp) => { info!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", provider = "gcp", result = "detected", timeout_ms = self.timeout.as_millis(), "trusted proxy cloud provider detected" ); let fetcher = crate::GcpMetadataFetcher::new(self.timeout); fetcher.fetch_trusted_proxy_ranges().await } Some(CloudProvider::Cloudflare) => { info!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", provider = "cloudflare", result = "detected", "trusted proxy cloud provider detected" ); let ranges = crate::CloudflareIpRanges::fetch().await?; Ok(ranges) } Some(CloudProvider::DigitalOcean) => { info!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", provider = "digitalocean", result = "detected", "trusted proxy cloud provider detected" ); let ranges = crate::DigitalOceanIpRanges::fetch().await?; Ok(ranges) } Some(CloudProvider::Unknown(name)) => { warn!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", provider = %name, result = "unknown", "trusted proxy cloud provider unresolved" ); Ok(Vec::new()) } None => { debug!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", result = "none", "trusted proxy cloud provider not detected" ); Ok(Vec::new()) } } } /// Attempts to fetch metadata from all supported providers sequentially. pub async fn try_all_providers(&self) -> Result, AppError> { if !self.enabled { return Ok(Vec::new()); } let providers: Vec> = vec![ Box::new(crate::AwsMetadataFetcher::new(self.timeout)), Box::new(crate::AzureMetadataFetcher::new(self.timeout)), Box::new(crate::GcpMetadataFetcher::new(self.timeout)), ]; for provider in providers { let provider_name = provider.provider_name(); debug!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", provider = provider_name, result = "attempt", "trusted proxy cloud provider fetch attempted" ); match provider.fetch_trusted_proxy_ranges().await { Ok(ranges) => { if !ranges.is_empty() { info!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", provider = provider_name, result = "loaded", range_count = ranges.len(), "trusted proxy cloud provider ranges loaded" ); return Ok(ranges); } } Err(e) => { debug!( event = "trusted_proxies.cloud_detect", component = "trusted_proxies", subsystem = "cloud_detector", provider = provider_name, result = "failed", error = %e, "trusted proxy cloud provider fetch failed" ); } } } Ok(Vec::new()) } } /// Returns a default `CloudDetector` with detection disabled. pub fn default_cloud_detector() -> CloudDetector { CloudDetector::new(false, Duration::from_secs(5), None) }