# net-wire
**`net-wire`** is one `no_std` Rust crate for inspecting and constructing exact network-wire layouts. It exposes byte-oriented, borrowed views; leaves storage and I/O with the caller; and provides bounded protocol state where the wire format actually needs it.
It targets **Rust 1.91** and **edition 2024**.
## ✨ What it promises
- **No production allocation, dependencies, or `unsafe`.** Production code is allocation-free, has zero normal dependencies, and builds with `unsafe_code = deny`.
- **Opt in deliberately.** The default feature set is empty.
- **Borrowed wire views.** Variable-length bytes stay borrowed from caller input; construction writes into caller-provided buffers.
- **Wire fidelity.** Unknown, private, reserved, GREASE, duplicate, ordered, and legal noncanonical values remain representable and preserved when the format permits them.
- **Bounded, not ambient state.** Protocol-local state such as compression-table accounting can be caller-owned without turning the crate into a connection runtime.
> [!NOTE]
> A successful parse establishes the structural boundary promised by that parser. It does not silently apply an application's semantic policy.
## 🚫 What it is not
`net-wire` does **not** provide sockets or I/O, an async runtime, connection or application state machines, a TLS/cryptography stack, a universal codec/schema framework, or automatic semantic policy. It is deliberately a wire-layout crate, not a network stack wearing a fake moustache.
## 🚀 Quick start
Enable only the formats you use. For Ethernet, IPv4, and UDP dispatch:
```toml
[dependencies]
net-wire = { version = "0.1.0", default-features = false, features = ["ethernet", "ipv4", "udp"] }
```
### Parse a nested packet
Each layer first parses its own bounded bytes, then dispatches only when its discriminator agrees:
```rust
use net_wire::{
ethernet::{EtherType, EthernetFrame},
ipv4::Ipv4Packet,
};
fn inspect(bytes: &[u8]) -> Result<(), net_wire::ParseError> {
let frame = EthernetFrame::parse(bytes)?;
if frame.ether_type() != EtherType::IPV4 {
return Ok(());
}
let packet = Ipv4Packet::parse(frame.payload())?;
match packet.udp()? {
Some(datagram) => {
println!("UDP {} -> {}", datagram.source_port(), datagram.destination_port());
println!("payload: {:02x?}", datagram.payload());
}
None => println!("not a whole UDP datagram for this IPv4 packet"),
}
Ok(())
}
```
Feature-gated dispatch methods use `Result<Option<T>, E>` on purpose:
- `Ok(Some(value))` — the selected protocol matches and its bounded bytes are structurally valid.
- `Ok(None)` — the discriminator does not match, or the enclosing packet is fragmented where that dispatch cannot safely claim a whole child.
- `Err(error)` — the selected child is malformed.
### Build into caller storage
Write payload bytes first, then calculate the transport checksum, then build the IPv4 header (and therefore its header checksum):
```rust
use net_wire::{
ipv4::{Ipv4Address, Ipv4PacketBuilder, Ipv4Protocol},
udp::UdpDatagramBuilder,
};
let source = Ipv4Address::new([192, 0, 2, 10]);
let destination = Ipv4Address::new([198, 51, 100, 20]);
let payload = b"ping";
let mut bytes = [0_u8; 20 + 8 + 4];
{
let mut udp = UdpDatagramBuilder::new(&mut bytes[20..], payload.len())
.source_port(49_152)
.destination_port(53)
.build()
.unwrap();
udp.payload_mut().copy_from_slice(payload);
udp.update_checksum_ipv4(source, destination);
}
let packet = Ipv4PacketBuilder::new(&mut bytes, 8 + payload.len())
.source(source)
.destination(destination)
.protocol(Ipv4Protocol::UDP)
.ttl(64)
.build()
.unwrap();
assert!(packet.checksum_is_valid());
```
> [!IMPORTANT]
> Structural validation answers whether bytes form a safely bounded layout. Semantic validation answers protocol-specific questions such as allowed values, ordering, or state transitions. Keep those decisions explicit.
>
> Editing payloads or fields through mutable views can stale checksums and dependent lengths. Setters and raw mutable slices do not repair them automatically; recompute or rebuild in the right order.
## 🧭 Consumption is part of the contract
Not every protocol consumes input the same way. `TlsRecord::parse` returns the first complete record; `record.as_bytes().len()` tells you the prefix it represented. Some public APIs intentionally return `(item, suffix)`, such as a KCP segment or QUIC frame. Other APIs validate a complete sequence, such as QPACK instruction sequences.
Use the contract of the specific parser rather than assuming every `parse` consumes all input. That distinction prevents a coalesced TLS record, a frame stream, or a sequence codec from being accidentally treated as one uniform blob.
## 🧩 Features
| `ethernet` | Ethernet II frames |
| `arp` | ARP packets |
| `ipv4` / `ipv6` | Internet packets and dispatch helpers |
| `icmpv4` / `icmpv6` | ICMP messages |
| `udp` / `tcp` | Transport datagrams and segments |
| `kcp` | KCP segments |
| `tls` | TLS records, handshakes, and extensions |
| `http1` | HTTP/1 heads and chunked framing |
| `http2` | HTTP/2 frames, sequencing, and HPACK |
| `quic` | QUIC primitives, packets, frames, and transport parameters |
| `qpack` | QPACK primitives and caller-owned bounded state |
| `http3` | HTTP/3 frames and bounded protocol state; enables `quic` and `qpack` |
Features select public protocol modules. Optional cross-layer adapters are available only when **both** involved features are enabled; enabling a lower layer never drags in an upper one.
## 📚 Examples
- [`packet-inspector`](examples/packet-inspector/) — parses an Ethernet frame and follows IPv4/IPv6 payloads to UDP or TCP, including checksum reporting.
- [`dns-query`](examples/dns-query/) — a **Linux-only raw IPv4** DNS query path without Ethernet framing, with a small project-local DNS codec; `socket2` is an example-only dev dependency, not a production dependency of `net-wire`.
- [`tls-client-hello`](examples/tls-client-hello/) — walks a TLS ClientHello while preserving extension envelopes and handles RFC 7685 padding with a small local borrowed parser.
For contributor and design rules, see [**ARCHITECTURE.md**](ARCHITECTURE.md).