diff --git a/main/Cargo.toml b/main/Cargo.toml index 424454a..e31947a 100644 --- a/main/Cargo.toml +++ b/main/Cargo.toml @@ -7,14 +7,16 @@ version.workspace = true [dependencies] anyhow = "1" +csv = "1" futures-channel = "0.3" futures-util = "0.3" log = "0.4" mockall = "0.12" prost = "0.12" +serde = { version = "1.0", features = ["derive"] } tansa-protocol = { path = "../protocol/rust" } thiserror = "1" -tokio = "1" +tokio = { version = "1", features = ["fs", "process"] } tokio-stream = { version = "0.1", features = ["net"] } tokio-util = { version = "0.7", features = ["codec", "net"]} tonic = "0.11" diff --git a/main/src/lib.rs b/main/src/lib.rs index 0ee5f9c..47b34ee 100644 --- a/main/src/lib.rs +++ b/main/src/lib.rs @@ -29,7 +29,9 @@ //! Usually contains the socket address accessible within the LAN you connect. mod network; +mod os; mod packet; +mod process; mod response_collector; mod response_sender; mod scanner; diff --git a/main/src/network/link_local/Print-IpNeighbors.ps1 b/main/src/network/link_local/Print-IpNeighbors.ps1 new file mode 100644 index 0000000..b4cc12a --- /dev/null +++ b/main/src/network/link_local/Print-IpNeighbors.ps1 @@ -0,0 +1,4 @@ +Write-Output "if_index,ip_address,state" +foreach ($row in (Get-NetNeighbor -AddressFamily IPv6)) { + Write-Output "$($row.ifIndex),$($row.IPAddress),$($row.State)" +} \ No newline at end of file diff --git a/main/src/network/link_local/mod.rs b/main/src/network/link_local/mod.rs index af49a1c..9e31a65 100644 --- a/main/src/network/link_local/mod.rs +++ b/main/src/network/link_local/mod.rs @@ -1,3 +1,7 @@ +mod windows; + +use crate::os::OperatingSystem; +use crate::process::ProcessError; use futures_util::future::BoxFuture; use futures_util::FutureExt; use mockall::automock; @@ -5,15 +9,32 @@ use std::net::Ipv6Addr; use std::net::SocketAddrV6; use thiserror::Error; +pub async fn ip_neighbor_scanner() -> Box { + match crate::os::detect_operating_system().await { + Ok(OperatingSystem::Windows) => Box::new(DummyIpNeighborScanner), // TODO + Err(e) => { + log::warn!("Failed to detect operating system: {}", e); + log::info!("Unknown operating system, disabling IP neighbor discovery."); + Box::new(DummyIpNeighborScanner) + } + } +} + #[derive(Error, Debug)] -pub enum IpNeighborScanError {} +pub enum IpNeighborScanError { + #[error("Failed in running an external command")] + ChildProcess(#[from] ProcessError), + + #[error("Failed to parse the CSV output of a child process")] + ChildProcessCsvOutput(#[from] csv::Error), +} #[automock] pub trait IpNeighborScanner { fn scan(&self) -> BoxFuture<'static, Result, IpNeighborScanError>>; } -pub struct DummyIpNeighborScanner; +struct DummyIpNeighborScanner; impl IpNeighborScanner for DummyIpNeighborScanner { fn scan(&self) -> BoxFuture<'static, Result, IpNeighborScanError>> { diff --git a/main/src/network/link_local/windows.rs b/main/src/network/link_local/windows.rs new file mode 100644 index 0000000..e556913 --- /dev/null +++ b/main/src/network/link_local/windows.rs @@ -0,0 +1,72 @@ +use super::IpNeighbor; +use super::IpNeighborScanError; +use super::IpNeighborScanner; +use csv::Reader; +use futures_util::future::BoxFuture; +use futures_util::FutureExt; +use serde::Deserialize; +use std::net::Ipv6Addr; + +pub struct WindowsIpNeighborScanner; + +impl WindowsIpNeighborScanner { + async fn scan() -> Result, IpNeighborScanError> { + let stdout = crate::process::eval( + "powershell", + &["-Command", "-"], + include_bytes!("./Print-IpNeighbors.ps1"), + ) + .await?; + let neighbors: Vec<_> = Reader::from_reader(stdout.as_slice()) + .deserialize::() + .collect::, _>>()?; + let neighbors = neighbors + .into_iter() + .filter(|n| n.state != "Unreachable") + .filter(|n| n.ip_address.segments().starts_with(&[0xF, 0xE, 0x8, 0x0])) + .map(Into::into) + .collect(); + Ok(neighbors) + } +} + +impl IpNeighborScanner for WindowsIpNeighborScanner { + fn scan(&self) -> BoxFuture<'static, Result, IpNeighborScanError>> { + Self::scan().boxed() + } +} + +#[derive(Deserialize)] +struct NetNeighbor { + if_index: u32, + ip_address: Ipv6Addr, + state: String, +} + +impl From for IpNeighbor { + fn from(value: NetNeighbor) -> Self { + Self { + address: value.ip_address, + network_interface_index: value.if_index, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::os::OperatingSystem; + + #[tokio::test] + async fn scan() { + crate::test::init(); + + if crate::os::detect_operating_system().await.unwrap() != OperatingSystem::Windows { + println!("Operating system is not Windows, skipping."); + return; + } + + let neighbors = WindowsIpNeighborScanner.scan().await.unwrap(); + assert!(!neighbors.is_empty()); + } +} diff --git a/main/src/os.rs b/main/src/os.rs new file mode 100644 index 0000000..1ba59e4 --- /dev/null +++ b/main/src/os.rs @@ -0,0 +1,42 @@ +use std::path::PathBuf; +use thiserror::Error; + +#[derive(PartialEq, Eq)] +pub enum OperatingSystem { + Windows, +} + +#[derive(Error, Debug)] +pub enum OperatingSystemDetectError { + #[error("Error in file system")] + FileSystem(#[from] std::io::Error), + + #[error("Unknown operating system")] + UnknownOperatingSystem, +} + +pub async fn detect_operating_system() -> Result { + if is_file(&[r"C:\", "Windows", "System32", "ntoskrnl.exe"]).await? { + return Ok(OperatingSystem::Windows); + } + if is_file(&["/", "usr", "bin", "systemctl"]).await? { + return Ok(OperatingSystem::Windows); + } + + Err(OperatingSystemDetectError::UnknownOperatingSystem) +} + +async fn is_file(path: &[&'static str]) -> std::io::Result { + let path_buf: PathBuf = path.iter().collect(); + Ok(tokio::fs::try_exists(&path_buf).await? && tokio::fs::metadata(&path_buf).await?.is_file()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn can_detect() { + detect_operating_system().await.expect("OS must be known"); + } +} diff --git a/main/src/process.rs b/main/src/process.rs new file mode 100644 index 0000000..9e4ee81 --- /dev/null +++ b/main/src/process.rs @@ -0,0 +1,46 @@ +use std::process::Stdio; +use thiserror::Error; +use tokio::io::AsyncReadExt; +use tokio::io::AsyncWriteExt; +use tokio::process::Command; + +pub async fn eval(command: &str, args: &[&str], stdin: &[u8]) -> Result, ProcessError> { + let mut process = Command::new(command) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn()?; + + let mut stdin_pipe = process + .stdin + .take() + .ok_or_else(|| ProcessError::StdioRedirection)?; + stdin_pipe.write_all(stdin).await?; + drop(stdin_pipe); + + if !process.wait().await?.success() { + return Err(ProcessError::ExternalCommand); + } + + let mut stdout = process + .stdout + .take() + .ok_or_else(|| ProcessError::StdioRedirection)?; + + let mut buffer = Default::default(); + stdout.read_to_end(&mut buffer).await?; + Ok(buffer) +} + +#[derive(Error, Debug)] +pub enum ProcessError { + #[error("Failed in create a child process")] + ChildProcessCreation(#[from] std::io::Error), + + #[error("Failed to redirect standard I/O")] + StdioRedirection, + + #[error("External command failed")] + ExternalCommand, +} diff --git a/main/src/server.rs b/main/src/server.rs index 3a0f00c..5547058 100644 --- a/main/src/server.rs +++ b/main/src/server.rs @@ -1,4 +1,3 @@ -use crate::network::link_local::DummyIpNeighborScanner; use crate::network::link_local::IpNeighbor; use crate::network::link_local::IpNeighborScanError; use crate::network::link_local::IpNeighborScanner; @@ -39,7 +38,7 @@ pub async fn serve(discovery_port: u16, service_port: u16) -> Result<(), ServeEr TokioMulticastReceiver, GrpcResponseSender, TokioUdpSender, - Box::new(DummyIpNeighborScanner), + crate::network::link_local::ip_neighbor_scanner().await, ) .await }