use crate::config::StateConfig;
use crate::error::{Result, TransportError};
use crate::storage::keys::load_or_generate_identity;
use crate::transport::iroh::derive_alpn;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NetworkProbe {
pub node_id: String,
pub relay_urls: Vec<String>,
pub direct_addresses: Vec<String>,
}
impl NetworkProbe {
pub fn has_direct_connectivity(&self) -> bool {
!self.direct_addresses.is_empty()
}
pub fn has_relay_connectivity(&self) -> bool {
!self.relay_urls.is_empty()
}
pub fn nat_description(&self) -> &'static str {
match (
self.has_direct_connectivity(),
self.has_relay_connectivity(),
) {
(true, _) => "Open NAT (direct connection available)",
(false, true) => "Restricted NAT (relay fallback required)",
(false, false) => "No connectivity detected",
}
}
}
pub async fn probe_network(state: &StateConfig) -> Result<NetworkProbe> {
let identity = load_or_generate_identity(state).await?;
let alpn = derive_alpn(None);
let endpoint = iroh::Endpoint::builder()
.secret_key(identity.secret_key)
.alpns(vec![alpn])
.relay_mode(iroh::RelayMode::Default)
.bind()
.await
.map_err(|source| TransportError::EndpointBind { source })?;
endpoint.online().await;
let addr = endpoint.addr();
let node_id = endpoint.id().to_string();
let relay_urls = addr.relay_urls().map(|u| u.to_string()).collect::<Vec<_>>();
let direct_addresses = addr.ip_addrs().map(|a| a.to_string()).collect::<Vec<_>>();
endpoint.close().await;
Ok(NetworkProbe {
node_id,
relay_urls,
direct_addresses,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SecurityReport {
pub root_path: std::path::PathBuf,
pub root_exists: bool,
pub root_mode: Option<u32>,
pub root_loose: bool,
pub key_path: std::path::PathBuf,
pub key_exists: bool,
pub key_mode: Option<u32>,
pub key_unsafe: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemReport {
pub ssh_version: Option<String>,
pub udp_available: bool,
}
pub fn check_security(state: &StateConfig) -> SecurityReport {
let root_path = state.root().to_path_buf();
let key_path = state.root().join("keys").join("node.secret");
#[allow(unused_mut)]
let mut report = SecurityReport {
root_path: root_path.clone(),
root_exists: root_path.exists(),
root_mode: None,
root_loose: false,
key_path: key_path.clone(),
key_exists: key_path.exists(),
key_mode: None,
key_unsafe: false,
};
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if let Ok(meta) = std::fs::metadata(&root_path) {
let mode = meta.mode() & 0o777;
report.root_mode = Some(mode);
report.root_loose = (mode & 0o077) != 0;
}
if let Ok(meta) = std::fs::metadata(&key_path) {
let mode = meta.mode() & 0o777;
report.key_mode = Some(mode);
report.key_unsafe = (mode & 0o077) != 0;
}
}
report
}
pub fn check_system() -> SystemReport {
let ssh_version = std::process::Command::new("ssh")
.arg("-V")
.output()
.ok()
.and_then(|out| {
String::from_utf8_lossy(&out.stderr)
.split_whitespace()
.next()
.map(|s| s.to_string())
});
let udp_available = std::net::UdpSocket::bind("0.0.0.0:0").is_ok();
SystemReport {
ssh_version,
udp_available,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nat_description_logic() {
let open_nat = NetworkProbe {
node_id: "test".to_string(),
relay_urls: vec!["relay".to_string()],
direct_addresses: vec!["1.2.3.4".to_string()],
};
assert_eq!(
open_nat.nat_description(),
"Open NAT (direct connection available)"
);
let restricted_nat = NetworkProbe {
node_id: "test".to_string(),
relay_urls: vec!["relay".to_string()],
direct_addresses: vec![],
};
assert_eq!(
restricted_nat.nat_description(),
"Restricted NAT (relay fallback required)"
);
let no_conn = NetworkProbe {
node_id: "test".to_string(),
relay_urls: vec![],
direct_addresses: vec![],
};
assert_eq!(no_conn.nat_description(), "No connectivity detected");
}
}