Skip to content
hotschmoePublic

About

Pure Zig TCP/IP stack for freestanding targets, inspired by smoltcp

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

zmoltcp

A pure Zig dual-stack (IPv4/IPv6) TCP/IP stack for freestanding targets, architecturally inspired by smoltcp (Rust no_std).

What This Is

zmoltcp is a standalone, zero-allocation network stack designed for bare-metal and custom OS use. It implements full dual-stack IPv4/IPv6 networking with comptime-parameterized generics -- zero runtime branching on address type, dead code elimination for single-stack builds, and type safety that prevents accidentally passing a v6 address to a v4 socket.

Supported protocols: TCP (full state machine, SACK, timestamps, Reno/Cubic), UDP, ICMPv4/v6, ARP, NDP, IGMP, MLD, SLAAC, DHCPv4, DNS (including mDNS), raw sockets, IPv4/IPv6 fragmentation/reassembly, IEEE 802.15.4, 6LoWPAN, RPL, IPsec ESP/AH wire formats.

Supported mediums: Ethernet (Layer 2), raw IP (Layer 3, TUN/PPP), IEEE 802.15.4 (constrained IoT).

zmoltcp has no kernel dependencies -- it is a pure protocol library that any freestanding Zig project can use.

Development Philosophy

Make it work, make it right, make it fast -- in that order.

This codebase will outlive you -- every shortcut becomes someone else's burden. Patterns you establish will be copied. Corners you cut will be cut again.

Fight entropy -- leave the codebase better than you found it.

Inspiration vs. Recreation -- we take inspiration from well-established patterns (smoltcp, RFCs) and make them our own in idiomatic Zig. We do not reinvent the wheel for the sake of it, but we also do not shy away from unconventional approaches when they serve the design better.

Design Principles

  • Comptime dual-stack: Each socket, endpoint, and routing type is parameterized over an Ip type (ipv4 or ipv6). The compiler generates fully specialized code per IP version. No enum tag checks in the hot path.

  • Explicit poll model: No background timers or callbacks. Call poll() with a timestamp, it tells you what to do. Your event loop owns the scheduling.

  • Zero allocation: All buffers are caller-provided slices. No allocator interface, no hidden heap usage. Packet buffers are []u8 pointing into your DMA pool or wherever you want.

  • Stateless layers: Wire parsing is pure: bytes in, structured data out. Socket state machines are explicit tagged unions -- the compiler enforces exhaustive handling of every TCP state.

  • Clean layer separation: wire/ handles parsing and serialization. socket/ handles protocol state machines. iface routes packets. Each layer is independently testable.

Architecture

src/
  wire/                  Protocol wire formats (parse + serialize)
    checksum.zig         IP/TCP/UDP internet checksum (RFC 1071)
    ethernet.zig         Ethernet II frame
    ip.zig               Generic Ip comptime contract, Cidr(Ip), Endpoint(Ip)
    arp.zig              ARP request/reply (Ethernet+IPv4)
    ipv4.zig             IPv4 header, Address, Protocol enum
    ipv6.zig             IPv6 header, Address, Protocol enum
    ipv6option.zig       IPv6 extension header TLV options
    ipv6ext_header.zig   Generic extension header base (next_header + length)
    ipv6fragment.zig     Fragment extension header (RFC 8200)
    ipv6routing.zig      Routing extension header (Type 2, RPL)
    ipv6hbh.zig          Hop-by-Hop options header
    tcp.zig              TCP header + options (MSS, window scale, SACK, timestamps)
    udp.zig              UDP datagram
    icmp.zig             ICMPv4 (echo, unreachable, time exceeded)
    icmpv6.zig           ICMPv6 (echo, NDP, MLD, RPL dispatch)
    ndisc.zig            NDP (RFC 4861): RS/RA/NS/NA/Redirect
    ndiscoption.zig      NDP options (SLLA, TLLA, PrefixInfo, MTU)
    mld.zig              MLDv2 (RFC 3810): multicast listener query/report
    igmp.zig             IGMPv1/v2 multicast group management
    dhcp.zig             DHCPv4 packet format
    dns.zig              DNS query/response (RFC 1035)
    ipsec_esp.zig        IPsec ESP header (RFC 4303)
    ipsec_ah.zig         IPsec AH header (RFC 4302)
    ieee802154.zig       IEEE 802.15.4 MAC frame
    sixlowpan.zig        6LoWPAN IPHC/NHC compression (RFC 6282)
    sixlowpan_frag.zig   6LoWPAN fragmentation (FRAG1/FRAGN)
    rpl.zig              RPL wire format (RFC 6550): DIS/DIO/DAO

  socket/                Protocol state machines
    tcp.zig              TCP FSM (RFC 793), congestion control (Reno/Cubic)
    udp.zig              UDP datagram send/recv
    icmp.zig             ICMP/ICMPv6 echo request/response tracking
    raw.zig              Raw IP socket (bind to protocol number)
    dhcp.zig             DHCPv4 client (DISCOVER/OFFER/REQUEST/ACK)
    dns.zig              DNS/mDNS resolver client

  storage/               Data structures
    ring_buffer.zig      Generic ring buffer (TX/RX queues)
    assembler.zig        TCP/IP out-of-order segment reassembly
    packet_buffer.zig    Dual-ring datagram store (UDP/ICMP/DNS)

  iface.zig              Network interface: ARP/NDP cache, ICMP auto-reply,
                         MLD/IGMP multicast, SLAAC, medium dispatch
  stack.zig              Top-level poll loop: protocol demux, socket dispatch,
                         fragmentation, 6LoWPAN, egress serialization
  fragmentation.zig      IPv4/IPv6/6LoWPAN fragmentation and reassembly
  phy.zig                PHY middleware: Tracer, FaultInjector, PcapWriter
  rpl.zig                RPL state machine (RFC 6550/6552/6206)
  time.zig               Timestamp/Duration types (i64 microseconds)
  root.zig               Library entry point (public API)

