Rust API Guide

July 5, 2026 · View on GitHub

This document summarizes the public Rust surface for generated packet tools. The API focuses on explicit builders, deterministic compile/decode behavior, bounded pcap workflows, and packet-level wire workflows. The only public crate is crafter.

Most examples should start with:

use crafter::prelude::*;

One Public Crate

Install and depend on crafter only. The public Rust surface is organized as modules inside the one crate:

ModuleResponsibility
crafter::preludeCommon imports for examples and agent-written tools.
crafter::corePacket model, layer model, encode/decode, checksums, formatting, and protocol registry.
crafter::wireWire packet I/O through PacketWire, PacketRecord, packet sources, packet writers, sniffers, transmitters, and transform chains.
crafter::netInterfaces, raw sockets, send, send-receive, routing helpers, and address helpers.

Packet Composition

Generated tools should prefer explicit builders:

let packet = Packet::new()
    .push(
        Ipv4::new()
            .src("192.0.2.10")?
            .dst("198.51.100.20")?
            .ipv4_protocol(Ipv4Protocol::Icmpv4),
    )
    .push(Icmpv4::echo_request().id(0x1234).seq(1))
    .push(Raw::from_bytes(b"HelloPing!\n"));

let bytes = packet.compile()?;

Concise examples may use / composition:

let packet =
    Ipv4::new().dst("198.51.100.20")?
        .ipv4_protocol(Ipv4Protocol::Icmpv4)
    / Icmpv4::echo_request().seq(1)
    / Raw::from_bytes(b"HelloPing!\n");

Use Ipv4Protocol for known IPv4 protocol-field values and Ipv4::protocol(u8) only when a tool deliberately needs an arbitrary raw protocol byte.

IGMP follows the same packet abstraction as every other IPv4 protocol body: compose the Ipv4 envelope explicitly with protocol Ipv4Protocol::Igmp, the required TTL, destination, and Router Alert option when the packet shape needs them, then add Igmp and typed IGMP body layers with /. The crate exposes IGMP packet construction and decode; it is not a multicast router implementation, snooper, proxy, scanner, or state machine.

TLS Packets

TLS support is exported through crafter::prelude::* as typed packet primitives such as Tls, TlsRecord, TlsHandshake, TlsClientHello, TlsServerHello, TlsRawExtension, TlsVersion, TlsContentType, and common TLS codepoint constants. Build TLS like any other layer, under TCP:

use crafter::prelude::*;

fn main() -> crafter::Result<()> {
    let hello = TlsClientHello::new()
        .with_random([0x44; TLS_CLIENT_HELLO_RANDOM_LEN])
        .with_raw_cipher_suites([TLS_CIPHER_SUITE_AES_128_GCM_SHA256])
        .with_extensions(vec![TlsRawExtension::alpn(
            TlsAlpnProtocols::h2_then_http_1_1(),
        )?]);
    let record = TlsRecord::handshake_messages([
        TlsHandshake::from_client_hello(hello)?,
    ])?;

    let packet = Ipv4::new()
        .src("192.0.2.10")?
        .dst("198.51.100.20")?
        .ipv4_protocol(Ipv4Protocol::Tcp)
        / Tcp::new().sport(49_171).dport(TLS_PORT_HTTPS)
        / Tls::from_record(record);

    let bytes = packet.compile()?;
    let decoded = Packet::decode_from_l3(NetworkLayer::Ipv4, bytes.as_bytes())?;
    println!("{}", decoded.summary());
    Ok(())
}

compile() fills TLS record and handshake lengths, TLS vector lengths, and the enclosing TCP/IP dependent fields unless the caller set an explicit override. Decode preserves unknown codepoints and encrypted payloads as typed opaque bytes or Raw where appropriate. TLS is a packet primitive, not a TLS session stack or decryption engine; see TLS wire coverage for the full boundary.

mDNS And DNS-SD Packets

mDNS support stays inside the existing DNS layer. Use Dns, DnsQuestion, DnsRecord, DnsName, and crafter::protocols::dns::mdns helpers to build UDP/5353 packet shapes, DNS-SD service names, QU questions, cache-flush records, known answers, probes, announcements, and goodbyes. The crate does not implement a resolver, responder daemon, cache, scanner, or service registry.

use crafter::prelude::*;
use std::net::Ipv4Addr;

fn main() -> crafter::Result<()> {
    let service = mdns::dns_sd_tcp_service_name("ipp", DNS_SD_DEFAULT_DOMAIN)?;
    let question = DnsQuestion::new(service, DNS_TYPE_PTR).mdns_qu(true);
    let dns = mdns::query(question);
    let packet = mdns::mdns_ipv4_packet(Ipv4Addr::new(192, 0, 2, 10), dns);

    let bytes = packet.compile()?;
    let decoded = Packet::decode_from_l3(NetworkLayer::Ipv4, bytes.as_bytes())?;
    println!("{}", decoded.summary());
    Ok(())
}

Explicit fields remain explicit: callers can override IDs, flags, ports, classes, TTLs, and record data for fixed vectors or malformed-packet work. See mDNS and DNS-SD wire coverage for the packet-level helper catalog.

Builder Conventions

