A userspace TCP/IP stack for macOS built on top of the kernel's utun virtual
network interface. It implements Ethernet frame parsing, ARP, IPv4, ICMPv4
echo (ping), and a minimal TCP state machine — all without using BSD sockets or
libc networking for the actual protocol logic.
MIT — Copyright © 2026 nithikapidikiti-collab
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- macOS (tested on macOS 13+)
- Xcode Command Line Tools (
xcode-select --install) clang(comes with Xcode CLT)
makeThe utun device requires root:
make run # calls: sudo ./tcp-ip-stackIn a second terminal, configure the host side of the tunnel and test:
# Assign the host end of the tunnel (replace utun3 with whatever was printed)
sudo ifconfig utun3 10.0.0.1 10.0.0.4 up
# Ping our stack's IP
ping 10.0.0.4
# Connect TCP (stack responds to the three-way handshake)
nc 10.0.0.4 8080Hit Ctrl-C to stop the stack.
main.c
└── utun_open() open PF_SYSTEM / SYSPROTO_CONTROL socket
select() loop
└── utun_read() strip 4-byte AF prefix → raw IP packet
ip_dispatch()
├── icmp_handle() echo reply
└── tcp_handle() state machine
└── tcp_tick() retransmission timer
utun_write() prepend 4-byte AF_INET prefix → kernel
Opens a PF_SYSTEM / SOCK_DGRAM / SYSPROTO_CONTROL socket, resolves the
UTUN_CONTROL_NAME kernel control identifier with CTLIOCGINFO, and
connect()s with sc_unit = 0 so the kernel picks the next available
utunN interface.
Every packet read from the fd has a 4-byte address-family prefix (AF_INET in
network byte order) prepended by the kernel; utun_read strips it.
utun_write re-adds it before writing.
Parses the 14-byte Ethernet II header (destination MAC, source MAC, EtherType)
and provides eth_build to construct reply frames. On utun there is no
layer-2 framing, but this module is used by the ARP handler and is ready for a
raw-socket or tap back-end.
Parses ARP packets, identifies requests for our IP (10.0.0.4), and builds an
ARP reply. On utun no ARP frames arrive (the kernel handles neighbour
resolution itself), so the module logs and skips the wire write; a tap or
raw-socket back-end would call utun_write here.
Parses the 20-byte IP header, validates the Internet checksum, and dispatches
to icmp_handle or tcp_handle based on the protocol field. Packets not
destined for 10.0.0.4 are silently dropped.
ip_build / ip_checksum are used by ICMP and TCP to construct reply
packets.
Responds to echo-request (type 8) with an echo-reply (type 0). The original ICMP payload (identifier, sequence, data) is reflected verbatim; only the type and checksum fields change.
Implements a minimal TCP state machine supporting up to 16 simultaneous connections:
| State | Description |
|---|---|
LISTEN |
Implicit — any SYN on any port opens a slot. |
SYN_RECEIVED |
SYN received; SYN-ACK sent. |
ESTABLISHED |
Handshake complete; data flows. |
CLOSE_WAIT |
FIN received; waiting to finish sending. |
LAST_ACK |
Our FIN sent; waiting for peer's ACK. |
CLOSED |
Connection gone; slot reused. |
Handshake: on SYN, the stack allocates a connection, chooses ISN
0x12345678, and sends SYN-ACK. The peer's ACK advances the state to
ESTABLISHED.
Data receive: in-order segments are ACK'd immediately. Out-of-order segments are dropped (no receive buffering).
Teardown: on FIN the stack sends ACK then immediately sends its own FIN
(passive close), transitioning through CLOSE_WAIT → LAST_ACK → CLOSED.
Retransmission: every segment that carries data, SYN, or FIN is saved in a
per-connection buffer. tcp_tick() is called every 100 ms from main; it
fires a retransmit when the deadline passes and applies exponential back-off
(initial 1 s, doubling, capped at 30 s, max 5 attempts).
checksum()— one's-complement Internet checksum over an arbitrary buffer.checksum_tcp()— same, but concatenates a pseudo-header with the TCP segment (used for TCP checksum computation).hex_dump()— prints a labelled hex dump to stdout for debugging.