use std::collections::HashMap;
use std::net::Ipv4Addr;
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ExposeHostMapping {
pub host_port: u16,
pub guest_port: u16,
}
#[derive(Debug, Clone)]
pub struct ProxyConfig {
pub dns_resolver: Ipv4Addr,
pub secrets: HashMap<String, SecretConfig>,
pub network: NetworkConfig,
pub expose_host: Vec<ExposeHostMapping>,
pub config_dir: Option<PathBuf>,
}
impl Default for ProxyConfig {
fn default() -> Self {
Self {
dns_resolver: Ipv4Addr::new(8, 8, 8, 8),
secrets: HashMap::new(),
network: NetworkConfig::default(),
expose_host: Vec::new(),
config_dir: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SecretConfig {
pub from: Option<String>,
pub command: Option<Vec<String>>,
pub ttl: Option<Duration>,
pub hosts: Vec<String>,
pub value: Option<String>,
}
impl SecretConfig {
pub fn from_env(var: impl Into<String>, hosts: Vec<String>) -> Self {
Self {
from: Some(var.into()),
hosts,
..Default::default()
}
}
pub fn from_command(command: Vec<String>, hosts: Vec<String>) -> Self {
Self {
command: Some(command),
hosts,
..Default::default()
}
}
pub fn matches_domain(&self, domain: &str) -> bool {
self.hosts
.iter()
.any(|pattern| domain_matches(pattern, domain))
}
}
#[derive(Debug, Clone, Default)]
pub struct NetworkConfig {
pub allow: Vec<String>,
}
impl NetworkConfig {
pub fn has_allowlist(&self) -> bool {
!self.allow.is_empty()
}
}
impl ProxyConfig {
pub fn is_domain_allowed(&self, domain: &str) -> bool {
if self.network.allow.is_empty() {
return true;
}
self.network
.allow
.iter()
.any(|pattern| domain_matches(pattern, domain))
}
pub fn exposed_host_port(&self, dst_ip: Ipv4Addr, guest_port: u16) -> Option<u16> {
const GATEWAY: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
if dst_ip != GATEWAY {
return None;
}
self.expose_host
.iter()
.find(|m| m.guest_port == guest_port)
.map(|m| m.host_port)
}
}
pub(crate) fn domain_matches(pattern: &str, domain: &str) -> bool {
if pattern == "*" {
true
} else if let Some(suffix) = pattern.strip_prefix("*.") {
domain.ends_with(suffix)
&& domain.len() > suffix.len()
&& domain.as_bytes()[domain.len() - suffix.len() - 1] == b'.'
} else {
pattern == domain
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exposed_host_port() {
use std::net::Ipv4Addr;
let config = ProxyConfig {
expose_host: vec![
ExposeHostMapping {
host_port: 3000,
guest_port: 8080,
},
ExposeHostMapping {
host_port: 5432,
guest_port: 5432,
},
],
..Default::default()
};
assert_eq!(
config.exposed_host_port(Ipv4Addr::new(10, 0, 0, 1), 8080),
Some(3000)
);
assert_eq!(
config.exposed_host_port(Ipv4Addr::new(10, 0, 0, 1), 5432),
Some(5432)
);
assert_eq!(
config.exposed_host_port(Ipv4Addr::new(10, 0, 0, 1), 9999),
None
);
assert_eq!(
config.exposed_host_port(Ipv4Addr::new(1, 2, 3, 4), 8080),
None
);
}
#[test]
fn test_domain_matching() {
assert!(domain_matches("*", "anything.com"));
assert!(domain_matches("*", "api.example.com"));
assert!(domain_matches("example.com", "example.com"));
assert!(!domain_matches("example.com", "api.example.com"));
assert!(domain_matches("*.example.com", "api.example.com"));
assert!(domain_matches("*.example.com", "deep.api.example.com"));
assert!(!domain_matches("*.example.com", "example.com"));
assert!(!domain_matches("*.example.com", "notexample.com"));
}
#[test]
fn test_has_allowlist() {
let empty = NetworkConfig::default();
assert!(!empty.has_allowlist());
let with_entries = NetworkConfig {
allow: vec!["api.example.com".into()],
};
assert!(with_entries.has_allowlist());
}
#[test]
fn default_dns_resolver_is_google_public_dns() {
assert_eq!(
ProxyConfig::default().dns_resolver,
Ipv4Addr::new(8, 8, 8, 8)
);
}
}