use microsandbox_types::SecretSource;
use zeroize::Zeroizing;
use crate::proxy::{
OutboundProxy, OutboundProxyBuildError, ResolvedOutboundProxy, ResolvedSocks5Credentials,
};
use super::types::{NetworkConfig, ResolvedNetworkConfig};
#[doc(hidden)]
pub trait NetworkSecretResolver {
fn resolve(
&self,
source: &SecretSource,
) -> Result<Zeroizing<String>, NetworkSecretResolveError>;
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, Default)]
pub struct EnvNetworkSecretResolver;
#[doc(hidden)]
#[derive(Debug, thiserror::Error)]
pub enum NetworkSecretResolveError {
#[error("host environment variable {var} is not set")]
MissingEnvironmentVariable {
var: String,
},
#[error("host environment variable {var} is empty")]
EmptyEnvironmentVariable {
var: String,
},
#[error("store-backed secret sources are not supported yet")]
UnsupportedStoreSource,
#[error("{message}")]
ResolutionFailed {
message: String,
},
}
#[doc(hidden)]
#[derive(Debug, thiserror::Error)]
pub enum NetworkConfigResolveError {
#[error("{subject}: {source}")]
SecretSource {
subject: String,
#[source]
source: NetworkSecretResolveError,
},
#[error(transparent)]
OutboundProxy(#[from] OutboundProxyBuildError),
}
impl NetworkConfig {
#[doc(hidden)]
pub fn resolve(
mut self,
resolver: &impl NetworkSecretResolver,
) -> Result<ResolvedNetworkConfig, NetworkConfigResolveError> {
if !self.enabled {
return Ok(ResolvedNetworkConfig::new(self, None));
}
for secret in &mut self.secrets.secrets {
if let Some(source) = &secret.source {
secret.value =
Self::resolve_source(resolver, &format!("secret {}", secret.env_var), source)?;
}
}
let outbound_proxy_credentials = match self.outbound_proxy.as_ref() {
Some(OutboundProxy::Socks5 {
credentials: Some(credentials),
..
}) => {
let password =
Self::resolve_source(resolver, "SOCKS5 proxy", credentials.password_source())?;
Some(ResolvedSocks5Credentials::new(
credentials.username(),
password.as_str(),
))
}
_ => None,
};
let outbound_proxy =
ResolvedOutboundProxy::build(self.outbound_proxy.as_ref(), outbound_proxy_credentials)?;
Ok(ResolvedNetworkConfig::new(self, outbound_proxy))
}
fn resolve_source(
resolver: &impl NetworkSecretResolver,
subject: &str,
source: &SecretSource,
) -> Result<Zeroizing<String>, NetworkConfigResolveError> {
resolver
.resolve(source)
.map_err(|source| NetworkConfigResolveError::SecretSource {
subject: subject.to_string(),
source,
})
}
}
impl NetworkSecretResolver for EnvNetworkSecretResolver {
fn resolve(
&self,
source: &SecretSource,
) -> Result<Zeroizing<String>, NetworkSecretResolveError> {
match source {
SecretSource::Store { .. } => Err(NetworkSecretResolveError::UnsupportedStoreSource),
SecretSource::Env { var } => {
let value = std::env::var(var).map_err(|_| {
NetworkSecretResolveError::MissingEnvironmentVariable { var: var.clone() }
})?;
if value.is_empty() {
return Err(NetworkSecretResolveError::EmptyEnvironmentVariable {
var: var.clone(),
});
}
Ok(Zeroizing::new(value))
}
}
}
}
#[cfg(test)]
mod tests {
use microsandbox_types::SecretSource;
use super::EnvNetworkSecretResolver;
use crate::config::NetworkConfig;
use crate::proxy::{OutboundProxyBuilder, OutboundProxyConfig};
#[test]
fn network_resolution_reads_authenticated_proxy_credentials() {
const PASSWORD_VAR: &str = "MSB_NETWORK_CONFIG_RESOLVE_TEST_SOCKS5_PASSWORD";
let config = NetworkConfig {
outbound_proxy: Some(
OutboundProxyBuilder::new()
.socks5("127.0.0.1:1080")
.credentials("sandbox", SecretSource::env(PASSWORD_VAR))
.build()
.unwrap(),
),
..Default::default()
};
assert!(config.clone().resolve(&EnvNetworkSecretResolver).is_err());
unsafe { std::env::set_var(PASSWORD_VAR, "password") };
let resolved = config.resolve(&EnvNetworkSecretResolver).unwrap();
unsafe { std::env::remove_var(PASSWORD_VAR) };
assert!(format!("{resolved:?}").contains("[REDACTED]"));
}
}