PatternMeaning
Layer::new()Construct a layer with unset auto-fill fields.
Layer::request() / Layer::reply()Protocol-specific semantic constructors when useful.
.field(value)Set a field explicitly and preserve it during compile.
.try_field(value)?Parse or validate a fallible field value.
.option(...)Append protocol options while preserving order.
.payload(...)Attach raw bytes when no higher layer is needed.
.clear_field()Return a field to unset so auto-fill may apply.

The model distinguishes unset, defaulted, and explicitly set fields. compile may fill unset lengths, protocol numbers, header lengths, and checksums, but it must not overwrite explicit user input.

Compile And Decode

let bytes = packet.compile()?;

let decoded = Packet::decode_from_link(LinkType::Ethernet, &bytes)?;
let l3 = Packet::decode_from_l3(NetworkLayer::Ipv4, &bytes)?;
let ipv4 = Packet::decode_ipv4(&bytes)?;

Decode rules:

  • Malformed enclosing headers return structured errors.
  • Unsupported next protocols are preserved as Raw.
  • Link-layer entry points accept explicit link types rather than guessing.
  • Deterministic inputs compile to deterministic bytes.

Typed Layer Access

let packet = Packet::decode_from_link(LinkType::Ethernet, frame)?;

let ip = packet.layer::<Ipv4>().ok_or("missing Ipv4")?;
let tcp = packet.layer::<Tcp>();

for layer in packet.iter() {
    println!("{}", layer.summary());
}
Rust APIPurpose
packet.layer::<T>() -> Option<&T>First layer of type T.
packet.layer_mut::<T>() -> Option<&mut T>Mutable typed access before compile.
packet.layers::<T>() -> impl Iterator<Item = &T>All layers of type T.
packet.get(index) -> Option<&dyn Layer>Positional stack access.
packet.iter()Ordered layer iteration.

Inspection Helpers

WorkflowRust API
Detailed treepacket.show()
One-line packet descriptionpacket.summary()
Hex outputpacket.hexdump() and hexdump(&bytes)
Raw payload as textpacket.raw_string_lossy()
Reply matching filterpacket.reply_filter()

summary should be compact and stable enough for snapshot tests. show can be more verbose and field-oriented.

Pcap Through Packet Wire

let source = PacketWire::pcap_file("input.pcap")
    .filter("tcp or udp")
    .open()?
    .source()?;

let records = Sniffer::new(source).collect_records()?;

for record in records {
    println!("{}", record.packet().summary());
    println!("{:?}", record.metadata());
}

Deterministic pcap output uses a recorder backend and Transmitter:

let writer = PacketWire::pcap_recorder("out.pcap", LinkType::Ethernet)
    .open()?
    .writer()?;

let mut tx = Transmitter::new(writer);
let reports = tx.send(packet)?;

Low-level pcap codec details are owned by the packet wire backend. User code should enter through PacketWire, Sniffer, and Transmitter so reads yield packet records with pcap metadata and writes consume packets or packet records. TLS pcap workflows use the same path, usually with a BPF filter such as tcp port 443; decoded records expose IPv4/IPv6, TCP, and Tls layers when the TCP payload contains complete TLS records.

Wire Packet I/O

The crate-level packet I/O surface lives under crafter::wire. See docs/reference/wire.md for the full guide to PacketWire, PacketRecord metadata, PacketTransform, Sniffer, and Transmitter. See docs/reference/wire.md for the inspectable API inventory and backend responsibility map.

The public stream shape is:

APIRole
PacketWireOpens one packet-capable backend or interface and exposes explicit source or writer capabilities.
PacketSourceSynchronous packet-record input trait used by Sniffer.
PacketWriterPacket-record output trait used by Transmitter.
PacketRecordStream item containing a Packet plus inspectable backend, link, pcap, transform, and medium metadata.
PacketTransformStateful zero/one/many stream transform contract for inbound or outbound packet records.
SnifferOwns one PacketSource, applies inbound transforms, and yields transformed PacketRecord values.
TransmitterOwns one PacketWriter, applies outbound transforms, and returns ordered write reports.

PacketWire::source(), PacketWire::writer(), and PacketWire::split() consume the opened wire and return typed WireError::UnsupportedCapability errors when the backend cannot satisfy the requested direction. The pcap codec and libpcap integration live behind the wire backend boundary; wire is the user-facing packet stream abstraction.

WPA decryption is available as the stateful WpaDecrypt inbound PacketTransform. It observes beacons and EAPOL handshakes, keeps per-network key state for configured SSID/passphrase or SSID/PMK entries, and emits decrypted packet records without changing Sniffer. The implemented decrypt path is passive WPA2-PSK CCMP-128; unsupported ciphers and missing key material remain inspectable metadata on packet-shaped records.

Offline pcap input:

let source = PacketWire::pcap_file("input.pcap")
    .filter("icmp")
    .open()?
    .source()?;

let mut sniffer = Sniffer::new(source).count(10);

while let Some(record) = sniffer.next_record()? {
    println!("{}", record.packet().summary());
    println!("{:?}", record.metadata());
}

Live pcap capture is explicit and bounded:

let source = PacketWire::pcap_interface("eth0")
    .filter("tcp port 80")
    .timeout(Duration::from_secs(1))
    .open()?
    .source()?;

let records = Sniffer::new(source)
    .timeout(Duration::from_secs(3))
    .count(100)
    .collect_records()?;

Offline pcap output and raw socket dry-run writing use Transmitter:

let writer = PacketWire::pcap_recorder("out.pcap", LinkType::Ethernet)
    .open()?
    .writer()?;

let mut tx = Transmitter::new(writer);
let reports = tx.send(packet)?;

PacketWire::raw_socket_interface("eth0") defaults to dry-run planning and requires .live() for real raw socket transmission. Live capture or transmit belongs in authorized endpoint provider or lab workflows, not local static tests.

IP Fragment Transforms

IpDefrag and IpFragment are packet-stream transforms under crafter::wire. They do not change the Packet builder surface: generated tools still compose IPv4 and IPv6 packets normally, then place the transform on the stream that needs it.

Use IpDefrag on receive-side sources and sniffers. It buffers IPv4 fragments and supported IPv6 Fragment Header records until a datagram is complete, then emits one packet-shaped PacketRecord with IpDefragMetadata and transform trace entries. Non-fragmented records pass through by default.

let first = PacketRecord::new(
    Ipv4::new().src("192.0.2.10")?.dst("198.51.100.20")?
        .protocol(IPPROTO_EXPERIMENTAL_1)
        .identification(0x4444)
        .more_fragments(true)
        .fragment_offset(0)
        / Raw::from_bytes(b"abcdefgh"),
);
let final_fragment = PacketRecord::new(
    Ipv4::new().src("192.0.2.10")?.dst("198.51.100.20")?
        .protocol(IPPROTO_EXPERIMENTAL_1)
        .identification(0x4444)
        .fragment_offset(1)
        / Raw::from_bytes(b"ijkl"),
);

let source = VecPacketSource::new([final_fragment, first]);
let records = Sniffer::new(source)
    .with(IpDefrag::new())
    .collect_records()?;

Use IpFragment on transmit-side writers and transmitters. It has an explicit MTU, emits one or more packet-shaped fragment records, and records IpFragmentMetadata on each emitted record. Offline pcap recorders and MemoryPacketWriter::dry_run() are the default examples; use documentation addresses such as 192.0.2.0/24, 198.51.100.0/24, and 2001:db8::/32.

let writer = MemoryPacketWriter::dry_run();
let mut tx = Transmitter::new(writer).with(IpFragment::new(1280));

let reports = tx.send(
    Ipv6::new().src_str("2001:db8:10::1")?.dst_str("2001:db8:10::2")?
        / Udp::new().sport(40000).dport(40001)
        / Raw::from_bytes(&[0u8; 1600]),
)?;

assert!(reports.iter().all(|report| report.is_dry_run()));

These transforms reconstruct IP datagrams only. TCP stream reassembly, fragmented application payload reconstruction, HTTP/file recovery, and full stack delivery are out of scope for the crate primitive.

Send And Send-Receive

The net API has both one-shot and reusable send surfaces:

APIRole
send_packet and SocketSenderCompatibility one-shot sends and dry-run plans for callers that send one packet at a time.
PacketSenderReusable sender that keeps one live backend open for an explicit homogeneous send class.
PacketWire::raw_socket_interfacePacket-wire writer surface for Transmitter; live writers reuse PacketSender internally.

PacketSender is useful when a generated tool sends several packets through the same interface. Dry-run mode only compiles and plans packets:

let packet =
    Ipv4::new().src("192.0.2.10")?.dst("198.51.100.20")?
    / Udp::new().sport(40000).dport(33434)
    / Raw::from("dry-run payload");

let mut sender = PacketSender::open(
    SendOptions::new()
        .iface("eth0")
        .network_layer()
        .dry_run(),
)?;

let report = sender.send(&packet)?;
assert!(report.is_dry_run());

Live reusable senders must use .live() with an explicit .link_layer() or .network_layer() mode. Link-layer live senders handle Ethernet and radiotap frames; network-layer live senders currently handle bare IPv4 packets. Full-header IPv6 network-layer live sends remain unsupported. A PacketSender is homogeneous, so tools that need both Ethernet/radiotap frames and bare IPv4 packets should open separate senders.

let report = packet.send_recv_report(
    SendRecv::new()
        .iface("eth0")
        .network_layer()
        .timeout(Duration::from_secs(1))
        .retries(3)
        .filter(packet.reply_filter()?),
)?;

if let Some(reply) = report.reply() {
    println!("{}", reply.summary());
}

Batch send-receive returns positional reports:

let report = send_recv_packets(
    &packets,
    BatchSendRecv::new()
        .iface("eth0")
        .network_layer()
        .timeout(Duration::from_millis(750))
        .retries(1),
)?;

Raw socket operations are exposed through typed wrappers rather than integer file descriptors in normal examples. Use PacketWire::raw_socket_interface("eth0").link_layer().live() for live Ethernet or radiotap writer streams, and PacketWire::raw_socket_interface("eth0").network_layer().live() for live bare IPv4 writer streams.

UDP Options

UDP options (RFC 9868) are modeled as a typed surplus layer. A packet stack places UdpOptions after the UDP user payload:

let options = UdpOptions::new()
    .udp_option(UdpOption::maximum_datagram_size(1200))?
    .udp_option(UdpOption::echo_request(0x0102_0304))?
    .additional_payload_checksum();

let packet = Ipv4::new()
    .src("192.0.2.10")?
    .dst("198.51.100.20")?
    / Udp::new().sport(53000).dport(33434)
    / Raw::from("probe")
    / options;