See the Integration Demos section for the full list of end-to-end example programs under examples/.

Building

# Run all tests (on host -- no VM, no hardware needed)
zig build test

# Cross-compile check for freestanding ARM64
zig build -Dtarget=aarch64-freestanding-none

# Build as static library (native)
zig build

Zig version: 0.17.0

Testing

The host suite covers deterministic conformance tests, integration demos, and regression tests. Fuzzing runs locally (mise run fuzz, mise run fuzz-long); CI does not fuzz, because runner time makes it flaky. Deterministic protocol tests are transliterated from smoltcp's test suite where applicable (see SPEC.md for the conformance testing methodology and tests/CONFORMANCE.md for per-test tracking). The smoltcp source is included as a git submodule under ref/smoltcp/ for reference.

Tests are diagnostic tools, not success criteria. A passing suite does not mean the code is good. A failing test does not mean the code is wrong. Tests are valuable for regression detection, sanity checks, and documenting current behavior -- but they are not a definition of correctness. The real metric is whether the code furthers the project's vision.

# Run the full local validation suite with the pinned Zig toolchain
mise run check

# Run all unit tests
mise run test

# Run all demos
mise run demo

# Run a bounded native fuzz pass
mise run fuzz

# Run longer local fuzz campaigns with preserved logs/caches
mise run fuzz-long

Laws and the assurance report

Verification uses zirakzigil (zv), pinned in build.zig.zon.

  • zig build test runs the executable laws in src/laws.zig with fixed seeds: parser totality and parse/emit round-trips for every wire format (plus structure-aware variants that get past checksum and version guards), framing and single-bit-integrity shapes, SeqNumber and storage model laws, a stateful TCP socket law, neighbor-cache and send-limit laws, and pinned counterexamples. A failing law prints its arguments and a zv.laws.replay(...) call; a law that panics is re-run with .trace = true. Set report_reach = true in src/laws.zig to print how often each parser reaches a value versus each error.

  • zig build assurance is the Phase 0 gate, and a plain zig build runs it. Every module carries pub const assurance and gates itself with zv.requireAssurance(@This(), .tested), so a new public function without a classification fails the build; and .tested is measured, not declared: the build runs a copy of src/ with a probe in every function and every unit test removed, runs the laws in src/laws.zig and the examples/ scenarios against it, and fails naming any classified function that neither executed. A unit test cannot raise a function's level; a law or scenario can. The table shows, per module, how many functions each suite reached. -Dghost=true compiles zv ghost code.

  • zig build verify proves the functions the manifests mark .verified (zvc): SeqNumber add, sub and diff (the inverse relations of modular arithmetic), the RFC 1071 calculate (no panic, terminates, the 32-bit accumulator does not wrap for any packet up to 131070 bytes) and finish (the folded sum differs from the input by a whole number of 0xFFFF and is zero only for zero), internetChecksum, and windowShiftFor (the window scale never exceeds 14). The proofs are of the AIR packs checked in under verify/; verify/PINS pins the sources they came from, so editing a verified file without exporting its pack again fails the gate. It needs Z3 (-Dz3=PATH, ZV_Z3 or PATH; scripts/install_z3.sh DEST installs the pinned release).

Interop with smoltcp

