ocaml-tcp#
Warning. This is a work-in-progress experimental fork of utcp. Do not use it in production; use utcp instead.
An implementation of the Transmission Control Protocol (RFC 9293) in pure OCaml whose core does no I/O and takes time as a parameter, so the same state machine runs under Eio, in a unikernel, and in deterministic tests.
It ships as two opam/ocamlfind packages -- nox-tcp (the core, module Tcp)
and nox-tcp-eio (the Eio adapter, module Tcp_eio). Those names are an
artefact of the published library namespace; the project is ocaml-tcp.
Installation#
Install with opam:
$ opam install nox-tcp
If opam cannot find the package, it may not yet be released in the public
opam-repository. Add the overlay repository, then install it:
$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install nox-tcp
Architecture#
ocaml-tcp follows the ocaml-protocols four-layer
model: the core does no I/O. It is a pure function from the incoming bytes and
the current connection state to the outgoing bytes and the next state, with the
reading and writing of sockets kept in a separate adapter. It is built bottom
up, each layer a thin wrapper over the one below and usable on its own:
| Layer | Module | Role |
|---|---|---|
| Codec -- wire combinators + primitives | wire + Tcp.Sequence |
binary combinators and RFC 1982 sequence-number arithmetic |
| Message -- option codec | Tcp.Options |
TCP options codec (RFC 2018, 7323) |
| Packet -- segment framing + checksum | Tcp.Segment |
RFC 9293 header codec and the IP pseudo-header checksum |
| Protocol -- connection state machine | Tcp.Connection |
the RFC 9293 FSM; no I/O, time as an explicit parameter |
IO adapter (nox-tcp-eio) |
Tcp_eio |
drives the state machine over a link and presents the stack as an Eio.Net.t |
Dependency is one-way, and keeping it that way is what makes the core
portable: the core (nox-tcp) has no dependency on eio, lwt, or unix. Tcp.Connection
never reads a clock, never touches a socket, and never schedules -- it consumes
a decoded segment and returns the next state, the segments to send, and the
events to surface (RFC 9293 s.3.3.2). The same Connection powers the
production Eio adapter, the fuzz harness, and the deterministic in-memory test
suite; only Tcp_eio knows what a socket is.
The adapter is just another network device. Tcp_eio.Net.eio presents a
running stack as a first-class Eio.Net.t, so any
existing Eio program -- an HTTP server, an iperf3 client or server -- runs over
this TCP stack unmodified: hand it this Eio.Net.t in place of the OS network
and Eio.Net.listen / connect / accept work as usual. (UDP and Unix
sockets are unsupported and raise.) Built over a utun relay link
(Tcp_eio.Net.over_flow) the stack is a real endpoint that stock iperf3 -c <ip> driving the host kernel can reach.
Implemented#
Tcp.Connection covers the RFC 9293 state machine with time as an explicit
parameter:
- the three-way handshake, in-order data transfer, and the FIN/RST teardown;
- out-of-order reassembly with a queue and in-order splicing (RFC 9293 s.3.4);
- selective acknowledgement: SACK-permitted negotiation and SACK blocks on duplicate acks (RFC 2018);
- window scaling negotiation and application (RFC 7323);
- SYN and SYN-ACK retransmission.
Congestion control and timers#
All of the following live in lib/connection.ml:
- slow start and congestion avoidance -- an initial window of four segments,
exponential then linear growth of
cwnd(RFC 5681); - fast retransmit and NewReno fast recovery -- retransmit on the third
duplicate ack, halve
ssthresh, and inflate then deflate the window through partial and full acks until the recovery point is reached (RFC 5681, 6582); - timeout collapse -- a retransmission timeout drops
cwndto one segment and re-enters slow start (RFC 5681); - adaptive retransmission timeout -- a smoothed RTT estimator with variance, Karn's algorithm (no sample from a retransmitted segment), and exponential backoff on each timeout (RFC 6298).
The Eio adapter adds software GSO: a single call into the state machine
(handle, send, or timer) returns a whole burst of segments -- up to a
cwnd's worth -- which the adapter gathers into one vectored write, so the
burst costs a single writev instead of one syscall per segment. See
writev_burst in eio/net.ml.
It is tested with an in-memory client/server through every transition (including reassembly, SACK emission, cwnd growth, fast retransmit, RTT-driven RTO, and window-scale negotiation), fuzzed for crash-safety, and carries CVE-class regression tests (bad data offsets, the SACK-panic class, degenerate options). The lineage is robur-coop/utcp (translated from the Cambridge Netsem HOL4 executable specification) and the socket API of robur-coop/mnet, retargeted from Miou to Eio.
Example#
Encoding a single segment reaches straight into the codec layer -- no connection state required:
let src = Ipaddr.of_string_exn "10.0.0.1"
let dst = Ipaddr.of_string_exn "10.0.0.2"
let syn =
{
Tcp.Segment.src_port = 12345;
dst_port = 80;
seq = Tcp.Sequence.of_int 0x01020304;
ack = Tcp.Sequence.zero;
flags = { Tcp.Segment.no_flags with syn = true };
window = 65535;
urg_ptr = 0;
options = [ Tcp.Options.Mss 1460 ];
}
let wire = Tcp.Segment.to_string ~src ~dst ~payload:Rope.empty syn
Credits#
The wire codec is forked from
mirage/mirage-tcpip (Anil
Madhavapeddy, Mindy Preston, Balraj Singh, and the MirageOS team); the state
machine from robur-coop/utcp and the
socket API from robur-coop/mnet (Hannes
Mehnert and the Robur team), all under the ISC license. The cstruct codec is
replaced by the wire library,
byte streaming by bytesrw, the IPv4
layer by nox-ip, and the checksum by
nox-crc. The original copyright notices are
preserved in LICENSE.md.