use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use ipnetwork::{Ipv4Network, Ipv6Network};
use serde::{Deserialize, Serialize};
use crate::dns::Nameserver;
use crate::policy::NetworkPolicy;
use crate::secrets::config::SecretsConfig;
use crate::tls::TlsConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub interface: InterfaceOverrides,
#[serde(default)]
pub ports: Vec<PublishedPort>,
#[serde(default)]
pub policy: NetworkPolicy,
#[serde(default)]
pub dns: DnsConfig,
#[serde(default)]
pub tls: TlsConfig,
#[serde(default)]
pub secrets: SecretsConfig,
#[serde(default)]
pub max_connections: Option<usize>,
#[serde(default)]
pub trust_host_cas: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct InterfaceOverrides {
#[serde(default)]
pub mac: Option<[u8; 6]>,
#[serde(default)]
pub mtu: Option<u16>,
#[serde(default)]
pub ipv4_address: Option<Ipv4Addr>,
#[serde(default)]
pub ipv4_pool: Option<Ipv4Network>,
#[serde(default)]
pub ipv6_address: Option<Ipv6Addr>,
#[serde(default)]
pub ipv6_pool: Option<Ipv6Network>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DnsConfig {
#[serde(default = "default_true")]
pub rebind_protection: bool,
#[serde(default)]
pub nameservers: Vec<Nameserver>,
#[serde(default = "default_query_timeout_ms")]
pub query_timeout_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublishedPort {
pub host_port: u16,
pub guest_port: u16,
#[serde(default)]
pub protocol: PortProtocol,
#[serde(default = "default_host_bind")]
pub host_bind: IpAddr,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum PortProtocol {
#[default]
#[serde(rename = "tcp", alias = "Tcp")]
Tcp,
#[serde(rename = "udp", alias = "Udp")]
Udp,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
enabled: true,
interface: InterfaceOverrides::default(),
ports: Vec::new(),
policy: NetworkPolicy::default(),
dns: DnsConfig::default(),
tls: TlsConfig::default(),
secrets: SecretsConfig::default(),
max_connections: None,
trust_host_cas: false,
}
}
}
impl Default for DnsConfig {
fn default() -> Self {
Self {
rebind_protection: true,
nameservers: Vec::new(),
query_timeout_ms: default_query_timeout_ms(),
}
}
}
fn default_true() -> bool {
true
}
fn default_host_bind() -> IpAddr {
IpAddr::V4(Ipv4Addr::LOCALHOST)
}
fn default_query_timeout_ms() -> u64 {
5000
}
#[cfg(test)]
mod tests {
use super::{InterfaceOverrides, NetworkConfig, PortProtocol};
use crate::dns::Nameserver;
use crate::policy::{Destination, NetworkPolicy, Rule};
#[test]
fn engine_network_subdocs_round_trip_through_wire_types() {
let mut config = NetworkConfig::default();
let mut policy = NetworkPolicy::public_only()
.allow_domain("example.com")
.expect("valid domain")
.allow_domain_suffix("staging.example.com")
.expect("valid suffix");
policy.rules.push(Rule::allow_egress(Destination::Cidr(
"151.101.0.0/16".parse().unwrap(),
)));
config.policy = policy;
config.dns.nameservers = vec![
"1.1.1.1:53".parse::<Nameserver>().unwrap(),
"dns.google".parse::<Nameserver>().unwrap(),
];
config.interface.ipv4_address = Some("172.16.0.2".parse().unwrap());
config.interface.ipv4_pool = Some("172.16.0.0/12".parse().unwrap());
config.interface.mac = Some([0x02, 0, 0, 0, 0, 0x01]);
let policy_json = serde_json::to_value(&config.policy).unwrap();
let dns_json = serde_json::to_value(&config.dns).unwrap();
let iface_json = serde_json::to_value(&config.interface).unwrap();
let wire_policy: microsandbox_types::NetworkPolicy =
serde_json::from_value(policy_json.clone()).unwrap();
let wire_dns: microsandbox_types::DnsConfig =
serde_json::from_value(dns_json.clone()).unwrap();
assert_eq!(policy_json, serde_json::to_value(&wire_policy).unwrap());
assert_eq!(dns_json, serde_json::to_value(&wire_dns).unwrap());
let wire_iface: microsandbox_types::InterfaceOverrides =
serde_json::from_value(iface_json.clone()).unwrap();
let back: InterfaceOverrides =
serde_json::from_value(serde_json::to_value(&wire_iface).unwrap()).unwrap();
assert_eq!(iface_json, serde_json::to_value(&back).unwrap());
assert_eq!(
serde_json::to_string(&Destination::DomainSuffix(
"staging.example.com".parse().unwrap()
))
.unwrap(),
r#"{"domain_suffix":"staging.example.com"}"#
);
let legacy: microsandbox_types::Destination =
serde_json::from_str(r#"{"domain-suffix":"old.example.com"}"#).unwrap();
assert!(matches!(
legacy,
microsandbox_types::Destination::DomainSuffix(_)
));
let legacy_group: microsandbox_types::DestinationGroup =
serde_json::from_str(r#""link-local""#).unwrap();
assert_eq!(
legacy_group,
microsandbox_types::DestinationGroup::LinkLocal
);
}
#[test]
fn port_protocol_serializes_lowercase_and_accepts_legacy_case() {
assert_eq!(
serde_json::to_string(&PortProtocol::Tcp).unwrap(),
"\"tcp\""
);
assert_eq!(
serde_json::to_string(&PortProtocol::Udp).unwrap(),
"\"udp\""
);
assert_eq!(
serde_json::from_str::<PortProtocol>("\"Tcp\"").unwrap(),
PortProtocol::Tcp
);
assert_eq!(
serde_json::from_str::<PortProtocol>("\"Udp\"").unwrap(),
PortProtocol::Udp
);
}
}