let bytes = packet.compile()?;
let decoded = Packet::decode_from_l3(NetworkLayer::Ipv4, bytes.as_bytes())?;
let udp = decoded.layer::<Udp>().ok_or("missing UDP")?;
let udp_options = decoded.layer::<UdpOptions>().ok_or("missing UDP options")?;

assert_eq!(udp.checksum_status(), UdpChecksumStatus::Valid);
assert_eq!(udp_options.status(), UdpOptionStatus::Valid);

compile() fills the normal UDP length and checksum over the UDP user payload, then materializes the UDP surplus area after that length. It also fills the UDP Option Checksum (OCS) and any auto additional_payload_checksum() APC option. Explicit UDP length, UDP checksum, OCS, and APC values are preserved, including intentionally wrong values.

Rust APIPurpose
UdpOptions::new()Start an empty surplus option layer.
UdpOptions::from_options(...)Encode typed UdpOption values.
UdpOptions::from_bytes(...)Preserve already encoded option bytes.
UdpOptions::udp_option(...)Append one typed option.
UdpOptions::additional_payload_checksum()Append an APC whose CRC32c is filled from UDP user data at compile time.
UdpOptions::option_checksum(value)Set OCS explicitly for malformed or fixed-vector cases.
UdpOptions::status()Return decoded or parsed UdpOptionStatus.
UdpOptions::options()Borrow cached parsed UdpOption values.
UdpOptions::option_iter()Iterate over the encoded option bytes.

Typed options cover EOL, NOP, APC, MDS, MRDS, REQ, RES, TIME, SAFE/UNSAFE experimental options, and generic known or unknown option kinds. Unknown SAFE options are preserved with UdpOptionStatus::UnknownSafe; unknown UNSAFE options are preserved with UdpOptionStatus::UnknownUnsafe and should be treated as unsupported behavior by higher-level tools. UDP FRAG is preserved as raw option data and reported as UdpOptionStatus::UnsupportedFragmentation because full UDP fragmentation/reassembly is not in scope.

Udp::checksum_status() reports checksum validation on decoded packets: Valid, Invalid, Ipv4NoChecksum, Ipv6ZeroChecksum, or NotChecked. UdpChecksumStatus::Ipv6ZeroChecksum is not accepted as ordinary IPv6 UDP; call requires_ipv6_zero_checksum_exception() when a tool explicitly supports the RFC 6935/RFC 6936 tunnel exception model.

SCTP Packets

SCTP support is exported through crafter::prelude::* as typed packet-layer values such as Sctp, SctpChunk, SctpParameter, SctpErrorCause, SctpChecksumStatus, and the SCTP codepoint constants. It composes directly under IPv4 or IPv6, and compile() fills IP protocol / next-header 132, SCTP chunk lengths, padding, and CRC32c checksum values when those fields are unset.

let packet =
    Ipv4::new().src("192.0.2.10")?.dst("198.51.100.20")?
    / Sctp::data(
        0x0102_0304,
        1,
        1,
        SCTP_PPID_WEBRTC_STRING,
        b"payload".to_vec(),
    )
    .sport(5_000)
    .dport(5_001)
    .vtag(0x1122_3344);

let bytes = packet.compile()?;
let decoded = Packet::decode_from_l3(NetworkLayer::Ipv4, bytes.as_bytes())?;
let sctp = decoded.layer::<Sctp>().ok_or("missing SCTP")?;

assert_eq!(sctp.checksum_status(), SctpChecksumStatus::Valid);
assert_eq!(sctp.chunk_count(), 1);

Native SCTP decode is selected from IPv4 protocol or IPv6 next-header value 132. RFC 6951 UDP encapsulation is also supported on SCTP_UDP_ENCAPSULATION_PORT (9899) after a conservative payload shape check:

let packet =
    Ipv4::new().src("192.0.2.30")?.dst("198.51.100.30")?
    / Udp::new().sport(49_152).dport(SCTP_UDP_ENCAPSULATION_PORT)
    / Sctp::data(0x1111_2222, 1, 2, SCTP_PPID_WEBRTC_STRING, b"udp-sctp".to_vec())
        .sport(5_010)
        .dport(5_011)
        .vtag(0x1020_3040);

let decoded = Packet::decode_from_l3(NetworkLayer::Ipv4, packet.compile()?.as_bytes())?;
assert!(decoded.layer::<Udp>().is_some());
assert!(decoded.layer::<Sctp>().is_some());

UDP/9899 payloads that do not have SCTP structure remain Raw, and custom registries with application decoding disabled keep SCTP bytes raw behind UDP.

Unknown SCTP chunk types, parameter types, error causes, PPIDs, flags, and extension values remain inspectable packet data when structurally valid. Use raw-preserving helpers such as SctpChunk::unknown, SctpParameter::unknown, SctpUnknownParameter, and SctpErrorCause::unknown when a generated tool needs future, private, reserved, or malformed codepoints. Truncated common headers and bad chunk, parameter, or cause lengths return structured errors with context rather than panicking.

Sctp::checksum(value) preserves explicit checksums, including intentionally wrong values. Decode reports SctpChecksumStatus::Valid, SctpChecksumStatus::Invalid, SctpChecksumStatus::ZeroChecksum, or SctpChecksumStatus::NotChecked; invalid or zero checksums do not discard the packet. See SCTP wire coverage for the full chunk, parameter, fixture, and live-safety boundary. SCTP is a packet primitive, not an association stack or socket API.

