Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ members = [
]

[workspace.package]
version = "0.24.1"
version = "0.25.0"
edition = "2024"
authors = ["shellrow <shellrow@foctal.com>"]

[workspace.dependencies]
nex-core = { version = "0.24.1", path = "nex-core" }
nex-datalink = { version = "0.24.1", path = "nex-datalink" }
nex-packet = { version = "0.24.1", path = "nex-packet" }
nex-sys = { version = "0.24.1", path = "nex-sys" }
nex-socket = { version = "0.24.1", path = "nex-socket" }
nex-core = { version = "0.25.0", path = "nex-core" }
nex-datalink = { version = "0.25.0", path = "nex-datalink" }
nex-packet = { version = "0.25.0", path = "nex-packet" }
nex-sys = { version = "0.25.0", path = "nex-sys" }
nex-socket = { version = "0.25.0", path = "nex-socket" }
serde = { version = "1" }
libc = "0.2"
netdev = { version = "0.39" }
netdev = { version = "0.40" }
bytes = "1"
tokio = { version = "1" }
rand = "0.8"
15 changes: 6 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,21 @@ Cross-platform low-level networking library in Rust
## Overview

`nex` is a Rust library that provides cross-platform low-level networking capabilities.
It includes a set of modules, each with a specific focus:
It includes sub-crates with responsibilities:

- `datalink`: Datalink layer networking.
- `packet`: Low-level packet parsing and building.
- `socket`: Socket-related functionality.
- `nex-packet`: Low-level packet parsing and serialization.
- `nex-datalink`: Raw datalink send/receive backends across platforms.
- `nex-socket`: Low-level socket operations with cross-platform option handling.

## Upcoming Features
The project has plans to enhance nex with the following features:
- More Protocol Support: Expanding protocol support to include additional network protocols and standards.
- Performance Improvements: Continuously working on performance enhancements for faster network operations.
The project aims to expose portable low-level primitives.

## Usage

To use `nex`, add it as a dependency in your `Cargo.toml`:

```toml
[dependencies]
nex = "0.24"
nex = "0.25"
```

## Using Specific Sub-crates
Expand Down
33 changes: 32 additions & 1 deletion nex-core/src/ip.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! IP address utilities.

use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};

/// Returns [`true`] if the address appears to be globally routable.
pub fn is_global_ip(ip_addr: &IpAddr) -> bool {
Expand All @@ -10,6 +10,19 @@ pub fn is_global_ip(ip_addr: &IpAddr) -> bool {
}
}

/// Returns an unspecified IP (`0.0.0.0` / `::`) with the same family as `ip_addr`.
pub fn unspecified_ip_for(ip_addr: &IpAddr) -> IpAddr {
match ip_addr {
IpAddr::V4(_) => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
IpAddr::V6(_) => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
}
}

/// Returns an unspecified socket address with the same family as `ip_addr`.
pub fn unspecified_socket_addr_for(ip_addr: &IpAddr, port: u16) -> SocketAddr {
SocketAddr::new(unspecified_ip_for(ip_addr), port)
}

/// Returns [`true`] if the address appears to be globally reachable
/// as specified by the [IANA IPv4 Special-Purpose Address Registry].
pub fn is_global_ipv4(ipv4_addr: &Ipv4Addr) -> bool {
Expand Down Expand Up @@ -139,4 +152,22 @@ mod tests {
assert!(!is_global_ip(&ip_private));
assert!(!is_global_ip(&ip_ula));
}

#[test]
fn test_unspecified_helpers() {
let v4 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
let v6 = IpAddr::V6(Ipv6Addr::LOCALHOST);

assert_eq!(unspecified_ip_for(&v4), IpAddr::V4(Ipv4Addr::UNSPECIFIED));
assert_eq!(unspecified_ip_for(&v6), IpAddr::V6(Ipv6Addr::UNSPECIFIED));

assert_eq!(
unspecified_socket_addr_for(&v4, 1234),
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 1234)
);
assert_eq!(
unspecified_socket_addr_for(&v6, 4321),
SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 4321)
);
}
}
4 changes: 2 additions & 2 deletions nex-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! Provides core network types and functionality.
//! Primarily designed for use with nex, it also includes extensions to the standard net module.
//! Core network types and helpers shared across the `nex` crates.
//! Includes interface, MAC/IP, and bitfield utilities used by low-level networking code.

pub use netdev;

