Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0d4cd99
chore: clean formatting and clippy baseline
shellrow Jun 6, 2026
eec2fd2
refactor: hide datalink backend modules
shellrow Jun 6, 2026
c1648c3
refactor: rename datalink fanout variants
shellrow Jun 7, 2026
40253ad
docs: create TODO.md
shellrow Jul 13, 2026
ad271d5
feat: make asynchronous networking APIs opt-in
shellrow Jul 18, 2026
52a7b18
feat: make IPv4 identification caller-controlled
shellrow Jul 18, 2026
635ed4d
feat: unify packet parsing errors and strictness
shellrow Jul 18, 2026
246f2b5
ci: add workspace quality and feature gates
shellrow Jul 18, 2026
c5c23e4
chore: update README
shellrow Jul 18, 2026
53e6a5e
fix: prevent non-terminating IPv6 extension padding
shellrow Jul 18, 2026
be47414
feat: make packet builders fallible and validating
shellrow Jul 18, 2026
5299b6c
feat: establish Rust 1.88 as the workspace MSRV
shellrow Jul 18, 2026
e0d9dc0
feat: add cargo-deny supply-chain checks
shellrow Jul 18, 2026
b5d8806
fix: harden raw descriptor ownership and socket FFI safety
shellrow Jul 18, 2026
d091512
feat: stabilize the packet parsing API
shellrow Jul 19, 2026
3d22349
fix: harden datalink resource and FFI safety
shellrow Jul 19, 2026
7c13525
feat: stabilize core network interface types
shellrow Jul 19, 2026
c0f195a
feat: stabilize socket configuration APIs
shellrow Jul 19, 2026
a977e0c
docs: update docs
shellrow Jul 19, 2026
d4b26fa
perf: add allocation-free views and cache mutable layouts
shellrow Jul 19, 2026
3034c6b
fix: preserve checksum words across split buffers
shellrow Jul 19, 2026
5533ffe
test: expand benchmarks and parser robustness coverage
shellrow Jul 19, 2026
3dda8f2
fix: harden datalink and socket backend behavior
shellrow Jul 19, 2026
0ac2632
docs: update roadmap
shellrow Jul 19, 2026
c1eb7e6
fix: resolve Windows-only clippy warnings
shellrow Jul 20, 2026
aae5fe8
feat: load Npcap Packet.dll at run time
shellrow Jul 20, 2026
4381fe9
ci: lint and build platform backends on every OS
shellrow Jul 20, 2026
a55ac19
ci: streamline Rust checks
shellrow Jul 25, 2026
b8fb50d
chore: update dependencies
shellrow Aug 9, 2026
76dc7c5
fix: accept macOS BPF headers without trailing padding
shellrow Aug 9, 2026
0a2fec1
fix: restore iOS and NetBSD socket compatibility
shellrow Aug 9, 2026
9012082
chore: bump version to 0.27.0
shellrow Aug 9, 2026
dbf10d0
chore: delete unused TODO.md
shellrow Aug 9, 2026
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
35 changes: 35 additions & 0 deletions .github/actions/npcap-sdk/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Install Npcap SDK
description: >-
Download the Npcap SDK and expose Packet.lib / wpcap.lib to the MSVC linker.
nex-datalink declares `#[link(name = "Packet")]`, so any Windows job that
links a test or example binary needs this.

inputs:
version:
description: Npcap SDK version to install.
required: false
default: "1.13"

runs:
using: composite
steps:
- name: Download and extract the Npcap SDK
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$version = '${{ inputs.version }}'
$sdk = Join-Path $env:RUNNER_TEMP "npcap-sdk-$version"
$zip = Join-Path $env:RUNNER_TEMP "npcap-sdk-$version.zip"
Invoke-WebRequest -Uri "https://npcap.com/dist/npcap-sdk-$version.zip" -OutFile $zip
Expand-Archive -Path $zip -DestinationPath $sdk -Force

# The SDK ships per-architecture import libraries; x64 is what the
# windows-latest runners target.
$lib = Join-Path $sdk 'Lib\x64'
if (-not (Test-Path (Join-Path $lib 'Packet.lib'))) {
throw "Packet.lib not found under $lib"
}

# Append rather than overwrite so the MSVC toolchain's own LIB entries
# (added by the runner image) keep working.
"LIB=$lib;$env:LIB" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
77 changes: 68 additions & 9 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,82 @@ name: Rust

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

env:
CARGO_TERM_COLOR: always

concurrency:
group: rust-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Check
checks:
name: Checks
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
- name: Check formatting
run: cargo fmt --all -- --check
- name: Run Clippy
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: Run tests
run: cargo test --workspace --all-features

