Skip to content

Commit

Permalink
Implement network interfacing and scanning utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
josephrhobbs committed Jul 23, 2024
1 parent 7b3d149 commit 4f5bac0
Show file tree
Hide file tree
Showing 14 changed files with 459 additions and 68 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"proton_arp",
"proton_mac",
"proton_nat",
"proton_nif",
]

[package]
Expand All @@ -18,4 +19,7 @@ path = "proton_arp"
path = "proton_mac"

[dependencies.proton_nat]
path = "proton_nat"
path = "proton_nat"

[dependencies.proton_nif]
path = "proton_nif"
3 changes: 3 additions & 0 deletions proton_arp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ features = ["full"]

[dependencies.pnet]
version = "0.35.0"

[dependencies.proton_nif]
path = "../proton_nif"
18 changes: 7 additions & 11 deletions proton_arp/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl ArpCache {
/// None.
pub fn add(&mut self, ipv4: Ipv4Addr, mac: MacAddr) {
// Create a cache entry
let entry = ArpCacheEntry::new(ipv4, mac, self.refresh);
let entry = ArpCacheEntry::new(ipv4, mac);

// Add the entry to the cache
self.cache.push(entry);
Expand Down Expand Up @@ -79,7 +79,7 @@ impl ArpCache {

for entry in &self.cache {
// Check if the entry needs to be refreshed
if entry.check() {
if entry.check(self.refresh) {
stale_ips.push(entry.ipv4);
}
}
Expand Down Expand Up @@ -140,9 +140,6 @@ pub struct ArpCacheEntry {

/// The time that this entry was created.
created: Instant,

/// The amount of time after which this entry needs to be refreshed.
refresh: Duration,
}

impl ArpCacheEntry {
Expand All @@ -151,30 +148,29 @@ impl ArpCacheEntry {
/// # Parameters
/// - `ipv4` (`Ipv4Addr`): the IPv4 address of the device
/// - `mac` (`MacAddr`): the MAC address of the device
/// - `refresh` (`Duration`): the refresh time of this cache entry
///
/// # Returns
/// A new `ArpCacheEntry` corresponding to the provided MAC address.
pub fn new(ipv4: Ipv4Addr, mac: MacAddr, refresh: Duration) -> Self {
pub fn new(ipv4: Ipv4Addr, mac: MacAddr) -> Self {
Self {
ipv4,
mac,
created: Instant::now(),
refresh,
}
}

/// Check if this entry needs to be refreshed (as of call time).
///
/// # Parameters
/// None.
/// - `refresh` (`Duration`): the amount of time after which this entry
/// should be refreshed.
///
/// # Returns
/// A `bool` indicating whether or not this entry should be refreshed.
pub fn check(&self) -> bool {
pub fn check(&self, refresh: Duration) -> bool {
// Check the time
let now = Instant::now();

now - self.created >= self.refresh
now - self.created >= refresh
}
}
20 changes: 17 additions & 3 deletions proton_arp/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,35 @@ use std::{
fmt,
};

use tokio::task::JoinError;

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
#[non_exhaustive]
/// An ARP management error.
pub enum ArpError {

/// Could not find Wi-Fi interface.
CouldNotFindWirelessInterface,

/// Could not join asynchronous task.
CouldNotJoinAsyncTask,
}

impl fmt::Display for ArpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ArpError::*;
let error = match self {
_ => "unrecognized error",
CouldNotFindWirelessInterface => "could not find wireless interface",
CouldNotJoinAsyncTask => "could not join asynchronous task",
};

write!(f, "{}", error)
}
}

impl Error for ArpError { }
impl Error for ArpError { }

impl From<JoinError> for ArpError {
fn from(_: JoinError) -> Self {
Self::CouldNotJoinAsyncTask
}
}
4 changes: 1 addition & 3 deletions proton_arp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ pub use cache::{
ArpCacheIterator,
};

pub use error::{
ArpError,
};
pub use error::ArpError;

pub use scan::scan;

Expand Down
49 changes: 0 additions & 49 deletions proton_arp/src/scan.rs

This file was deleted.

74 changes: 74 additions & 0 deletions proton_arp/src/scan/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//! Network scanning utility for the ARP manager.

mod reply;
mod request;

use std::{
net::Ipv4Addr,
time::Duration,
};

use tokio::{
sync::mpsc,
task,
};

use proton_nif::{
ifnames::DEFAULT_WIRELESS_INTERFACE,
NetworkInterface,
};

use crate::{
ArpCacheEntry,
ArpError,
ScanResult,
};

use reply::listen;
use request::request;

/// Buffer size for the asynchronous communication channel for ARP replies.
pub const ARP_CHANNEL_BUFFER_SIZE: usize = 256;

/// Default delay to wait before closing the ARP reply listener.
pub static ARP_LISTENER_DELAY: Duration = Duration::from_millis(2_500);

/// Scan the provided list of IPv4 addresses and return all ARP replies.
///
/// # Parameters
/// - `ips` (`Vec<Ipv4Addr>`): the IPv4 addresses to scan
///
/// # Returns
/// A `ScanResult` containing the ARP responses
/// received, if the scan was successful.
pub async fn scan(ips: Vec<Ipv4Addr>) -> ScanResult {
// Get the wireless network interface
let interface = NetworkInterface::new(DEFAULT_WIRELESS_INTERFACE)
.ok_or(ArpError::CouldNotFindWirelessInterface)?;

// Create an asynchronous communication channel for received replies
let (reply_tx, reply_rx) = mpsc::channel::<ArpCacheEntry>(ARP_CHANNEL_BUFFER_SIZE);

// Begin listening for ARP replies
let rx_task = task::spawn(listen(interface.clone(), reply_tx));

// Begin making ARP requests
let tx_task = task::spawn(request(interface, ips, reply_rx));

// Await the transmitter
// After completing it will pass back the async channel receiver
let mut reply_rx = tx_task.await?;

// Await the listener
let _ = rx_task.await?;

// Construct a list of entries
let mut entries = Vec::new();

// Extract each entry
while let Some (entry) = reply_rx.recv().await {
entries.push(entry)
}

Ok (entries)
}
85 changes: 85 additions & 0 deletions proton_arp/src/scan/reply.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! ARP Reply functionality.

use pnet::{
packet::{
arp::ArpPacket,
ethernet::{
EtherTypes,
EthernetPacket,
},
Packet,
},
};

use tokio::sync::mpsc;

use proton_nif::NetworkInterface;

use crate::ArpCacheEntry;

/// Receive a series of ARP replies.
///
/// # Parameters
/// - `interface` (`NetworkInterface`): the network interface to use
/// - `tx` (`Sender<ArpCacheEntry>`): the cache entry transmitter
///
/// # Returns
/// None.
pub async fn listen(
mut interface: NetworkInterface,
tx: mpsc::Sender<ArpCacheEntry>,
) {
// Get interface MAC address
let mac = interface.mac.unwrap();

while let Some (packet) = interface.recv().await {
// Check if the MPSC channel has closed
// There's no point in continuing if it is because
// all future packets will be dropped anyways
if tx.is_closed() {
break;
}

// Convert to ETH Frame
let eth_frame = if let Some (f) = EthernetPacket::new(&packet) {
f
} else {
continue;
};

// Check ETH Frame Type
let frame_type = eth_frame.get_ethertype();
if frame_type != EtherTypes::Arp {
continue;
}

// Convert to ARP Packet
let arp_packet = if let Some (a) = ArpPacket::new(eth_frame.payload()) {
a
} else {
continue;
};

// Drop the frame if it was sent from our own computer
if arp_packet.get_sender_hw_addr() == mac {
continue;
}

// Construct cache entry
let entry = ArpCacheEntry::new(
arp_packet.get_sender_proto_addr(),
arp_packet.get_sender_hw_addr(),
);

// Send the reply
let send = tx.send(entry);

// When the receiver side of the channel is closed,
// this will return, because `tx::send` will return an error
if send.await.is_err() {
break;
}

// If there are no packets left, the function returns
}
}
Loading

0 comments on commit 4f5bac0

Please sign in to comment.