DHCPv4 Packets

Dhcpv4 is a packet primitive: it crafts and inspects BOOTP/DHCPv4 frames but is not a DHCPv4 client, server, or lease engine. It composes over UDP, compiles through Packet::compile() with protocol-correct defaults, and decodes through the registry when carried on the DHCPv4 ports.

Named constructors cover the registered message types:

let discover = Dhcpv4::discover(client_mac);
let request = Dhcpv4::request(client_mac, "192.0.2.50".parse()?, "192.0.2.1".parse()?);
let offer = Dhcpv4::offer(client_mac, "192.0.2.50".parse()?, "192.0.2.1".parse()?);
// Also: decline, ack, nak, release, inform, force_renew,
// lease_query_by_ip, lease_query_by_mac, lease_query_by_client_id.

Option model

Options follow a code-plus-value model rather than one enum variant per code. A Dhcpv4Option exposes its registered code, a typed Dhcpv4OptionValue for formats the codec understands, and the raw payload bytes for everything else. Unknown, private-use, removed, ambiguous, and vendor-specific option payloads are preserved as raw bytes rather than dropped or guessed.

let dhcpv4 = Dhcpv4::discover(client_mac)
    .option(Dhcpv4Option::parameter_request_list([1, 3, 6, 15]))
    .host_name("workstation");

Use Dhcpv4::concatenated_option(code) to read a logical option that may be split across repeated instances (RFC 3396 long options). Per-area raw segments remain inspectable through the segment scanner.

Option overload, long options, and areas

DHCPv4 options may live in the normal options area or in the overloaded file and sname BOOTP fields (option 52). Place options into those areas with file_options(...) / sname_options(...); compile() auto-inserts the overload option (52) when an area is used, and option_overload() reports the decoded selector. Long values are split into 255-byte segments on encode and concatenated back into one logical value on decode (RFC 3396), while the raw segments stay inspectable.

Relay agent information (option 82)

Relay agent information (RFC 3046, option 82) is a typed container of sub-options. Registered sub-options decode to typed values; unknown sub-options are preserved with Dhcpv4RelaySuboption::other(code, data).

let relay = Dhcpv4RelayAgentInfo::new(vec![
    Dhcpv4RelaySuboption::circuit_id(b"eth0:vlan100".to_vec()),
    Dhcpv4RelaySuboption::remote_id(b"relay-1".to_vec()),
]);
let dhcpv4 = Dhcpv4::discover(client_mac).relay_agent_info(relay);
let recovered = dhcpv4.relay_agent_information();

Client identifiers (option 61)

Dhcpv4ClientIdentifier covers the common Ethernet MAC form, the RFC 4361 node-specific (IAID + DUID) form, and a raw fallback:

let id = Dhcpv4ClientIdentifier::ethernet_mac(client_mac.octets());
let dhcpv4 = Dhcpv4::discover(client_mac).client_id_value(id);
let recovered = dhcpv4.client_identifier_value();

Authentication and leasequery packet fields

These are packet fields only; the crate does not derive, sign, or verify authentication, and does not run a leasequery state machine. The authentication option (RFC 3118, option 90) is exposed as Dhcpv4Authentication (authentication()), and the leasequery family exposes typed status/state values (Dhcpv4StatusCodeOption, Dhcpv4State, Dhcpv4DataSource) read back through status_code(), dhcp_state(), and associated_ip().

Raw and opaque cases

Some registered codepoints are intentionally kept raw (for example PCP server 158, DNR 162, 6RD 212, and ambiguous historical codes). In all cases the option code and payload bytes are preserved so they remain inspectable and re-encodable.

DHCPv6 Packets

Dhcpv6 is a packet primitive for DHCPv6 client/server and relay messages. It builds and inspects packet bytes; it is not a DHCPv6 client, server, relay daemon, lease database, policy engine, or address allocator. Compose it under IPv6/UDP with Udp::dhcpv6_client(), Udp::dhcpv6_server(), or Udp::dhcpv6_relay(), then use the normal Packet::compile(), Packet::decode_from_l3, summary(), and show() surface.

Named constructors cover common top-level message shapes:

let client_duid = Dhcpv6Duid::ll(1, [0x02, 0x00, 0x5e, 0x00, 0x06, 0x01]);
let solicit = Dhcpv6::solicit(0x010203)
    .client_duid(client_duid)
    .oro([DHCPV6_OPTION_DNS_SERVERS, DHCPV6_OPTION_DOMAIN_LIST])
    .elapsed_time(1);

let packet =
    Ipv6::new().src("2001:db8::10")?.dst("2001:db8::1")?
    / Udp::dhcpv6_client()
    / solicit;

Client/server messages preserve the 24-bit transaction ID. Relay-forward and relay-reply messages use the relay header and carry nested DHCPv6 bytes through OPTION_RELAY_MSG:

let relay = Dhcpv6::relay_forward("2001:db8:100::".parse()?, "2001:db8::10".parse()?)
    .hop_count(1)
    .interface_id(b"access-loop-1".as_slice())
    .relay_message(Dhcpv6::solicit(0x0a0b0c).client_id([
        0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x5e, 0x00, 0x06, 0x02,
    ]))?;

DHCPv6 options and identities