zig build interop runs zmoltcp against a real smoltcp (a Rust peer in tests/interop/peer) over a link the harness controls: both stacks share one virtual clock and exchange Ethernet frames through tests/interop/harness.zig, which can drop, duplicate, reorder, delay and corrupt them from a seeded schedule. Nothing touches a real network, a TAP device or the wall clock, so a run is a pure function of its seeds. It needs cargo (stable Rust) on PATH, builds the peer twice (release, and release with debug assertions and overflow checks), and is deliberately not part of zig build test or zig build matrix.

  • tests/interop/scenarios.zig: handshake and echo, 1 MiB bulk, bulk over a lossy reordering link, window scaling, zero window and persist, simultaneous close, half-close, abort, SACK and timestamp negotiation, MSS/MTU edges; both directions, IPv4 and IPv6. transferIsExact is a zv law over link schedules, with pinned entries for failures.
  • zig build interop-upstream sends smoltcp (08bacc9 and upstream main, each in release, release with assertions, and dev builds) the raw segment sequences behind four zmoltcp TCP fixes and checks what it does. -Dinterop-filter=<text> narrows the scenario run to matching test names.

Integration Demos

Fifteen end-to-end demos exercise the full stack API (sockets -> Stack poll loop -> Device) with no manual packet construction. They serve as both functional validation and usage documentation.

# Run all demos
zig build demo

# Run with verbose output
zig build demo -- --summary all
Demo File What it proves
TCP loopback echo examples/loopback_echo.zig Full TCP lifecycle on a single stack: ARP, handshake, data echo, close
TCP back-to-back examples/back_to_back.zig Two stacks communicate over a simulated wire: ARP discovery, 1KB transfer
UDP + ICMP examples/udp_icmp.zig UDP datagram exchange and ICMP ping with auto-reply
IPv6 echo examples/ipv6_echo.zig TCP6 echo + ICMPv6 ping between two stacks with NDP neighbor discovery
Fault tolerant examples/fault_tolerant.zig TCP data transfer over 10% packet-loss link (FaultInjector retransmission)
IP medium examples/ip_medium.zig UDP echo over Medium::Ip -- no Ethernet, no ARP, raw IP point-to-point
Fragmentation examples/fragmentation.zig 600B UDP over 576B MTU: IPv4 fragmentation on egress, reassembly on ingress
Multi-socket examples/multi_socket.zig TCP + UDP + ICMP active simultaneously, proving protocol demux under load
TCP forwarder gateway examples/tcp_forwarder_gateway.zig Gateway accepts a non-local IPv4 TCP SYN into a caller-owned socket and exchanges data
Raw socket examples/raw_socket.zig Raw IP socket on protocol 253: bidirectional payload exchange
Dual-stack examples/dual_stack.zig IPv4 TCP + IPv6 UDP running concurrently on same stacks
DNS resolve examples/dns_resolve.zig DNS A-record resolution via DNS socket + mock UDP:53 server
PHY middleware examples/phy_middleware.zig PcapWriter(Tracer(LoopbackDevice)) composition with callback validation
DHCP client examples/dhcp_client.zig Full DHCP lifecycle: DISCOVER -> OFFER -> REQUEST -> ACK -> configured
6LoWPAN examples/sixlowpan.zig UDP over 6LoWPAN/802.15.4 with IPHC compression and EUI-64 addressing

Using in Your Project

Add zmoltcp as a Zig dependency in your build.zig.zon:

.dependencies = .{
    .zmoltcp = .{
        .url = "https://github.com/hotschmoe/zmoltcp/archive/refs/tags/v<version>.tar.gz",
        .hash = "...",
    },
},

Then in your build.zig:

const zmoltcp_dep = b.dependency("zmoltcp", .{
    .target = target,
    .optimize = optimize,
});
exe.root_module.addImport("zmoltcp", zmoltcp_dep.module("zmoltcp"));

Binary Size

zmoltcp's comptime generics produce smaller binaries than smoltcp's runtime dispatch as feature count grows. Measured on aarch64-freestanding-none with size-optimized settings (Zig -OReleaseSmall, Rust opt-level=z + LTO):

Scenario            zmoltcp   smoltcp   Ratio    .text bytes
A: TCP/IPv4          14,405    11,332    1.27x   (zmoltcp slightly larger)
B: TCP+UDP+ICMP      16,917    15,243    1.11x   (near parity)
C: Full IPv4         20,317    24,872    0.82x   (zmoltcp 18% smaller)
D: Dual-stack        27,721    43,380    0.64x   (zmoltcp 36% smaller)
E: Wire-only          1,145     5,276    0.22x   (zmoltcp 78% smaller)
TOTAL                80,505   100,103    0.80x   (zmoltcp 20% smaller overall)

Comptime generics break even at ~3 socket types and then progressively win. Dual-stack is where the advantage is most pronounced: zmoltcp generates fully specialized code per IP version, while smoltcp dispatches through IpRepr enums at runtime.

Run bash bench/size/measure.sh to reproduce. Requires zig, cargo, rustup target add aarch64-unknown-none, and llvm-size on PATH.

Status

Feature-complete dual-stack IPv4/IPv6 TCP/IP stack with full smoltcp feature parity. See docs/20260224_plan.md for the development plan and phase history.

License

MIT

About

Pure Zig TCP/IP stack for freestanding targets, inspired by smoltcp

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages