use std::collections::{HashMap, HashSet};
use std::io::ErrorKind;
use std::net::{IpAddr, Shutdown, SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock, RwLock};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use super::reconcile::StatusReporter;
use super::state::{DeploymentRecord, LocalState};
use crate::broker::node_sync::OwnerMeshIps;
const ACCEPT_POLL: Duration = Duration::from_millis(100);
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
pub trait MeshExposer {
fn expose(
&self,
id: &str,
listen_ip: &str,
target: &str,
preferred_port: u16,
) -> Option<String>;
fn release(&self, id: &str);
fn exposed_ids(&self) -> Vec<String>;
}
struct Forward {
listen: SocketAddr,
target: String,
stop: Arc<AtomicBool>,
accept_loop: JoinHandle<()>,
}
impl Forward {
fn close(self) {
self.stop.store(true, Ordering::Relaxed);
let _ = self.accept_loop.join();
}
}
type Policy = Arc<dyn Fn(IpAddr, IpAddr) -> bool + Send + Sync>;
pub struct TcpForwarder {
forwards: Mutex<HashMap<String, Forward>>,
policy: Policy,
}
impl Default for TcpForwarder {
fn default() -> Self {
TcpForwarder::with_policy(|peer, own| peer == own)
}
}
impl TcpForwarder {
pub fn with_policy(policy: impl Fn(IpAddr, IpAddr) -> bool + Send + Sync + 'static) -> Self {
TcpForwarder {
forwards: Mutex::default(),
policy: Arc::new(policy),
}
}
fn forwards(&self) -> MutexGuard<'_, HashMap<String, Forward>> {
self.forwards
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
impl MeshExposer for TcpForwarder {
fn expose(
&self,
id: &str,
listen_ip: &str,
target: &str,
preferred_port: u16,
) -> Option<String> {
let ip: IpAddr = listen_ip.parse().ok()?;
let mut forwards = self.forwards();
let mut port = preferred_port;
if let Some(existing) = forwards.remove(id) {
if existing.target == target && existing.listen.ip() == ip {
let endpoint = existing.listen.to_string();
forwards.insert(id.to_string(), existing);
return Some(endpoint);
}
port = existing.listen.port();
existing.close();
}
let listener = bind(ip, port)?;
let listen = listener.local_addr().ok()?;
let stop = Arc::new(AtomicBool::new(false));
let accept_loop = {
let stop = Arc::clone(&stop);
let policy = Arc::clone(&self.policy);
let target = target.to_string();
std::thread::Builder::new()
.name(format!("mesh-fwd-{id}"))
.spawn(move || accept_loop(listener, target, stop, policy))
.ok()?
};
forwards.insert(
id.to_string(),
Forward {
listen,
target: target.to_string(),
stop,
accept_loop,
},
);
Some(listen.to_string())
}
fn release(&self, id: &str) {
let released = self.forwards().remove(id);
if let Some(forward) = released {
forward.close();
}
}
fn exposed_ids(&self) -> Vec<String> {
let mut ids: Vec<String> = self.forwards().keys().cloned().collect();
ids.sort();
ids
}
}
fn bind(ip: IpAddr, preferred_port: u16) -> Option<TcpListener> {
let listener =
match TcpListener::bind((ip, preferred_port)).or_else(|_| TcpListener::bind((ip, 0))) {
Ok(listener) => listener,
Err(e) => {
eprintln!(" [DEPLOY] cannot listen on mesh IP {ip}: {e}");
return None;
}
};
listener.set_nonblocking(true).ok()?;
Some(listener)
}
fn accept_loop(listener: TcpListener, target: String, stop: Arc<AtomicBool>, policy: Policy) {
let own = listener.local_addr().map(|addr| addr.ip()).ok();
while !stop.load(Ordering::Relaxed) {
match listener.accept() {
Ok((client, peer)) => {
if !own.is_some_and(|own| policy(peer.ip(), own)) {
log_refusal(peer.ip(), &target);
drop(client);
continue;
}
let conn_target = target.clone();
let spawned = std::thread::Builder::new()
.name("mesh-fwd-conn".to_string())
.spawn(move || splice(client, &conn_target));
if let Err(e) = spawned {
eprintln!(
" [DEPLOY] mesh forward to {target}: no thread for a connection: {e}"
);
}
}
Err(e) if e.kind() == ErrorKind::WouldBlock => std::thread::sleep(ACCEPT_POLL),
Err(e) => {
eprintln!(" [DEPLOY] mesh forward to {target}: accept failed: {e}");
std::thread::sleep(ACCEPT_POLL);
}
}
}
}
const REFUSAL_LOG_EVERY: Duration = Duration::from_secs(60);
fn log_refusal(peer: IpAddr, target: &str) {
static LOGGED: OnceLock<Mutex<HashMap<IpAddr, Instant>>> = OnceLock::new();
let mut logged = LOGGED
.get_or_init(Mutex::default)
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let now = Instant::now();
if logged
.get(&peer)
.is_some_and(|at| now.duration_since(*at) < REFUSAL_LOG_EVERY)
{
return;
}
logged.insert(peer, now);
eprintln!(
" [DEPLOY] mesh forward to {target}: refused {peer}, not one of the owner's addresses (others use /serve)"
);
}
fn splice(client: TcpStream, target: &str) {
if client.set_nonblocking(false).is_err() {
return;
}
let Ok(addr) = target.parse::<SocketAddr>() else {
return;
};
let upstream = match TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) {
Ok(upstream) => upstream,
Err(e) => {
eprintln!(" [DEPLOY] mesh forward to {target} failed: {e}");
return;
}
};
let (Ok(mut from_client), Ok(mut from_upstream)) = (client.try_clone(), upstream.try_clone())
else {
return;
};
let (mut to_client, mut to_upstream) = (client, upstream);
let downstream = std::thread::spawn(move || {
let _ = std::io::copy(&mut from_upstream, &mut to_client);
let _ = to_client.shutdown(Shutdown::Write);
});
let _ = std::io::copy(&mut from_client, &mut to_upstream);
let _ = to_upstream.shutdown(Shutdown::Write);
let _ = downstream.join();
}
pub fn forwarder() -> &'static TcpForwarder {
static FORWARDER: OnceLock<TcpForwarder> = OnceLock::new();
FORWARDER.get_or_init(|| TcpForwarder::with_policy(|peer, own| allowlist().allows(peer, own)))
}
pub fn exposure_ip(mesh_ip: Option<String>, mesh_expose: Option<&str>) -> Option<String> {
let disabled = mesh_expose.is_some_and(|value| {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"0" | "false" | "off" | "no"
)
});
mesh_ip.filter(|_| !disabled)
}
#[derive(Debug, Default)]
pub struct MeshAllowlist {
ips: RwLock<Option<HashSet<IpAddr>>>,
}
impl MeshAllowlist {
pub fn allows(&self, peer: IpAddr, own: IpAddr) -> bool {
peer == own || self.read().as_ref().is_some_and(|ips| ips.contains(&peer))
}
pub fn apply(&self, answer: &OwnerMeshIps) -> bool {
let mut ips = self.write();
if !answer.complete && ips.is_some() {
return false;
}
let fresh: HashSet<IpAddr> = answer.ips.iter().copied().collect();
if ips.as_ref() == Some(&fresh) {
return false;
}
*ips = Some(fresh);
true
}
pub fn persist(&self) {
let Some(ips) = self.read().clone() else {
return;
};
let Some(path) = Self::path() else {
return;
};
let mut list: Vec<String> = ips.iter().map(IpAddr::to_string).collect();
list.sort();
let Ok(json) = serde_json::to_string(&serde_json::json!({ "ips": list })) else {
return;
};
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
let _ = std::fs::write(&path, json);
}
pub fn load() -> MeshAllowlist {
let ips = Self::path()
.and_then(|path| std::fs::read_to_string(path).ok())
.and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
.and_then(|v| {
let ips = v.get("ips")?.as_array()?;
Some(
ips.iter()
.filter_map(|ip| ip.as_str()?.parse().ok())
.collect::<HashSet<IpAddr>>(),
)
});
MeshAllowlist {
ips: RwLock::new(ips),
}
}
fn path() -> Option<std::path::PathBuf> {
crate::credentials::dir().map(|dir| dir.join("mesh_allowlist.json"))
}
fn read(&self) -> std::sync::RwLockReadGuard<'_, Option<HashSet<IpAddr>>> {
self.ips
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn write(&self) -> std::sync::RwLockWriteGuard<'_, Option<HashSet<IpAddr>>> {
self.ips
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
pub fn allowlist() -> &'static MeshAllowlist {
static ALLOWLIST: OnceLock<MeshAllowlist> = OnceLock::new();
ALLOWLIST.get_or_init(MeshAllowlist::load)
}
const ALLOWLIST_REFRESH_EVERY: Duration = Duration::from_secs(60);
pub fn refresh_allowlist(api_url: &str, api_key: &str) {
static LAST: Mutex<Option<Instant>> = Mutex::new(None);
{
let mut last = LAST.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if last.is_some_and(|at| at.elapsed() < ALLOWLIST_REFRESH_EVERY) {
return;
}
*last = Some(Instant::now());
}
match crate::broker::node_sync::fetch_owner_ips(api_url, api_key) {
Ok(answer) => {
let list = allowlist();
if list.apply(&answer) {
list.persist();
}
}
Err(e) => eprintln!(" [DEPLOY] could not refresh the mesh owner allowlist: {e}"),
}
}
pub fn sync(
local: &mut LocalState,
exposer: &dyn MeshExposer,
mesh_ip: Option<&str>,
reporter: &dyn StatusReporter,
) -> bool {
for id in exposer.exposed_ids() {
let keep = mesh_ip.is_some() && local.deployments.get(&id).and_then(target_of).is_some();
if !keep {
exposer.release(&id);
}
}
let mut ids: Vec<String> = local.deployments.keys().cloned().collect();
ids.sort();
let mut changed = false;
for id in ids {
let Some(rec) = local.deployments.get_mut(&id) else {
continue;
};
let exposed = match (mesh_ip, target_of(rec)) {
(Some(ip), Some((target, container_port))) => {
exposer.expose(&id, ip, &target, rec.mesh_port.unwrap_or(container_port))
}
_ => None,
};
if exposed == rec.mesh_endpoint {
continue;
}
if let Some(port) = exposed.as_deref().and_then(port_of) {
rec.mesh_port = Some(port);
}
rec.mesh_endpoint = exposed;
changed = true;
if rec.phase == "healthy" {
reporter.report(
&id,
rec.version,
"healthy",
"",
1,
rec.container_id.as_deref(),
rec.endpoint.as_deref(),
rec.mesh_endpoint.as_deref(),
);
}
}
changed
}
fn target_of(rec: &DeploymentRecord) -> Option<(String, u16)> {
if rec.phase != "healthy" {
return None;
}
let ip: IpAddr = rec.ip.as_deref()?.parse().ok()?;
let port = rec.port?;
Some((SocketAddr::new(ip, port).to_string(), port))
}
fn port_of(endpoint: &str) -> Option<u16> {
endpoint.parse::<SocketAddr>().ok().map(|addr| addr.port())
}
#[cfg(test)]
#[derive(Default)]
pub(crate) struct FakeExposer {
exposed: Mutex<HashMap<String, (String, String, u16)>>,
}
#[cfg(test)]
impl FakeExposer {
pub(crate) fn target_of(&self, id: &str) -> Option<String> {
self.exposed
.lock()
.unwrap()
.get(id)
.map(|(_, t, _)| t.clone())
}
pub(crate) fn port_of(&self, id: &str) -> Option<u16> {
self.exposed.lock().unwrap().get(id).map(|(_, _, p)| *p)
}
}
#[cfg(test)]
impl MeshExposer for FakeExposer {
fn expose(
&self,
id: &str,
listen_ip: &str,
target: &str,
preferred_port: u16,
) -> Option<String> {
let mut exposed = self.exposed.lock().unwrap();
let port = exposed.get(id).map_or(preferred_port, |(_, _, p)| *p);
exposed.insert(
id.to_string(),
(listen_ip.to_string(), target.to_string(), port),
);
Some(format!("{listen_ip}:{port}"))
}
fn release(&self, id: &str) {
self.exposed.lock().unwrap().remove(id);
}
fn exposed_ids(&self) -> Vec<String> {
let mut ids: Vec<String> = self.exposed.lock().unwrap().keys().cloned().collect();
ids.sort();
ids
}
}
#[cfg(test)]
pub(crate) struct TempZakuroHome {
dir: std::path::PathBuf,
prev: Option<std::ffi::OsString>,
_lock: MutexGuard<'static, ()>,
}
#[cfg(test)]
impl TempZakuroHome {
pub(crate) fn new() -> Self {
let lock = crate::credentials::HOME_ENV_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let dir = std::env::temp_dir().join(format!("zc-expose-test-{}", uuid::Uuid::new_v4()));
let prev = std::env::var_os("ZAKURO_HOME");
std::env::set_var("ZAKURO_HOME", &dir);
TempZakuroHome {
dir,
prev,
_lock: lock,
}
}
}
#[cfg(test)]
impl Drop for TempZakuroHome {
fn drop(&mut self) {
match self.prev.take() {
Some(v) => std::env::set_var("ZAKURO_HOME", v),
None => std::env::remove_var("ZAKURO_HOME"),
}
let _ = std::fs::remove_dir_all(&self.dir);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Write};
fn echo_server(prefix: &'static str) -> String {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
std::thread::spawn(move || {
for conn in listener.incoming() {
let Ok(mut conn) = conn else { continue };
std::thread::spawn(move || {
let mut got = Vec::new();
let _ = conn.read_to_end(&mut got);
let _ = conn.write_all(prefix.as_bytes());
let _ = conn.write_all(&got);
});
}
});
addr.to_string()
}
fn roundtrip(endpoint: &str, msg: &[u8]) -> Vec<u8> {
let mut conn = TcpStream::connect(endpoint).unwrap();
conn.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
conn.write_all(msg).unwrap();
conn.shutdown(Shutdown::Write).unwrap();
let mut reply = Vec::new();
conn.read_to_end(&mut reply).unwrap();
reply
}
fn free_port() -> u16 {
TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port()
}
#[test]
fn forwarder_splices_both_directions_to_the_target() {
let fwd = TcpForwarder::default();
let endpoint = fwd
.expose("dep_1", "127.0.0.1", &echo_server("echo:"), free_port())
.expect("exposed");
assert!(endpoint.starts_with("127.0.0.1:"), "{endpoint}");
assert_eq!(roundtrip(&endpoint, b"hello"), b"echo:hello");
fwd.release("dep_1");
}
#[test]
fn forwarder_listens_on_the_preferred_port_when_it_is_free() {
let fwd = TcpForwarder::default();
let port = free_port();
let endpoint = fwd.expose("dep_1", "127.0.0.1", &echo_server(""), port);
assert_eq!(endpoint, Some(format!("127.0.0.1:{port}")));
fwd.release("dep_1");
}
#[test]
fn forwarder_falls_back_to_another_port_when_the_preferred_one_is_taken() {
let taken = TcpListener::bind("127.0.0.1:0").unwrap();
let port = taken.local_addr().unwrap().port();
let fwd = TcpForwarder::default();
let endpoint = fwd
.expose("dep_1", "127.0.0.1", &echo_server("echo:"), port)
.expect("exposed");
assert_ne!(endpoint, format!("127.0.0.1:{port}"));
assert_eq!(roundtrip(&endpoint, b"x"), b"echo:x");
fwd.release("dep_1");
}
#[test]
fn forwarder_keeps_an_unchanged_exposure() {
let fwd = TcpForwarder::default();
let target = echo_server("");
let first = fwd.expose("dep_1", "127.0.0.1", &target, free_port());
let second = fwd.expose("dep_1", "127.0.0.1", &target, free_port());
assert!(first.is_some());
assert_eq!(first, second);
assert_eq!(fwd.exposed_ids(), vec!["dep_1".to_string()]);
fwd.release("dep_1");
}
#[test]
fn forwarder_repointed_at_a_new_target_keeps_its_port() {
let fwd = TcpForwarder::default();
let first = fwd
.expose("dep_1", "127.0.0.1", &echo_server("old:"), free_port())
.expect("exposed");
let second = fwd
.expose("dep_1", "127.0.0.1", &echo_server("new:"), free_port())
.expect("re-exposed");
assert_eq!(first, second);
assert_eq!(roundtrip(&second, b"x"), b"new:x");
fwd.release("dep_1");
}
#[test]
fn forwarder_release_frees_the_port() {
let fwd = TcpForwarder::default();
let endpoint = fwd
.expose("dep_1", "127.0.0.1", &echo_server(""), free_port())
.expect("exposed");
fwd.release("dep_1");
assert!(fwd.exposed_ids().is_empty());
assert!(
TcpStream::connect(&endpoint).is_err(),
"still listening on {endpoint}"
);
}
#[test]
fn forwarder_refuses_a_listen_address_that_is_not_an_ip() {
let fwd = TcpForwarder::default();
assert_eq!(
fwd.expose("dep_1", "zakuro0", &echo_server(""), free_port()),
None
);
assert!(fwd.exposed_ids().is_empty());
}
#[derive(Default)]
struct Reports(Mutex<Vec<(String, String, Option<String>)>>);
impl Reports {
fn taken(&self) -> Vec<(String, String, Option<String>)> {
std::mem::take(&mut *self.0.lock().unwrap())
}
}
impl StatusReporter for Reports {
fn report(
&self,
id: &str,
_version: u64,
phase: &str,
_message: &str,
_attempt: u32,
_container_id: Option<&str>,
_endpoint: Option<&str>,
mesh_endpoint: Option<&str>,
) {
self.0.lock().unwrap().push((
id.to_string(),
phase.to_string(),
mesh_endpoint.map(str::to_string),
));
}
}
fn report(
id: &str,
phase: &str,
mesh_endpoint: Option<&str>,
) -> (String, String, Option<String>) {
(
id.to_string(),
phase.to_string(),
mesh_endpoint.map(str::to_string),
)
}
fn healthy(ip: &str, port: u16) -> DeploymentRecord {
DeploymentRecord {
version: 1,
container_id: Some("c1".to_string()),
phase: "healthy".to_string(),
ip: Some(ip.to_string()),
port: Some(port),
endpoint: Some(format!("{ip}:{port}")),
..Default::default()
}
}
#[test]
fn sync_exposes_a_healthy_deployment_and_reports_where() {
let exposer = FakeExposer::default();
let reports = Reports::default();
let mut local = LocalState::default();
local
.deployments
.insert("dep_1".into(), healthy("172.17.0.3", 8888));
assert!(sync(&mut local, &exposer, Some("10.13.13.22"), &reports));
assert_eq!(
exposer.target_of("dep_1").as_deref(),
Some("172.17.0.3:8888")
);
let rec = &local.deployments["dep_1"];
assert_eq!(rec.mesh_endpoint.as_deref(), Some("10.13.13.22:8888"));
assert_eq!(rec.mesh_port, Some(8888));
assert_eq!(
reports.taken(),
vec![report("dep_1", "healthy", Some("10.13.13.22:8888"))]
);
}
#[test]
fn sync_reports_nothing_when_nothing_changed() {
let exposer = FakeExposer::default();
let reports = Reports::default();
let mut local = LocalState::default();
local
.deployments
.insert("dep_1".into(), healthy("172.17.0.3", 8888));
sync(&mut local, &exposer, Some("10.13.13.22"), &reports);
reports.taken();
assert!(!sync(&mut local, &exposer, Some("10.13.13.22"), &reports));
assert!(reports.taken().is_empty());
}
#[test]
fn sync_reuses_the_port_a_previous_exposure_listened_on() {
let exposer = FakeExposer::default();
let mut rec = healthy("172.17.0.3", 8888);
rec.mesh_port = Some(40123);
let mut local = LocalState::default();
local.deployments.insert("dep_1".into(), rec);
sync(
&mut local,
&exposer,
Some("10.13.13.22"),
&Reports::default(),
);
assert_eq!(exposer.port_of("dep_1"), Some(40123));
assert_eq!(
local.deployments["dep_1"].mesh_endpoint.as_deref(),
Some("10.13.13.22:40123")
);
}
#[test]
fn sync_releases_a_deployment_that_is_no_longer_healthy() {
let exposer = FakeExposer::default();
exposer.expose("dep_1", "10.13.13.22", "172.17.0.3:8888", 8888);
let reports = Reports::default();
let mut rec = healthy("172.17.0.3", 8888);
rec.phase = "stopped".to_string();
let mut local = LocalState::default();
local.deployments.insert("dep_1".into(), rec);
sync(&mut local, &exposer, Some("10.13.13.22"), &reports);
assert!(exposer.exposed_ids().is_empty());
assert!(reports.taken().is_empty());
}
#[test]
fn sync_releases_deployments_this_node_no_longer_tracks() {
let exposer = FakeExposer::default();
exposer.expose("dep_gone", "10.13.13.22", "172.17.0.3:8888", 8888);
sync(
&mut LocalState::default(),
&exposer,
Some("10.13.13.22"),
&Reports::default(),
);
assert!(exposer.exposed_ids().is_empty());
}
#[test]
fn sync_without_a_mesh_ip_withdraws_the_address_from_the_hub() {
let exposer = FakeExposer::default();
exposer.expose("dep_1", "10.13.13.22", "172.17.0.3:8888", 8888);
let reports = Reports::default();
let mut rec = healthy("172.17.0.3", 8888);
rec.mesh_endpoint = Some("10.13.13.22:8888".to_string());
rec.mesh_port = Some(8888);
let mut local = LocalState::default();
local.deployments.insert("dep_1".into(), rec);
assert!(sync(&mut local, &exposer, None, &reports));
assert!(exposer.exposed_ids().is_empty());
let rec = &local.deployments["dep_1"];
assert_eq!(rec.mesh_endpoint, None);
assert_eq!(rec.mesh_port, Some(8888));
assert_eq!(reports.taken(), vec![report("dep_1", "healthy", None)]);
}
#[test]
fn exposure_is_on_unless_the_operator_turns_it_off() {
let ip = || Some("10.13.13.22".to_string());
assert_eq!(exposure_ip(ip(), None), ip());
assert_eq!(exposure_ip(ip(), Some("1")), ip());
for off in ["0", "false", "OFF", " no "] {
assert_eq!(exposure_ip(ip(), Some(off)), None, "{off:?}");
}
assert_eq!(exposure_ip(None, None), None);
}
fn ip(s: &str) -> IpAddr {
s.parse().unwrap()
}
fn answer(ips: &[&str], complete: bool) -> OwnerMeshIps {
OwnerMeshIps {
ips: ips.iter().map(|s| ip(s)).collect(),
complete,
}
}
const OWN: &str = "10.13.13.22";
fn reply_or_nothing(endpoint: &str, msg: &[u8]) -> Vec<u8> {
let Ok(mut conn) = TcpStream::connect(endpoint) else {
return Vec::new();
};
let _ = conn.set_read_timeout(Some(Duration::from_secs(5)));
let _ = conn.write_all(msg);
let _ = conn.shutdown(Shutdown::Write);
let mut reply = Vec::new();
let _ = conn.read_to_end(&mut reply);
reply
}
#[test]
fn allowlist_always_lets_this_brokers_own_address_through() {
assert!(MeshAllowlist::default().allows(ip(OWN), ip(OWN)));
}
#[test]
fn allowlist_fails_closed_until_the_hub_has_answered() {
assert!(!MeshAllowlist::default().allows(ip("10.13.13.5"), ip(OWN)));
}
#[test]
fn allowlist_lets_exactly_the_owners_addresses_through() {
let list = MeshAllowlist::default();
assert!(list.apply(&answer(&["10.13.13.5", "10.13.13.9"], true)));
assert!(list.allows(ip("10.13.13.5"), ip(OWN)));
assert!(list.allows(ip("10.13.13.9"), ip(OWN)));
assert!(!list.allows(ip("10.13.13.7"), ip(OWN)));
assert!(list.allows(ip(OWN), ip(OWN)));
}
#[test]
fn a_complete_answer_replaces_the_allowlist() {
let list = MeshAllowlist::default();
list.apply(&answer(&["10.13.13.5", "10.13.13.9"], true));
assert!(list.apply(&answer(&["10.13.13.5"], true)));
assert!(!list.allows(ip("10.13.13.9"), ip(OWN)));
assert!(
!list.apply(&answer(&["10.13.13.5"], true)),
"the same answer again is no change"
);
}
#[test]
fn an_incomplete_answer_is_used_only_when_nothing_is_known() {
let list = MeshAllowlist::default();
assert!(list.apply(&answer(&["10.13.13.9"], false)));
assert!(list.allows(ip("10.13.13.9"), ip(OWN)));
list.apply(&answer(&["10.13.13.9", "10.13.13.5"], true));
assert!(!list.apply(&answer(&["10.13.13.9"], false)));
assert!(list.allows(ip("10.13.13.5"), ip(OWN)));
}
#[test]
fn allowlist_survives_a_restart_through_its_cache_file() {
let _home = TempZakuroHome::new();
let list = MeshAllowlist::default();
list.apply(&answer(&["10.13.13.5"], true));
list.persist();
let reloaded = MeshAllowlist::load();
assert!(reloaded.allows(ip("10.13.13.5"), ip(OWN)));
assert!(!reloaded.allows(ip("10.13.13.7"), ip(OWN)));
}
#[test]
fn a_missing_cache_file_loads_fail_closed() {
let _home = TempZakuroHome::new();
let list = MeshAllowlist::load();
assert!(!list.allows(ip("10.13.13.5"), ip(OWN)));
assert!(list.allows(ip(OWN), ip(OWN)));
}
#[test]
fn forwarder_drops_connections_its_policy_refuses() {
let fwd = TcpForwarder::with_policy(|_, _| false);
let endpoint = fwd
.expose("dep_1", "127.0.0.1", &echo_server("echo:"), free_port())
.expect("exposed");
assert!(reply_or_nothing(&endpoint, b"hello").is_empty());
fwd.release("dep_1");
}
}