msrv:
name: MSRV
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.88.0
- uses: Swatinem/rust-cache@v2
- name: Check workspace with Rust 1.88
run: cargo check --workspace --all-targets --all-features

platform-check:
name: Check (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: true
matrix:
os: [macos-latest, windows-latest]

steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Check workspace
run: cargo check --workspace --all-targets --all-features

cross-target-check:
name: Check (${{ matrix.target }})
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
fail-fast: true
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
include:
- os: macos-latest
target: aarch64-apple-ios
- os: ubuntu-latest
target: x86_64-unknown-netbsd

steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- name: Check cross-platform library
run: cargo check -p nex --lib --features async,serde --target ${{ matrix.target }}
49 changes: 49 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Contributing

Run formatting, linting, and the workspace tests before submitting changes:

```sh
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
```

Raw socket and datalink changes also require the manual checks in
`docs/PRIVILEGED_TESTING.md`. Packet hot-path changes should be measured using
`docs/BENCHMARKING.md`.

## Fuzzing

Install the nightly fuzzing frontend and list the available targets:

```sh
cargo install cargo-fuzz
cargo +nightly fuzz list
```

Run a target with its checked-in seed corpus:

```sh
cargo +nightly fuzz run frame_parse
cargo +nightly fuzz run ethernet_vlan
cargo +nightly fuzz run dns_records
```

Use `cargo +nightly fuzz run <target> -- -max_total_time=60` for a bounded local
run. Crashes are written under `fuzz/artifacts/<target>`. Minimize a finding
before diagnosing it:

```sh
cargo +nightly fuzz tmin <target> fuzz/artifacts/<target>/<crash>
```

Every confirmed parser defect must receive a deterministic unit or integration
regression test before the implementation is fixed. Keep only sanitized,
minimal corpus inputs. The checked-in `hex:` format is decoded by targets that
use it and makes packet seeds reviewable without committing opaque binaries.

## Repository conventions

Documentation, comments, public APIs, and commit messages are written in
English. Avoid unrelated formatting or generated-file changes, and keep each
commit focused on one reviewable concern.
17 changes: 9 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,22 @@ members = [
]

[workspace.package]
version = "0.26.0"
version = "0.27.0"
edition = "2024"
rust-version = "1.88"
authors = ["shellrow <shellrow@foctal.com>"]

[workspace.dependencies]
nex-core = { version = "0.26.0", path = "nex-core" }
nex-datalink = { version = "0.26.0", path = "nex-datalink" }
nex-packet = { version = "0.26.0", path = "nex-packet" }
nex-sys = { version = "0.26.0", path = "nex-sys" }
nex-socket = { version = "0.26.0", path = "nex-socket" }
nex-core = { version = "0.27.0", path = "nex-core" }
nex-datalink = { version = "0.27.0", path = "nex-datalink" }
nex-packet = { version = "0.27.0", path = "nex-packet" }
nex-sys = { version = "0.27.0", path = "nex-sys" }
nex-socket = { version = "0.27.0", path = "nex-socket" }
serde = { version = "1" }
libc = "0.2"
netdev = { version = "0.41.0" }
netdev = { version = "0.46.0" }
mac-addr = { version = "0.3.0" }
ipnet = { version = "2.12" }
bytes = "1"
tokio = { version = "1" }
rand = "0.8"
rand = "0.10"
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ It includes sub-crates with responsibilities:

The project aims to expose portable low-level primitives.

## Minimum Supported Rust Version

`nex` requires Rust 1.88.0 or later. MSRV changes are treated as compatibility
changes and are verified in CI.

## Usage

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

```toml
[dependencies]
nex = "0.26"
nex = "0.27"
```

## Using Specific Sub-crates
Expand All @@ -34,6 +39,25 @@ You can also directly use specific sub-crates by importing them individually.

If you want to focus on network interfaces, you can use the [netdev](https://github.com/shellrow/netdev).

## Features

All optional features are disabled by default in `nex`, `nex-packet`,
`nex-datalink`, and `nex-socket`. `nex-core` keeps `gateway` enabled by default
because gateway discovery is part of its primary interface functionality.

| Feature | Crate | Purpose |
| --- | --- | --- |
| `async` | `nex`, `nex-datalink`, `nex-socket` | Enables asynchronous datalink I/O and Tokio-based socket APIs. |
| `pcap` | `nex`, `nex-datalink` | Enables the libpcap backend. |
| `serde` | `nex`, `nex-core`, `nex-packet`, `nex-datalink` | Enables serialization support for public data types. |
| `gateway` | `nex-core` | Enables default gateway discovery through `netdev`. |

To use asynchronous APIs through the facade:

```toml
[dependencies]
nex = { version = "0.27", features = ["async"] }
```

## Privileges
`nex-datalink` uses a raw socket which may require elevated privileges depending on your system's configuration.
Expand Down
30 changes: 30 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[graph]
all-features = true

[advisories]
ignore = [
{ id = "RUSTSEC-2024-0436", reason = "Transitive dependency of netdev on Linux; no maintained upgrade path is currently available." },
]

[licenses]
allow = [
"Apache-2.0",
"ISC",
"MIT",
"Unicode-3.0",
"Zlib",
]
confidence-threshold = 0.8

[bans]
multiple-versions = "warn"
wildcards = "deny"
highlight = "all"
workspace-default-features = "allow"
external-default-features = "allow"

[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []
30 changes: 30 additions & 0 deletions docs/BENCHMARKING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Benchmarking

Run packet benchmarks on an otherwise idle machine:

```sh
cargo bench -p nex-packet
```

`packet_parse` compares owned, decoded-view, and allocation-free slice parsing.
`packet_operations`
tracks Ethernet/VLAN, IPv4/IPv6, TCP/UDP, and DNS-name parsing together with
IPv4 serialization and checksum throughput. Criterion stores baselines under
`target/criterion`; compare changes with:

```sh
cargo bench -p nex-packet -- --save-baseline before
cargo bench -p nex-packet -- --baseline before
```

`FrameSlice` avoids packet-byte copies and heap allocation. `FrameView` may
allocate decoded variable-length options; owned parsing and serialization may
allocate or increment a `Bytes` reference count. Use an allocation profiler
such as DHAT, heaptrack, or Instruments when changing parser ownership.
Datalink send/receive throughput depends on kernel,
driver, interface, and privileges, so measure it manually using the matrix in
`PRIVILEGED_TESTING.md`; do not compare those results across hosts.

Benchmark changes are reviewed locally rather than gated by a fixed CI
percentage. Record the command, CPU, OS, Rust version, and Criterion comparison
when a change intentionally affects a hot path.
48 changes: 48 additions & 0 deletions docs/PACKET_MODEL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Packet Model

`nex-packet` separates packet handling into four categories:

| Category | Ownership | Mutation | Typical types |
| --- | --- | --- | --- |
| Borrowed view | Borrows caller bytes | No | `FrameSlice<'a>`, `FrameView<'a>` |
| Mutable borrowed view | Exclusively borrows caller bytes | In place | `MutableIpv4Packet<'a>`, `GenericMutablePacket<'a, P>` |
| Owned decoded packet | Owns serialized `Bytes` and decoded fields | By replacing owned fields | `Ipv4Packet`, `TcpPacket`, `DnsPacket` |
| Builder | Owns construction state | Validated setters/build | Types under `builder` |

Use allocation-free `FrameSlice` for layer boundaries on a hot path.
`FrameView` retains decoded header compatibility and may allocate for variable
options. Use a mutable view when editing an existing buffer, an owned packet
when data must outlive the input, and a builder when constructing new wire
data.

## Mutable layout and freeze

Protocol-specific mutable views validate their minimum layout on construction.
`GenericMutablePacket` additionally caches header and payload boundaries after
one parse. Ordinary field and payload edits do not cause another parse.
Changing a structural field through `packet_mut()` does not update the cached
boundaries; call `refresh_layout()` before requesting slices under the new
layout.

`freeze()` is the commit point. It parses the current bytes again and returns
an owned packet only when the complete layout is valid. The parse is deliberate:
it prevents stale or inconsistent mutable bytes from becoming an owned packet.

The allocation-returning `Packet::to_bytes_mut`, `header_mut`, and
`payload_mut` compatibility methods are deprecated. Their explicit
`copy_*_to_bytes_mut` replacements make the allocation visible at call sites.

## Generated representation details

The `nex_core::bitfield` aliases are `#[doc(hidden)]` implementation details
shared by packet field representations. They are public for cross-crate code
generation only and carry no independent semantic contract.

## Variable-length protocol data

IPv4 options, IPv6 extension headers, TCP options, DNS compression, and DHCP
options are length-delimited and reject truncated structural fields. Parsers
that distinguish capture truncation expose `ParseMode`: lenient mode preserves
available captured payload where documented, while strict mode rejects data
shorter than its declared protocol length. Fuzz and property tests exercise
both arbitrary input and valid parse/serialize round trips.
Loading
Loading