The DHCPv6 option model preserves order and raw payload bytes. Registered helpers cover DUIDs, ORO, elapsed time, status codes, DNS/domain options, vendor options, relay metadata, boot options, IA_NA/IA Address, IA_PD/IA Prefix, leasequery families, and service discovery options. Unknown, private-use, future, or malformed option payloads can still be carried with Dhcpv6Option::raw(...) or Dhcpv6::raw_option(...).

let prefix = Dhcpv6IaPrefix::new(300, 600, 56, "2001:db8:200::".parse()?);
let ia_pd = Dhcpv6IaPd::new(0x0506_0708, 90, 180).ia_prefix(prefix)?;
let reply = Dhcpv6::reply(0x050607)
    .client_id([0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x5e, 0x00, 0x06, 0x03])
    .server_id([0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x5e, 0x00, 0x06, 0x04])
    .ia_pd(ia_pd)?
    .status(Dhcpv6StatusCode::Success);

DHCPv6 decode and safe workflows

Decode dispatch is tied to IPv6/UDP DHCPv6 ports and conservative payload recognition. Valid but unknown message types, option codepoints, DUID types, and status codes remain inspectable instead of being dropped. Truncated headers or TLVs return structured errors.

Examples and generated tools should use documentation address space and either offline compile/decode or dry-run send/receive plans. Real DHCPv6 traffic belongs in an authorized provider-backed endpoint or lab workflow.

Address And Range Helpers

let targets = Ipv4Range::parse("192.0.2.1-20")?;
HelperPurpose
find_interface(name)Load interface metadata.
get_ip_strings(iface)Return local addresses as strings for generated CLIs.
Ipv4Range::parse(...)Parse IPv4 CIDR/range/list expressions.

Protocol Names

ProtocolRust type
Raw bytesRaw
EthernetEthernet
ARPArp
IPv4Ipv4
IGMPIgmp, IgmpQuery, IgmpReport, IgmpGroupRecord, IgmpExtension
IPv6Ipv6
TCPTcp
UDPUdp
UDP surplus optionsUdpOptions, UdpOption
SCTPSctp, SctpChunk, SctpParameter, SctpErrorCause
ICMPv4Icmpv4
ICMPv6Icmpv6, Icmpv6Body
ICMPv6 Neighbor Discovery optionsNdpOptions, NdpOption
DNSDns
mDNS / DNS-SDDns, DnsQuestion, DnsRecord, and crafter::protocols::dns::mdns helpers
DHCPv4Dhcpv4
DHCPv6Dhcpv6, Dhcpv6Option, Dhcpv6Duid, Dhcpv6IaNa, Dhcpv6IaPd
802.1Q VLANVlan
Null/loopbackNullLoopback
Linux cooked captureLinuxSll

IPv4-specific construction and decode behavior is covered in IPv4 wire coverage. The Ipv4 layer exposes DSCP/ECN helpers, protocol-number labels and constants, decode-time checksum status, typed IPv4 options, fragment metadata fields, IpDefrag / IpFragment packet-stream transforms, enriched summary() / show() output, and Raw fallback for unknown or unsupported payloads.

IGMP-specific construction and decode behavior is covered in IGMP wire coverage. Igmp is the fixed IGMP header that composes after an explicit Ipv4 layer, with typed IgmpQuery, IgmpReport, IgmpGroupRecord, and IgmpExtension layers for supported IGMPv3 bodies and generic RFC 9279 extensions. compile() fills unset IGMP checksums and dependent counts or lengths while preserving explicit overrides, including intentionally malformed values. Decode supports IGMPv1, IGMPv2, IGMPv3, and multicast router discovery packet shapes, while unsupported registered or unknown valid payloads remain inspectable as raw bytes.

IPv6 base-header and extension-header details live in IPv6 wire coverage, including source manifests, fixture coverage, and the offline ipv6-enrichment oracle profile. IPv6 examples should use documentation address space (2001:db8::/32) and dry-run or offline flows unless a provider-backed live workflow is explicitly selected.

ICMPv4 Messages

Icmpv4 is the fixed ICMPv4 header and the front of an ICMPv4 packet. Data that follows the fixed header is composed as its own typed body or extension layer with /, so the same compile, decode_from_l3, summary, and show surface applies to ICMPv4 as to every other protocol.

Icmp is now a deprecated alias for Icmpv4; existing code that imports Icmp keeps compiling within the 0.x line with a deprecation warning that points at the v4-explicit name. The ICMPv4 body layers below renamed alongside it, each with a deprecated alias under its old name: IcmpQuotedIpv4Icmpv4QuotedIp, IcmpTimestampIcmpv4Timestamp, IcmpAddressMaskIcmpv4AddressMask, IcmpRouterAdvertisementEntryIcmpv4RouterAdvertisementEntry. The version-neutral types (IcmpKind, IcmpLayer, and the IcmpExtension* RFC 4884 family) keep the Icmp prefix because they are shared by both ICMP versions.

Typed constructors track the IANA ICMP Parameters registry:

Message familyConstructors
Echo (RFC 792)Icmpv4::echo_request(), Icmpv4::echo_reply()
Errors (RFC 792)Icmpv4::destination_unreachable(), Icmpv4::time_exceeded(), plus Icmpv4::new().type_(...) for source quench, redirect, and parameter problem
Timestamp / information (RFC 792)Icmpv4::timestamp_request(), Icmpv4::timestamp_reply(), Icmpv4::information_request(), Icmpv4::information_reply()
Address mask (RFC 950)Icmpv4::address_mask_request(), Icmpv4::address_mask_reply()
Router discovery (RFC 1256)Icmpv4::router_advertisement(), Icmpv4::router_solicitation()
Extended echo (RFC 8335)Icmpv4::extended_echo_request(), Icmpv4::extended_echo_reply()
Deprecated / experimentalBy-name constructors such as Icmpv4::traceroute(), Icmpv4::photuris(), Icmpv4::experiment_1()

The bytes after the fixed header are typed body and extension layers:

LayerCarries
Icmpv4QuotedIpThe quoted original datagram in an ICMPv4 error message.
Icmpv4TimestampRFC 792 originate, receive, and transmit timestamps.
Icmpv4AddressMaskThe RFC 950 address mask.
Icmpv4RouterAdvertisementEntryOne RFC 1256 advertised router address and preference.
IcmpExtension / IcmpExtensionObjectRFC 4884 multi-part framing and a generic extension object.
IcmpExtensionMplsRFC 4950 MPLS label stack object body.
IcmpExtensionInterfaceInfoRFC 5837 interface information object body.
IcmpExtensionInterfaceIdRFC 8335 interface identification object body.

Build a port-unreachable error that quotes the offending datagram:

let offending =
    Ipv4::new().src("198.51.100.20")?.dst("192.0.2.10")?
    / Udp::new().sport(40000).dport(53)
    / Raw::from_bytes(b"query");

let packet =
    Ipv4::new().src("192.0.2.10")?.dst("198.51.100.20")?
    / Icmpv4::destination_unreachable().code(ICMP_CODE_DU_PORT_UNREACHABLE)
    / Icmpv4QuotedIp::new(offending);

let bytes = packet.compile()?;

compile fills the fields the caller left unset — the ICMP checksum, the RFC 4884 length and zero padding, extension checksums, router advertisement counts and entry size, and type-specific default codes. Any value set explicitly survives untouched, including intentionally invalid ones. The raw escape hatches keep malformed or not-yet-typed messages reachable:

let icmp = Icmpv4::new()
    .type_(8)               // raw ICMP type
    .code(0)                // raw code
    .checksum(0xdead)       // explicit (here deliberately wrong) checksum
    .rest_of_header([0x11, 0x22, 0x33, 0x44]); // raw rest-of-header bytes

decode_from_l3 types the header and any body it can parse defensibly. Unknown types, codes, object classes, and trailing bytes stay inspectable through raw values and Raw payloads; genuine header truncation returns a structured buffer error instead of panicking.

let decoded = Packet::decode_from_l3(NetworkLayer::Ipv4, bytes.as_bytes())?;
let icmp = decoded.layer::<Icmpv4>().ok_or("missing Icmpv4")?;
let quote = decoded.layer::<Icmpv4QuotedIp>();

summary names the known type and code while keeping the raw numbers visible, for example Icmp(type=destination-unreachable(3), code=port-unreachable(3), id=-, seq=-). Unassigned types print numerically, such as Icmp(type=200, code=7, ...). show lists every typed field — checksum, rest-of-header, identifier, sequence number, RFC 4884 length, router fields, and extended-echo flags — for field-level inspection workflows.

ICMPv6 Messages

Icmpv6 is the fixed ICMPv6 header and the front of an ICMPv6 packet. It carries the IANA Type codepoint space and uses the same shape as Icmpv4: the bytes after the fixed header are typed body layers composed with /, the checksum is auto-filled over the IPv6 pseudo-header at compile(), and summary, show, and decode_from_l3 apply uniformly.

Typed constructors track the ICMPv6 Parameters registry:

Message familyType(s)Constructors
Echo (RFC 4443)128/129Icmpv6::echo_request(), Icmpv6::echo_reply()
Errors (RFC 4443)1–4Icmpv6::destination_unreachable(), Icmpv6::packet_too_big(), Icmpv6::time_exceeded(), plus Icmpv6::new().icmp_type(...) for parameter problem
Multicast Listener Discovery v1 (RFC 2710)130–132Icmpv6::mld_query(), Icmpv6::mld_general_query(), Icmpv6::mld_report(), Icmpv6::mld_done()
Multicast Listener Discovery v2 (RFC 3810)130, 143Icmpv6::mldv2_report(), Icmpv6::mldv2_query(), Icmpv6::mldv2_general_query()
Neighbor Discovery (RFC 4861)133–137Icmpv6::router_solicitation(), Icmpv6::router_advertisement(), Icmpv6::neighbor_solicitation(), Icmpv6::neighbor_advertisement(), Icmpv6::redirect()
Node Information (RFC 4620, experimental)139/140Icmpv6::node_information_query(), Icmpv6::node_information_response()
Extended Echo (RFC 8335)160/161Icmpv6::extended_echo_request(), Icmpv6::extended_echo_reply()

The Neighbor Discovery messages carry an ordered list of NDP options as a typed TLV layer. Build options through NdpOption constructors and collect them in NdpOptions; compile() auto-fills each option's length field (in 8-octet units, with padding), and unknown option types round-trip byte-for-byte.

