Skip to content

Commit

Permalink
WIP Detect OS before choosing IpNeighborScanner
Browse files Browse the repository at this point in the history
WIP: In unit test, neighbors are empty.
  • Loading branch information
seamlik committed Mar 13, 2024
1 parent 1db1da7 commit 95ae202
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 5 deletions.
4 changes: 3 additions & 1 deletion main/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions main/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions main/src/network/link_local/Print-IpNeighbors.ps1
Original file line number Diff line number Diff line change
@@ -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)"
}
25 changes: 23 additions & 2 deletions main/src/network/link_local/mod.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
mod windows;

use crate::os::OperatingSystem;
use crate::process::ProcessError;
use futures_util::future::BoxFuture;
use futures_util::FutureExt;
use mockall::automock;
use std::net::Ipv6Addr;
use std::net::SocketAddrV6;
use thiserror::Error;

pub async fn ip_neighbor_scanner() -> Box<dyn IpNeighborScanner> {
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<Vec<IpNeighbor>, IpNeighborScanError>>;
}

pub struct DummyIpNeighborScanner;
struct DummyIpNeighborScanner;

impl IpNeighborScanner for DummyIpNeighborScanner {
fn scan(&self) -> BoxFuture<'static, Result<Vec<IpNeighbor>, IpNeighborScanError>> {
Expand Down
72 changes: 72 additions & 0 deletions main/src/network/link_local/windows.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<IpNeighbor>, IpNeighborScanError> {
let stdout = crate::process::eval(
"powershell",
&["-Command", "-"],
include_bytes!("./Print-IpNeighbors.ps1"),
)
.await?;
let neighbors: Vec<_> = Reader::from_reader(stdout.as_slice())
.deserialize::<NetNeighbor>()
.collect::<Result<Vec<NetNeighbor>, _>>()?;
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<Vec<IpNeighbor>, IpNeighborScanError>> {
Self::scan().boxed()
}
}

#[derive(Deserialize)]
struct NetNeighbor {
if_index: u32,
ip_address: Ipv6Addr,
state: String,
}

impl From<NetNeighbor> 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());
}
}
42 changes: 42 additions & 0 deletions main/src/os.rs
Original file line number Diff line number Diff line change
@@ -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<OperatingSystem, OperatingSystemDetectError> {
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<bool> {
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");
}
}
46 changes: 46 additions & 0 deletions main/src/process.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>, 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,
}
3 changes: 1 addition & 2 deletions main/src/server.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit 95ae202

Please sign in to comment.