mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-31 09:18:28 +00:00
9059a9c68d
* refactor(logging): standardize concurrency and proxy events * chore(logging): extend guardrails for concurrency and proxies * feat(skill): add rustfs logging governance skill
446 lines
17 KiB
Rust
446 lines
17 KiB
Rust
// 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!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "metadata_request",
|
|
path = %path,
|
|
"trusted proxy cloud metadata request started"
|
|
);
|
|
|
|
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!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "metadata_request",
|
|
path = %path,
|
|
result = "http_error",
|
|
status = %response.status(),
|
|
"trusted proxy cloud metadata request failed"
|
|
);
|
|
Err(AppError::cloud(format!("GCP metadata API returned status: {}", response.status())))
|
|
}
|
|
}
|
|
Err(e) => {
|
|
debug!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "metadata_request",
|
|
path = %path,
|
|
result = "request_failed",
|
|
error = %e,
|
|
"trusted proxy cloud metadata request failed"
|
|
);
|
|
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!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "network_cidrs",
|
|
source = "metadata",
|
|
result = "fallback",
|
|
reason = "no_interfaces",
|
|
"trusted proxy cloud metadata fallback applied"
|
|
);
|
|
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!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "network_interface",
|
|
interface_index = index,
|
|
result = "request_failed",
|
|
error = %e,
|
|
"trusted proxy cloud metadata request failed"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if cidrs.is_empty() {
|
|
warn!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "network_cidrs",
|
|
source = "metadata",
|
|
result = "fallback",
|
|
reason = "empty_metadata",
|
|
"trusted proxy cloud metadata fallback applied"
|
|
);
|
|
Self::default_gcp_network_ranges()
|
|
} else {
|
|
info!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "network_cidrs",
|
|
source = "metadata",
|
|
result = "loaded",
|
|
range_count = cidrs.len(),
|
|
"trusted proxy cloud metadata loaded"
|
|
);
|
|
Ok(cidrs)
|
|
}
|
|
}
|
|
Err(e) => {
|
|
warn!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "network_cidrs",
|
|
source = "metadata",
|
|
result = "fallback",
|
|
error = %e,
|
|
"trusted proxy cloud metadata fallback applied"
|
|
);
|
|
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!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "public_ip_ranges",
|
|
source = %url,
|
|
"trusted proxy cloud metadata request started"
|
|
);
|
|
|
|
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!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "public_ip_ranges",
|
|
source = "api",
|
|
result = "loaded",
|
|
range_count = networks.len(),
|
|
"trusted proxy cloud metadata loaded"
|
|
);
|
|
Ok(networks)
|
|
} else {
|
|
debug!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "public_ip_ranges",
|
|
source = %url,
|
|
result = "http_error",
|
|
status = %response.status(),
|
|
"trusted proxy cloud metadata request failed"
|
|
);
|
|
Self::default_gcp_ip_ranges()
|
|
}
|
|
}
|
|
Err(e) => {
|
|
debug!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "public_ip_ranges",
|
|
source = %url,
|
|
result = "request_failed",
|
|
error = %e,
|
|
"trusted proxy cloud metadata request failed"
|
|
);
|
|
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!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "public_ip_ranges",
|
|
result = "fallback",
|
|
source = "default_ranges",
|
|
range_count = networks.len(),
|
|
"trusted proxy cloud metadata fallback applied"
|
|
);
|
|
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!(
|
|
event = "trusted_proxies.cloud_metadata",
|
|
component = "trusted_proxies",
|
|
subsystem = "gcp_metadata",
|
|
provider = "gcp",
|
|
operation = "network_cidrs",
|
|
result = "fallback",
|
|
source = "default_ranges",
|
|
range_count = networks.len(),
|
|
"trusted proxy cloud metadata fallback applied"
|
|
);
|
|
Ok(networks)
|
|
}
|
|
Err(e) => Err(AppError::cloud(format!("Failed to parse default GCP network ranges: {}", e))),
|
|
}
|
|
}
|
|
}
|