securitydept-realip 0.2.0

Real IP of SecurityDept, a layered authentication and authorization toolkit built as reusable Rust crates.
Documentation
use std::sync::Arc;

use bollard::{API_DEFAULT_VERSION, Docker, models::Ipam, query_parameters::InspectNetworkOptions};
use ipnet::IpNet;

use super::string_list;
use crate::{
    config::CustomProviderConfig,
    error::{RealIpError, RealIpResult},
    extension::{CustomProviderFactory, DynamicProvider, ProviderLoadFuture},
};

pub(crate) struct DockerProviderFactory;

impl CustomProviderFactory for DockerProviderFactory {
    fn kind(&self) -> &'static str {
        "docker-provider"
    }

    fn create(&self, config: &CustomProviderConfig) -> RealIpResult<Arc<dyn DynamicProvider>> {
        let host = config
            .extra
            .get("host")
            .and_then(|value| value.as_str())
            .map(str::to_string);
        let networks = string_list(config, "networks");
        let docker =
            connect_docker(host.as_deref()).map_err(|error| RealIpError::ProviderLoad {
                provider: config.name.clone(),
                details: error,
            })?;

        Ok(Arc::new(DockerProvider {
            provider_name: config.name.clone(),
            docker,
            networks,
        }))
    }
}

struct DockerProvider {
    provider_name: String,
    docker: Docker,
    networks: Vec<String>,
}

impl DynamicProvider for DockerProvider {
    fn load<'a>(&'a self) -> ProviderLoadFuture<'a> {
        Box::pin(async move {
            let ipams: Vec<Option<Ipam>> = if self.networks.is_empty() {
                self.docker
                    .list_networks(None::<bollard::query_parameters::ListNetworksOptions>)
                    .await
                    .map_err(|error| RealIpError::ProviderLoad {
                        provider: self.provider_name.clone(),
                        details: error.to_string(),
                    })?
                    .into_iter()
                    .map(|n| n.ipam)
                    .collect()
            } else {
                let mut items = Vec::new();
                for network in &self.networks {
                    let item = self
                        .docker
                        .inspect_network(network, None::<InspectNetworkOptions>)
                        .await
                        .map_err(|error| RealIpError::ProviderLoad {
                            provider: self.provider_name.clone(),
                            details: error.to_string(),
                        })?;
                    items.push(item.ipam);
                }
                items
            };

            extract_docker_subnets(&self.provider_name, &ipams)
        })
    }
}

fn connect_docker(host: Option<&str>) -> Result<Docker, String> {
    match host {
        None => Docker::connect_with_local_defaults().map_err(|error| error.to_string()),
        Some(host) if host.starts_with("unix://") => {
            Docker::connect_with_local(host.trim_start_matches("unix://"), 120, API_DEFAULT_VERSION)
                .map_err(|error| error.to_string())
        }
        Some(host) => Docker::connect_with_http(host, 120, API_DEFAULT_VERSION)
            .map_err(|error| error.to_string()),
    }
}

fn extract_docker_subnets(provider: &str, ipams: &[Option<Ipam>]) -> RealIpResult<Vec<IpNet>> {
    let mut cidrs = Vec::new();
    for ipam in ipams {
        let Some(ipam) = ipam else {
            continue;
        };
        let Some(configs) = &ipam.config else {
            continue;
        };
        for config in configs {
            let Some(subnet) = &config.subnet else {
                continue;
            };
            cidrs.push(
                subnet
                    .parse::<IpNet>()
                    .map_err(|_| RealIpError::ProviderLoad {
                        provider: provider.to_string(),
                        details: format!("invalid docker subnet `{subnet}`"),
                    })?,
            );
        }
    }
    Ok(cidrs)
}

#[cfg(test)]
mod tests {
    use bollard::models::{Ipam, IpamConfig};

    use super::extract_docker_subnets;

    #[test]
    fn extract_docker_subnets_parses_all_valid_subnets() {
        let cidrs = extract_docker_subnets(
            "docker-test",
            &[Some(Ipam {
                config: Some(vec![
                    IpamConfig {
                        subnet: Some("10.0.0.0/24".to_string()),
                        ..Default::default()
                    },
                    IpamConfig {
                        subnet: Some("fd00::/64".to_string()),
                        ..Default::default()
                    },
                ]),
                ..Default::default()
            })],
        )
        .unwrap();

        assert_eq!(cidrs.len(), 2);
        assert_eq!(cidrs[0].to_string(), "10.0.0.0/24");
        assert_eq!(cidrs[1].to_string(), "fd00::/64");
    }
}