mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-02 10:18:10 +00:00
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:
@@ -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::*;
|
||||
@@ -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::*;
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user