Expand Down
27 changes: 16 additions & 11 deletions nex-datalink/src/async_io/bpf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,32 +151,37 @@ impl Stream for AsyncBpfSocketReceiver {
/// Create a new asynchronous BPF socket channel.
pub fn channel(network_interface: &Interface, config: Config) -> io::Result<AsyncChannel> {
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "openbsd"))]
fn get_fd(attempts: usize) -> RawFd {
fn get_fd(attempts: usize) -> io::Result<RawFd> {
for i in 0..attempts {
let file_name = format!("/dev/bpf{}", i);
let c_file_name = CString::new(file_name).unwrap();
let c_file_name = CString::new(file_name).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "invalid bpf device path")
})?;
let fd = unsafe { libc::open(c_file_name.as_ptr(), libc::O_RDWR, 0) };
if fd != -1 {
return fd;
return Ok(fd);
}
}
-1
Err(io::Error::last_os_error())
}
#[cfg(any(
target_os = "freebsd",
target_os = "netbsd",
target_os = "illumos",
target_os = "solaris",
))]
fn get_fd(_attempts: usize) -> RawFd {
let c_file_name = CString::new("/dev/bpf").unwrap();
unsafe { libc::open(c_file_name.as_ptr(), libc::O_RDWR, 0) }
fn get_fd(_attempts: usize) -> io::Result<RawFd> {
let c_file_name = CString::new("/dev/bpf")
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid bpf device path"))?;
let fd = unsafe { libc::open(c_file_name.as_ptr(), libc::O_RDWR, 0) };
if fd == -1 {
Err(io::Error::last_os_error())
} else {
Ok(fd)
}
}

let fd = get_fd(config.bpf_fd_attempts);
if fd == -1 {
return Err(io::Error::last_os_error());
}
let fd = get_fd(config.bpf_fd_attempts)?;

let mut iface: bpf::ifreq = unsafe { mem::zeroed() };
for (i, c) in network_interface.name.bytes().enumerate() {
Expand Down
37 changes: 32 additions & 5 deletions nex-datalink/src/async_io/wpcap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,29 @@ impl Stream for AsyncWpcapSocketReceiver {
type Item = io::Result<Vec<u8>>;

fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut queue = self.inner.packets.lock().unwrap();
let mut queue = match self.inner.packets.lock() {
Ok(queue) => queue,
Err(_) => {
return Poll::Ready(Some(Err(io::Error::new(
io::ErrorKind::Other,
"wpcap packet queue mutex poisoned",
))));
}
};
if let Some(pkt) = queue.pop_front() {
Poll::Ready(Some(Ok(pkt)))
} else {
*self.inner.waker.lock().unwrap() = Some(cx.waker().clone());
match self.inner.waker.lock() {
Ok(mut waker) => {
*waker = Some(cx.waker().clone());
}
Err(_) => {
return Poll::Ready(Some(Err(io::Error::new(
io::ErrorKind::Other,
"wpcap waker mutex poisoned",
))));
}
}
Poll::Pending
}
}
Expand All @@ -108,7 +126,9 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result<Asyn

let adapter = unsafe {
let npf_if_name: String = windows::to_npf_name(&network_interface.name);
let net_if_str = CString::new(npf_if_name.as_bytes()).unwrap();
let net_if_str = CString::new(npf_if_name.as_bytes()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "interface name contains NUL")
})?;
windows::PacketOpenAdapter(net_if_str.as_ptr() as *mut libc::c_char)
};
if adapter.is_null() {
Expand Down Expand Up @@ -185,14 +205,21 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result<Asyn
let data_ptr = ((*read_packet).Buffer as isize + start) as *const u8;
let data = slice::from_raw_parts(data_ptr, caplen).to_vec();
{
let mut queue = packets.lock().unwrap();
let mut queue = match packets.lock() {
Ok(queue) => queue,
Err(poisoned) => poisoned.into_inner(),
};
queue.push_back(data);
}
let offset = (*hdr).bh_hdrlen as isize + (*hdr).bh_caplen as isize;
ptr = ptr.offset(bpf::BPF_WORDALIGN(offset));
}
}
if let Some(w) = waker.lock().unwrap().take() {
let mut waker = match waker.lock() {
Ok(waker) => waker,
Err(poisoned) => poisoned.into_inner(),
};
if let Some(w) = waker.take() {
w.wake();
}
}
Expand Down
41 changes: 24 additions & 17 deletions nex-datalink/src/bpf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,25 +74,30 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result<supe
target_os = "illumos",
target_os = "solaris"
))]
fn get_fd(_attempts: usize) -> libc::c_int {
let c_file_name = CString::new(&b"/dev/bpf"[..]).unwrap();
unsafe { libc::open(c_file_name.as_ptr(), libc::O_RDWR, 0) }
fn get_fd(_attempts: usize) -> io::Result<libc::c_int> {
let c_file_name = CString::new(&b"/dev/bpf"[..])
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid bpf device path"))?;
let fd = unsafe { libc::open(c_file_name.as_ptr(), libc::O_RDWR, 0) };
if fd == -1 {
Err(io::Error::last_os_error())
} else {
Ok(fd)
}
}

#[cfg(any(target_os = "openbsd", target_os = "macos", target_os = "ios"))]
fn get_fd(attempts: usize) -> libc::c_int {
fn get_fd(attempts: usize) -> io::Result<libc::c_int> {
for i in 0..attempts {
let fd = unsafe {
let file_name = format!("/dev/bpf{}", i);
let c_file_name = CString::new(file_name.as_bytes()).unwrap();
libc::open(c_file_name.as_ptr(), libc::O_RDWR, 0)
};
let file_name = format!("/dev/bpf{}", i);
let c_file_name = CString::new(file_name.as_bytes()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidInput, "invalid bpf device path")
})?;
let fd = unsafe { libc::open(c_file_name.as_ptr(), libc::O_RDWR, 0) };
if fd != -1 {
return fd;
return Ok(fd);
}
}

-1
Err(io::Error::last_os_error())
}

#[cfg(any(
Expand All @@ -117,10 +122,7 @@ pub fn channel(network_interface: &Interface, config: Config) -> io::Result<supe
Ok(())
}

let fd = get_fd(config.bpf_fd_attempts);
if fd == -1 {
return Err(io::Error::last_os_error());
}
let fd = get_fd(config.bpf_fd_attempts)?;
let mut iface: bpf::ifreq = unsafe { mem::zeroed() };
for (i, c) in network_interface.name.bytes().enumerate() {
iface.ifr_name[i] = c as libc::c_char;
Expand Down Expand Up @@ -409,7 +411,12 @@ impl RawReceiver for RawReceiverImpl {
}
}
}
let (mut start, mut len) = self.packets.pop_front().unwrap();
let (mut start, mut len) = self.packets.pop_front().ok_or_else(|| {
io::Error::new(
io::ErrorKind::UnexpectedEof,
"packet queue unexpectedly empty",
)
})?;
len += self.buffer_offset;
// If on loopback, adjust the start position to include padding for the fake Ethernet header.
if self.loopback {
Expand Down
Loading