NDP optionTypeReferenceConstructor
Source Link-Layer Address1RFC 4861NdpOption::source_link_layer_address(...)
Target Link-Layer Address2RFC 4861NdpOption::target_link_layer_address(...)
Prefix Information3RFC 4861NdpOption::prefix_information(...)
Redirected Header4RFC 4861NdpOption::redirected_header(...)
MTU5RFC 4861NdpOption::mtu(...)
Nonce14RFC 3971NdpOption::nonce(...)
Route Information24RFC 4191NdpOption::route_information(...)
RDNSS25RFC 8106NdpOption::rdnss(...)
RA Flags Extension26RFC 5175NdpOption::ra_flags_extension(...)
DNSSL31RFC 8106NdpOption::dnssl(...)
Captive Portal37RFC 8910NdpOption::captive_portal(...)
PREF6438RFC 8781NdpOption::pref64(...)

Router Advertisement also exposes the RFC 4191 Default Router Preference (Prf) through Icmpv6::router_advertisement_with_preference(...). Icmpv6::body() returns an Icmpv6Body view that classifies the decoded message (echo, error, the five NDP types, MLD, extended echo, node information) from the header type; unknown types are preserved as Icmpv6Body::Unknown with a trailing Raw body.

The codepoint coverage, the experimental status of Node Information, and the deferred families (Router Renumbering, Inverse Neighbor Discovery) are detailed in the ICMPv6 guide.

Example Map

ExampleAPI surface exercised
hello_worldIPv4, ICMP, Raw construction, compile, summary, and hexdump output.
packet_buildingBuilder-style packet composition, typed field setting, and deterministic compile output.
packet_inspectionTyped layer access, mutation before compile, packet summaries, and detailed inspection output.
decode_bytesDecode from link, network, and IPv4 entry points while preserving raw payloads.
custom_registryProtocol registry customization and default decode comparison.
send_planNetwork-layer send planning, compiled bytes, targets, and derived reply filters.
send_packetNetwork-layer and link-layer send reports using dry-run options by default; use PacketSender for repeated live sends through one explicit send class.
send_recv_icmpICMP send/receive configuration, retry timing, filters, and dry-run reports.
network_pingNetwork-layer ICMP echo send/receive for disposable endpoint smoke tests.
reply_matchingSynthetic request/reply matching and generated reply filters.
batch_sendPositional batch send reports for multiple TCP packets.
batch_send_recvBatch send/receive reports across IPv4 and IPv6 requests.
interface_helpersDocumentation-safe interface metadata and address helper output.
ip_rangesIPv4 CIDR, range, and list parsing.
pcap_writeGenerated Ethernet/IPv4/TCP packets written to a pcap file.
pcap_readPcap metadata inspection, packet collection, and bounded PacketWire source workflows.
sniffer_offlineOffline PacketWire pcap input, PacketRecord metadata, and bounded Sniffer iteration.
capture_pcapBounded PacketWire pcap interface capture and pcap writing after isolated-lab opt-in.
arp_who_hasExplicit Ethernet broadcast ARP who-has construction from known MAC and IPv4 values.
dns_queryDNS query construction, dry-run send/receive reporting, and synthetic response decoding.
dhcpv4_discoverDHCPv4 discover construction with an explicit client MAC and link-layer send options.
dhcpv4_option82Offline DHCPv4 relay agent information (option 82), classless static routes, and option overload construction and decode.
dhcpv4_leasequeryOffline DHCPv4 leasequery, typed client identifier, authentication, and status/state packet-field construction and decode.
dhcpv6_solicitDHCPv6 Solicit construction, option request data, derived reply filter, and network-layer dry-run send/receive planning.
dhcpv6_information_requestDHCPv6 Information-request construction, dry-run send/receive reporting, decode, ORO inspection, and hexdump output.
dhcpv6_prefix_delegationOffline DHCPv6 IA_PD and IA Prefix construction, compile, decode, and typed prefix inspection.
dhcpv6_relayOffline DHCPv6 Relay-forward construction with Interface-Id and nested Relay Message decoding.
icmpv4_errorICMPv4 time-exceeded error with a quoted datagram and an RFC 4884/4950 MPLS extension object, compiled and decoded offline.
icmpv6_echoIPv6 ICMPv6 echo construction and optional dry-run send/receive reporting.
vlan802.1Q VLAN frame construction, compile, and decode.
linux_sllLinux cooked capture packet construction, compile, and decode.
null_loopbackBSD null/loopback link-layer packet construction, compile, and decode.
ipv4_enrichmentIPv4 DSCP/ECN helpers, typed options, checksum status, and fragment metadata inspection.
ipv4_optionsIPv4 option builders with checksum and length auto-fill.
tcp_optionsTCP option builders, option ordering, and header-length auto-fill.
ipv6_extensionsIPv6 routing, segment-routing, and fragment extension header decoding.

The example set is limited to construction, decoding, pcap, and bounded validation flows.

Agent-Oriented Style

Agent-generated tools should prefer:

  • Packet::new().push(...).push(...) over deeply nested expressions.
  • Explicit SendRecv configuration over positional timeout/retry arguments.
  • ? propagation with concrete error types or Box<dyn std::error::Error>.
  • packet.summary() for compact output and packet.show() for detailed output.
  • Offline compile, decode, and pcap flows before live send flows.
  • Named protocol constructors such as Icmpv4::echo_request() and Dns::query_a(name).

Concise human examples may use /, but generated examples should keep the builder form nearby in documentation.