Refactor trusted-proxies: modernize utils, improve safety, and fix clippy lints (#1693)

Co-authored-by: majinghe <42570491+majinghe@users.noreply.github.com>
Co-authored-by: GatewayJ <835269233@qq.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
This commit is contained in:
houseme
2026-02-03 01:06:22 +08:00
committed by GitHub
parent d1a70176a2
commit cb468fb32f
55 changed files with 5145 additions and 93 deletions
@@ -0,0 +1,257 @@
// 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 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<Self, Self::Err> {
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<Self> {
// Check for AWS environment variables.
if std::env::var("RUSTFS_AWS_EXECUTION_ENV").is_ok()
|| std::env::var("RUSTFS_AWS_REGION").is_ok()
|| std::env::var("RUSTFS_EC2_INSTANCE_ID").is_ok()
{
return Some(Self::Aws);
}
// Check for Azure environment variables.
if std::env::var("RUSTFS_WEBSITE_SITE_NAME").is_ok()
|| std::env::var("RUSTFS_WEBSITE_INSTANCE_ID").is_ok()
|| std::env::var("RUSTFS_APPSETTING_WEBSITE_SITE_NAME").is_ok()
{
return Some(Self::Azure);
}
// Check for GCP environment variables.
if std::env::var("RUSTFS_GCP_PROJECT").is_ok()
|| std::env::var("RUSTFS_GOOGLE_CLOUD_PROJECT").is_ok()
|| std::env::var("RUSTFS_GAE_INSTANCE").is_ok()
{
return Some(Self::Gcp);
}
// Check for DigitalOcean environment variables.
if std::env::var("RUSTFS_DIGITALOCEAN_REGION").is_ok() {
return Some(Self::DigitalOcean);
}
// Check for Cloudflare environment variables.
if std::env::var("RUSTFS_CF_PAGES").is_ok() || std::env::var("RUSTFS_CF_WORKERS").is_ok() {
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,
}
}
}
/// 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<Vec<ipnetwork::IpNetwork>, AppError>;
/// Fetches the public IP ranges for the cloud provider.
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError>;
/// Fetches all IP ranges that should be considered trusted proxies.
async fn fetch_trusted_proxy_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let mut ranges = Vec::new();
match self.fetch_network_cidrs().await {
Ok(cidrs) => ranges.extend(cidrs),
Err(e) => warn!("Failed to fetch network CIDRs from {}: {}", self.provider_name(), e),
}
match self.fetch_public_ip_ranges().await {
Ok(public_ranges) => ranges.extend(public_ranges),
Err(e) => warn!("Failed to fetch public IP ranges from {}: {}", self.provider_name(), e),
}
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<CloudProvider>,
}
impl CloudDetector {
/// Creates a new `CloudDetector`.
pub fn new(enabled: bool, timeout: Duration, forced_provider: Option<String>) -> 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<CloudProvider> {
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<Vec<ipnetwork::IpNetwork>, AppError> {
if !self.enabled {
debug!("Cloud metadata fetching is disabled");
return Ok(Vec::new());
}
let provider = self.detect_provider();
match provider {
Some(CloudProvider::Aws) => {
info!("Detected AWS environment, fetching metadata");
let fetcher = crate::AwsMetadataFetcher::new(self.timeout);
fetcher.fetch_trusted_proxy_ranges().await
}
Some(CloudProvider::Azure) => {
info!("Detected Azure environment, fetching metadata");
let fetcher = crate::AzureMetadataFetcher::new(self.timeout);
fetcher.fetch_trusted_proxy_ranges().await
}
Some(CloudProvider::Gcp) => {
info!("Detected GCP environment, fetching metadata");
let fetcher = crate::GcpMetadataFetcher::new(self.timeout);
fetcher.fetch_trusted_proxy_ranges().await
}
Some(CloudProvider::Cloudflare) => {
info!("Detected Cloudflare environment");
let ranges = crate::CloudflareIpRanges::fetch().await?;
Ok(ranges)
}
Some(CloudProvider::DigitalOcean) => {
info!("Detected DigitalOcean environment");
let ranges = crate::DigitalOceanIpRanges::fetch().await?;
Ok(ranges)
}
Some(CloudProvider::Unknown(name)) => {
warn!("Unknown cloud provider detected: {}", name);
Ok(Vec::new())
}
None => {
debug!("No cloud provider detected");
Ok(Vec::new())
}
}
}
/// Attempts to fetch metadata from all supported providers sequentially.
pub async fn try_all_providers(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
if !self.enabled {
return Ok(Vec::new());
}
let providers: Vec<Box<dyn CloudMetadataFetcher>> = 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!("Trying to fetch metadata from {}", provider_name);
match provider.fetch_trusted_proxy_ranges().await {
Ok(ranges) => {
if !ranges.is_empty() {
info!("Fetched {} IP ranges from {}", ranges.len(), provider_name);
return Ok(ranges);
}
}
Err(e) => {
debug!("Failed to fetch metadata from {}: {}", provider_name, e);
}
}
}
Ok(Vec::new())
}
}
/// Returns a default `CloudDetector` with detection disabled.
pub fn default_cloud_detector() -> CloudDetector {
CloudDetector::new(false, Duration::from_secs(5), None)
}
@@ -0,0 +1,153 @@
// 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.
//! AWS metadata fetching implementation for identifying trusted proxy ranges.
use async_trait::async_trait;
use reqwest::Client;
use std::str::FromStr;
use std::time::Duration;
use tracing::{debug, info};
use crate::AppError;
use crate::CloudMetadataFetcher;
/// Fetcher for AWS-specific metadata.
#[derive(Debug, Clone)]
pub struct AwsMetadataFetcher {
client: Client,
metadata_endpoint: String,
}
impl AwsMetadataFetcher {
/// Creates a new `AwsMetadataFetcher`.
///
/// # Arguments
///
/// * `timeout` - Duration to use for HTTP request timeouts.
///
/// Returns a new instance of `AwsMetadataFetcher`.
pub fn new(timeout: Duration) -> Self {
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new());
Self {
client,
metadata_endpoint: "http://169.254.169.254".to_string(),
}
}
/// Retrieves an IMDSv2 token for secure metadata access.
#[allow(dead_code)]
async fn get_metadata_token(&self) -> Result<String, AppError> {
let url = format!("{}/latest/api/token", self.metadata_endpoint);
match self
.client
.put(&url)
.header("X-aws-ec2-metadata-token-ttl-seconds", "21600")
.send()
.await
{
Ok(response) => {
if response.status().is_success() {
let token = response
.text()
.await
.map_err(|e| AppError::cloud(format!("Failed to read IMDSv2 token: {}", e)))?;
Ok(token)
} else {
debug!("IMDSv2 token request failed with status: {}", response.status());
Err(AppError::cloud("Failed to obtain IMDSv2 token"))
}
}
Err(e) => {
debug!("IMDSv2 token request failed: {}", e);
Err(AppError::cloud(format!("IMDSv2 request failed: {}", e)))
}
}
}
}
#[async_trait]
impl CloudMetadataFetcher for AwsMetadataFetcher {
fn provider_name(&self) -> &str {
"aws"
}
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
// Simplified implementation: returns standard AWS VPC private ranges.
let default_ranges = vec![
"10.0.0.0/8", // Large VPCs
"172.16.0.0/12", // Medium VPCs
"192.168.0.0/16", // Small VPCs
];
let networks: Result<Vec<_>, _> = default_ranges.into_iter().map(ipnetwork::IpNetwork::from_str).collect();
match networks {
Ok(networks) => {
debug!("Using default AWS VPC network ranges");
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse default AWS ranges: {}", e))),
}
}
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let url = "https://ip-ranges.amazonaws.com/ip-ranges.json";
#[derive(Debug, serde::Deserialize)]
struct AwsIpRanges {
prefixes: Vec<AwsPrefix>,
}
#[derive(Debug, serde::Deserialize)]
struct AwsPrefix {
ip_prefix: String,
service: String,
}
match self.client.get(url).timeout(Duration::from_secs(5)).send().await {
Ok(response) => {
if response.status().is_success() {
let ip_ranges: AwsIpRanges = response
.json()
.await
.map_err(|e| AppError::cloud(format!("Failed to parse AWS IP ranges JSON: {}", e)))?;
let mut networks = Vec::new();
for prefix in ip_ranges.prefixes {
// Include EC2 and CloudFront ranges as potential trusted proxies.
if (prefix.service == "EC2" || prefix.service == "CLOUDFRONT")
&& let Ok(network) = ipnetwork::IpNetwork::from_str(&prefix.ip_prefix)
{
networks.push(network);
}
}
info!("Successfully fetched {} AWS public IP ranges", networks.len());
Ok(networks)
} else {
debug!("Failed to fetch AWS IP ranges: HTTP {}", response.status());
Ok(Vec::new())
}
}
Err(e) => {
debug!("Failed to fetch AWS IP ranges: {}", e);
Ok(Vec::new())
}
}
}
}
@@ -0,0 +1,308 @@
// 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.
//! Azure Cloud metadata fetching implementation for identifying trusted proxy ranges.
use async_trait::async_trait;
use reqwest::Client;
use serde::Deserialize;
use std::str::FromStr;
use std::time::Duration;
use tracing::{debug, info, warn};
use crate::AppError;
use crate::CloudMetadataFetcher;
/// Fetcher for Azure-specific metadata.
#[derive(Debug, Clone)]
pub struct AzureMetadataFetcher {
client: Client,
metadata_endpoint: String,
}
impl AzureMetadataFetcher {
/// Creates a new `AzureMetadataFetcher`.
pub fn new(timeout: Duration) -> Self {
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new());
Self {
client,
metadata_endpoint: "http://169.254.169.254".to_string(),
}
}
/// Retrieves metadata from the Azure Instance Metadata Service (IMDS).
async fn get_metadata(&self, path: &str) -> Result<String, AppError> {
let url = format!("{}/metadata/{}?api-version=2021-05-01", self.metadata_endpoint, path);
debug!("Fetching Azure metadata from: {}", url);
match self.client.get(&url).header("Metadata", "true").send().await {
Ok(response) => {
if response.status().is_success() {
let text = response
.text()
.await
.map_err(|e| AppError::cloud(format!("Failed to read Azure metadata response: {}", e)))?;
Ok(text)
} else {
debug!("Azure metadata request failed with status: {}", response.status());
Err(AppError::cloud(format!("Azure metadata API returned status: {}", response.status())))
}
}
Err(e) => {
debug!("Azure metadata request failed: {}", e);
Err(AppError::cloud(format!("Azure metadata request failed: {}", e)))
}
}
}
/// Fetches Azure public IP ranges from the official Microsoft download source.
async fn fetch_azure_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
// Official Azure IP ranges download URL (periodically updated).
// See: https://www.microsoft.com/en-us/download/details.aspx?id=56519
let url =
"https://download.microsoft.com/download/7/1/D/71D86715-5596-4529-9B13-DA13A5DE5B63/ServiceTags_Public_20260126.json";
#[derive(Debug, Deserialize)]
struct AzureServiceTags {
values: Vec<AzureServiceTag>,
}
#[derive(Debug, Deserialize)]
struct AzureServiceTag {
name: String,
properties: AzureServiceTagProperties,
}
#[derive(Debug, Deserialize)]
struct AzureServiceTagProperties {
address_prefixes: Vec<String>,
}
debug!("Fetching Azure IP ranges from: {}", url);
match self.client.get(url).timeout(Duration::from_secs(10)).send().await {
Ok(response) => {
if response.status().is_success() {
let service_tags: AzureServiceTags = response
.json()
.await
.map_err(|e| AppError::cloud(format!("Failed to parse Azure IP ranges JSON: {}", e)))?;
let mut networks = Vec::new();
for tag in service_tags.values {
// Include general Azure datacenter ranges, excluding specific internal services.
if tag.name.contains("Azure") && !tag.name.contains("ActiveDirectory") {
for prefix in tag.properties.address_prefixes {
if let Ok(network) = ipnetwork::IpNetwork::from_str(&prefix) {
networks.push(network);
}
}
}
}
info!("Successfully fetched {} Azure public IP ranges", networks.len());
Ok(networks)
} else {
debug!("Failed to fetch Azure IP ranges: HTTP {}", response.status());
Ok(Vec::new())
}
}
Err(e) => {
debug!("Failed to fetch Azure IP ranges: {}", e);
// Fallback to hardcoded ranges if the download fails.
Self::default_azure_ranges()
}
}
}
/// Returns a set of default Azure IP ranges as a fallback.
fn default_azure_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let ranges = vec![
"13.64.0.0/11",
"13.96.0.0/13",
"13.104.0.0/14",
"20.33.0.0/16",
"20.34.0.0/15",
"20.36.0.0/14",
"20.40.0.0/13",
"20.48.0.0/12",
"20.64.0.0/10",
"20.128.0.0/16",
"20.135.0.0/16",
"20.136.0.0/13",
"20.150.0.0/15",
"20.157.0.0/16",
"20.184.0.0/13",
"20.190.0.0/16",
"20.192.0.0/10",
"40.64.0.0/10",
"40.80.0.0/12",
"40.96.0.0/13",
"40.112.0.0/13",
"40.120.0.0/14",
"40.124.0.0/16",
"40.125.0.0/17",
"51.12.0.0/15",
"51.104.0.0/15",
"51.120.0.0/16",
"51.124.0.0/16",
"51.132.0.0/16",
"51.136.0.0/15",
"51.138.0.0/16",
"51.140.0.0/14",
"51.144.0.0/15",
"52.96.0.0/12",
"52.112.0.0/14",
"52.120.0.0/14",
"52.124.0.0/16",
"52.125.0.0/16",
"52.126.0.0/15",
"52.130.0.0/15",
"52.136.0.0/13",
"52.144.0.0/15",
"52.146.0.0/15",
"52.148.0.0/14",
"52.152.0.0/13",
"52.160.0.0/12",
"52.176.0.0/13",
"52.184.0.0/14",
"52.188.0.0/14",
"52.224.0.0/11",
"65.52.0.0/14",
"104.40.0.0/13",
"104.208.0.0/13",
"104.215.0.0/16",
"137.116.0.0/15",
"137.135.0.0/16",
"138.91.0.0/16",
"157.56.0.0/16",
"168.61.0.0/16",
"168.62.0.0/15",
"191.233.0.0/18",
"193.149.0.0/19",
"2603:1000::/40",
"2603:1010::/40",
"2603:1020::/40",
"2603:1030::/40",
"2603:1040::/40",
"2603:1050::/40",
"2603:1060::/40",
"2603:1070::/40",
"2603:1080::/40",
"2603:1090::/40",
"2603:10a0::/40",
"2603:10b0::/40",
"2603:10c0::/40",
"2603:10d0::/40",
"2603:10e0::/40",
"2603:10f0::/40",
"2603:1100::/40",
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(ipnetwork::IpNetwork::from_str).collect();
match networks {
Ok(networks) => {
debug!("Using default Azure public IP ranges");
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse default Azure ranges: {}", e))),
}
}
}
#[async_trait]
impl CloudMetadataFetcher for AzureMetadataFetcher {
fn provider_name(&self) -> &str {
"azure"
}
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
// Attempt to fetch network interface information from Azure IMDS.
match self.get_metadata("instance/network/interface").await {
Ok(metadata) => {
#[derive(Debug, Deserialize)]
struct AzureNetworkInterface {
ipv4: AzureIpv4Info,
}
#[derive(Debug, Deserialize)]
struct AzureIpv4Info {
subnet: Vec<AzureSubnet>,
}
#[derive(Debug, Deserialize)]
struct AzureSubnet {
address: String,
prefix: String,
}
let interfaces: Vec<AzureNetworkInterface> = serde_json::from_str(&metadata)
.map_err(|e| AppError::cloud(format!("Failed to parse Azure network metadata JSON: {}", e)))?;
let mut cidrs = Vec::new();
for interface in interfaces {
for subnet in interface.ipv4.subnet {
let cidr = format!("{}/{}", subnet.address, subnet.prefix);
if let Ok(network) = ipnetwork::IpNetwork::from_str(&cidr) {
cidrs.push(network);
}
}
}
if !cidrs.is_empty() {
info!("Successfully fetched {} network CIDRs from Azure metadata", cidrs.len());
Ok(cidrs)
} else {
debug!("No network CIDRs found in Azure metadata, falling back to defaults");
Self::default_azure_network_ranges()
}
}
Err(e) => {
warn!("Failed to fetch Azure network metadata: {}", e);
Self::default_azure_network_ranges()
}
}
}
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
self.fetch_azure_ip_ranges().await
}
}
impl AzureMetadataFetcher {
/// Returns a set of default Azure VNet ranges as a fallback.
fn default_azure_network_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let ranges = vec![
"10.0.0.0/8", // Large VNets
"172.16.0.0/12", // Medium VNets
"192.168.0.0/16", // Small VNets
"100.64.0.0/10", // Azure reserved range
"192.0.0.0/24", // Azure reserved
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(ipnetwork::IpNetwork::from_str).collect();
match networks {
Ok(networks) => {
debug!("Using default Azure VNet network ranges");
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse default Azure network ranges: {}", e))),
}
}
}
@@ -0,0 +1,309 @@
// 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.
//! Google Cloud Platform (GCP) metadata fetching implementation for identifying trusted proxy ranges.
use async_trait::async_trait;
use reqwest::Client;
use serde::Deserialize;
use std::str::FromStr;
use std::time::Duration;
use tracing::{debug, info, warn};
use crate::AppError;
use crate::CloudMetadataFetcher;
/// Fetcher for GCP-specific metadata.
#[derive(Debug, Clone)]
pub struct GcpMetadataFetcher {
client: Client,
metadata_endpoint: String,
}
impl GcpMetadataFetcher {
/// Creates a new `GcpMetadataFetcher`.
pub fn new(timeout: Duration) -> Self {
let client = Client::builder().timeout(timeout).build().unwrap_or_else(|_| Client::new());
Self {
client,
metadata_endpoint: "http://metadata.google.internal".to_string(),
}
}
/// Retrieves metadata from the GCP Compute Engine metadata server.
async fn get_metadata(&self, path: &str) -> Result<String, AppError> {
let url = format!("{}/computeMetadata/v1/{}", self.metadata_endpoint, path);
debug!("Fetching GCP metadata from: {}", url);
match self.client.get(&url).header("Metadata-Flavor", "Google").send().await {
Ok(response) => {
if response.status().is_success() {
let text = response
.text()
.await
.map_err(|e| AppError::cloud(format!("Failed to read GCP metadata response: {}", e)))?;
Ok(text)
} else {
debug!("GCP metadata request failed with status: {}", response.status());
Err(AppError::cloud(format!("GCP metadata API returned status: {}", response.status())))
}
}
Err(e) => {
debug!("GCP metadata request failed: {}", e);
Err(AppError::cloud(format!("GCP metadata request failed: {}", e)))
}
}
}
/// Converts a dotted-decimal subnet mask to a CIDR prefix length.
fn subnet_mask_to_prefix_length(mask: &str) -> Result<u8, AppError> {
let parts: Vec<&str> = mask.split('.').collect();
if parts.len() != 4 {
return Err(AppError::cloud(format!("Invalid subnet mask format: {}", mask)));
}
let mut prefix_length = 0;
for part in parts {
let octet: u8 = part
.parse()
.map_err(|_| AppError::cloud(format!("Invalid octet in subnet mask: {}", part)))?;
let mut remaining = octet;
while remaining > 0 {
if remaining & 0x80 == 0x80 {
prefix_length += 1;
remaining <<= 1;
} else {
break;
}
}
if remaining != 0 {
return Err(AppError::cloud("Non-contiguous subnet mask detected"));
}
}
Ok(prefix_length)
}
}
#[async_trait]
impl CloudMetadataFetcher for GcpMetadataFetcher {
fn provider_name(&self) -> &str {
"gcp"
}
async fn fetch_network_cidrs(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
// Attempt to list network interfaces from GCP metadata.
match self.get_metadata("instance/network-interfaces/").await {
Ok(interfaces_metadata) => {
let interface_indices: Vec<usize> = interfaces_metadata
.lines()
.filter_map(|line| {
let line = line.trim().trim_end_matches('/');
if line.chars().all(|c| c.is_ascii_digit()) {
line.parse().ok()
} else {
None
}
})
.collect();
if interface_indices.is_empty() {
warn!("No network interfaces found in GCP metadata");
return Self::default_gcp_network_ranges();
}
let mut cidrs = Vec::new();
for index in interface_indices {
// Try to get IP and subnet mask for each interface.
let ip_path = format!("instance/network-interfaces/{}/ip", index);
let mask_path = format!("instance/network-interfaces/{}/subnetmask", index);
match tokio::try_join!(self.get_metadata(&ip_path), self.get_metadata(&mask_path)) {
Ok((ip, mask)) => {
let ip = ip.trim();
let mask = mask.trim();
if let (Ok(ip_addr), Ok(prefix_len)) =
(std::net::Ipv4Addr::from_str(ip), Self::subnet_mask_to_prefix_length(mask))
{
let cidr_str = format!("{}/{}", ip_addr, prefix_len);
if let Ok(network) = ipnetwork::IpNetwork::from_str(&cidr_str) {
cidrs.push(network);
}
}
}
Err(e) => {
debug!("Failed to get IP/mask for GCP interface {}: {}", index, e);
}
}
}
if cidrs.is_empty() {
warn!("Could not determine network CIDRs from GCP metadata, falling back to defaults");
Self::default_gcp_network_ranges()
} else {
info!("Successfully fetched {} network CIDRs from GCP metadata", cidrs.len());
Ok(cidrs)
}
}
Err(e) => {
warn!("Failed to fetch GCP network metadata: {}", e);
Self::default_gcp_network_ranges()
}
}
}
async fn fetch_public_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
self.fetch_gcp_ip_ranges().await
}
}
impl GcpMetadataFetcher {
/// Fetches GCP public IP ranges from the official Google source.
async fn fetch_gcp_ip_ranges(&self) -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let url = "https://www.gstatic.com/ipranges/cloud.json";
#[derive(Debug, Deserialize)]
struct GcpIpRanges {
prefixes: Vec<GcpPrefix>,
}
#[derive(Debug, Deserialize)]
struct GcpPrefix {
ipv4_prefix: Option<String>,
}
debug!("Fetching GCP IP ranges from: {}", url);
match self.client.get(url).timeout(Duration::from_secs(10)).send().await {
Ok(response) => {
if response.status().is_success() {
let ip_ranges: GcpIpRanges = response
.json()
.await
.map_err(|e| AppError::cloud(format!("Failed to parse GCP IP ranges JSON: {}", e)))?;
let mut networks = Vec::new();
for prefix in ip_ranges.prefixes {
if let Some(ipv4_prefix) = prefix.ipv4_prefix
&& let Ok(network) = ipnetwork::IpNetwork::from_str(&ipv4_prefix)
{
networks.push(network);
}
}
info!("Successfully fetched {} GCP public IP ranges", networks.len());
Ok(networks)
} else {
debug!("Failed to fetch GCP IP ranges: HTTP {}", response.status());
Self::default_gcp_ip_ranges()
}
}
Err(e) => {
debug!("Failed to fetch GCP IP ranges: {}", e);
Self::default_gcp_ip_ranges()
}
}
}
/// Returns a set of default GCP public IP ranges as a fallback.
fn default_gcp_ip_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let ranges = vec![
"8.34.208.0/20",
"8.35.192.0/20",
"8.35.208.0/20",
"23.236.48.0/20",
"23.251.128.0/19",
"34.0.0.0/15",
"34.2.0.0/16",
"34.3.0.0/23",
"34.4.0.0/14",
"34.8.0.0/13",
"34.16.0.0/12",
"34.32.0.0/11",
"34.64.0.0/10",
"34.128.0.0/10",
"35.184.0.0/13",
"35.192.0.0/14",
"35.196.0.0/15",
"35.198.0.0/16",
"35.200.0.0/13",
"35.208.0.0/12",
"35.224.0.0/12",
"35.240.0.0/13",
"104.154.0.0/15",
"104.196.0.0/14",
"107.167.160.0/19",
"107.178.192.0/18",
"108.59.80.0/20",
"108.170.192.0/18",
"108.177.0.0/17",
"130.211.0.0/16",
"136.112.0.0/12",
"142.250.0.0/15",
"146.148.0.0/17",
"172.217.0.0/16",
"172.253.0.0/16",
"173.194.0.0/16",
"192.178.0.0/15",
"209.85.128.0/17",
"216.58.192.0/19",
"216.239.32.0/19",
"2001:4860::/32",
"2404:6800::/32",
"2600:1900::/28",
"2607:f8b0::/32",
"2620:15c::/36",
"2800:3f0::/32",
"2a00:1450::/32",
"2c0f:fb50::/32",
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(ipnetwork::IpNetwork::from_str).collect();
match networks {
Ok(networks) => {
debug!("Using default GCP public IP ranges");
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse default GCP ranges: {}", e))),
}
}
/// Returns a set of default GCP VPC ranges as a fallback.
fn default_gcp_network_ranges() -> Result<Vec<ipnetwork::IpNetwork>, AppError> {
let ranges = vec![
"10.0.0.0/8", // Large VPCs
"172.16.0.0/12", // Medium VPCs
"192.168.0.0/16", // Small VPCs
"100.64.0.0/10", // GCP reserved range
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(ipnetwork::IpNetwork::from_str).collect();
match networks {
Ok(networks) => {
debug!("Using default GCP VPC network ranges");
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse default GCP network ranges: {}", e))),
}
}
}
@@ -0,0 +1,26 @@
// 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 metadata fetching
//!
//! This module contains implementations for fetching metadata
//! from various cloud providers.
mod aws;
mod azure;
mod gcp;
pub use aws::*;
pub use azure::*;
pub use gcp::*;
+26
View File
@@ -0,0 +1,26 @@
// 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 service integration module
//!
//! This module provides integration with various cloud providers
//! for automatic IP range detection and metadata fetching.
mod detector;
pub mod metadata;
mod ranges;
pub use detector::*;
pub use metadata::*;
pub use ranges::*;
+216
View File
@@ -0,0 +1,216 @@
// 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.
//! Static and dynamic IP range definitions for various cloud providers.
use std::str::FromStr;
use std::time::Duration;
use ipnetwork::IpNetwork;
use reqwest::Client;
use tracing::{debug, info};
use crate::error::AppError;
/// Utility for fetching Cloudflare IP ranges.
pub struct CloudflareIpRanges;
impl CloudflareIpRanges {
/// Returns a static list of Cloudflare IP ranges.
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
let ranges = vec![
// IPv4 ranges
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"104.16.0.0/13",
"104.24.0.0/14",
"108.162.192.0/18",
"131.0.72.0/22",
"141.101.64.0/18",
"162.158.0.0/15",
"172.64.0.0/13",
"173.245.48.0/20",
"188.114.96.0/20",
"190.93.240.0/20",
"197.234.240.0/22",
"198.41.128.0/17",
// IPv6 ranges
"2400:cb00::/32",
"2606:4700::/32",
"2803:f800::/32",
"2405:b500::/32",
"2405:8100::/32",
"2a06:98c0::/29",
"2c0f:f248::/32",
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(IpNetwork::from_str).collect();
match networks {
Ok(networks) => {
info!("Loaded {} static Cloudflare IP ranges", networks.len());
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse static Cloudflare IP ranges: {}", e))),
}
}
/// Fetches the latest Cloudflare IP ranges from their official API.
pub async fn fetch_from_api() -> Result<Vec<IpNetwork>, AppError> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()
.map_err(|e| AppError::cloud(format!("Failed to create HTTP client: {}", e)))?;
let urls = ["https://www.cloudflare.com/ips-v4", "https://www.cloudflare.com/ips-v6"];
let mut all_ranges = Vec::new();
for url in urls {
match client.get(url).send().await {
Ok(response) => {
if response.status().is_success() {
let text = response
.text()
.await
.map_err(|e| AppError::cloud(format!("Failed to read response from {}: {}", url, e)))?;
let ranges: Result<Vec<_>, _> = text
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.map(IpNetwork::from_str)
.collect();
match ranges {
Ok(mut networks) => {
debug!("Fetched {} IP ranges from {}", networks.len(), url);
all_ranges.append(&mut networks);
}
Err(e) => {
debug!("Failed to parse IP ranges from {}: {}", url, e);
}
}
} else {
debug!("Failed to fetch IP ranges from {}: HTTP {}", url, response.status());
}
}
Err(e) => {
debug!("Failed to fetch from {}: {}", url, e);
}
}
}
if all_ranges.is_empty() {
// Fallback to static list if API requests fail.
Self::fetch().await
} else {
info!("Successfully fetched {} Cloudflare IP ranges from API", all_ranges.len());
Ok(all_ranges)
}
}
}
/// Utility for fetching DigitalOcean IP ranges.
pub struct DigitalOceanIpRanges;
impl DigitalOceanIpRanges {
/// Returns a static list of DigitalOcean IP ranges.
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
let ranges = vec![
// Datacenter IP ranges
"64.227.0.0/16",
"138.197.0.0/16",
"139.59.0.0/16",
"157.230.0.0/16",
"159.65.0.0/16",
"167.99.0.0/16",
"178.128.0.0/16",
"206.189.0.0/16",
"207.154.0.0/16",
"209.97.0.0/16",
// Load Balancer IP ranges
"144.126.0.0/16",
"143.198.0.0/16",
"161.35.0.0/16",
];
let networks: Result<Vec<_>, _> = ranges.into_iter().map(IpNetwork::from_str).collect();
match networks {
Ok(networks) => {
info!("Loaded {} static DigitalOcean IP ranges", networks.len());
Ok(networks)
}
Err(e) => Err(AppError::cloud(format!("Failed to parse static DigitalOcean IP ranges: {}", e))),
}
}
}
/// Utility for fetching Google Cloud IP ranges.
pub struct GoogleCloudIpRanges;
impl GoogleCloudIpRanges {
/// Fetches the latest Google Cloud IP ranges from their official source.
pub async fn fetch() -> Result<Vec<IpNetwork>, AppError> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()
.map_err(|e| AppError::cloud(format!("Failed to create HTTP client: {}", e)))?;
let url = "https://www.gstatic.com/ipranges/cloud.json";
#[derive(Debug, serde::Deserialize)]
struct GoogleIpRanges {
prefixes: Vec<GooglePrefix>,
}
#[derive(Debug, serde::Deserialize)]
struct GooglePrefix {
ipv4_prefix: Option<String>,
}
match client.get(url).send().await {
Ok(response) => {
if response.status().is_success() {
let ip_ranges: GoogleIpRanges = response
.json()
.await
.map_err(|e| AppError::cloud(format!("Failed to parse Google IP ranges JSON: {}", e)))?;
let mut networks = Vec::new();
for prefix in ip_ranges.prefixes {
if let Some(ipv4_prefix) = prefix.ipv4_prefix
&& let Ok(network) = IpNetwork::from_str(&ipv4_prefix)
{
networks.push(network);
}
}
info!("Successfully fetched {} Google Cloud IP ranges from API", networks.len());
Ok(networks)
} else {
debug!("Failed to fetch Google IP ranges: HTTP {}", response.status());
Ok(Vec::new())
}
}
Err(e) => {
debug!("Failed to fetch Google IP ranges: {}", e);
Ok(Vec::new())
}
}
}
}