User-mode network gateway (SLIRP / vpnkit) in pure OCaml
0

Configure Feed

Select the types of activity you want to include in your feed.

OCaml 90.7%
Shell 3.1%
C 2.6%
Dune 0.5%
HTML 0.1%
Other 3.0%
213 1 0

Clone this repository

https://git.vm.fail/gazagnaire.org/ocaml-slirp https://git.vm.fail/did:plc:rwg675ovlczrhqesuzxhgse3
ssh://git@git.recoil.org:2222/gazagnaire.org/ocaml-slirp ssh://git@git.recoil.org:2222/did:plc:rwg675ovlczrhqesuzxhgse3

For self-hosted knots, clone URLs may differ based on your setup.


README.md

slirp#

A pure, I/O-free user-mode network gateway in OCaml, in the shape of moby/vpnkit and SLIRP.

slirp is the gateway an unprivileged guest -- a virtual machine, a unikernel, a tap device -- talks to for its network. It hands the guest an address (DHCP on IPv4, SLAAC on IPv6), answers its ARP and Neighbor Discovery, resolves its names and forwards its connections out through ordinary host sockets, with no changes to the host routing table and no root.

Installation#

Install with opam:

$ opam install slirp

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 slirp

Design#

The core does no I/O. It is a pure state machine: handle takes the current state and one inbound guest Ethernet frame and returns the next state and the frames to emit back to the guest (and, in later milestones, the host sockets to open). A thin Eio edge supplies the guest frame transport (an Apple Virtualization.framework device, a Solo5 net device, a tap) and the host sockets; the core stays testable in memory with no device present.

It builds on the monorepo's protocol codecs -- nox-ethernet, nox-arp, nox-ip, nox-udp and nox-tcp -- rather than re-implementing them.

Library#

# let gw, _ = Slirp.v (Slirp.Config.v ()) in
  Ipaddr.V4.to_string (Slirp.gateway_ip gw)
- : string = "192.168.65.1"

v returns the gateway together with the frames to emit at once (a gratuitous ARP announcing the gateway). Feed each guest frame to handle and send back whatever it returns.

Status#

  • Gateway ARP responder (answers the guest's who-has for the gateway).
  • DHCP lease (hand the guest its address).
  • TCP NAT (terminate guest TCP, splice to a host socket).
  • UDP NAT and DNS forwarding (datagrams to a host socket; the gateway is the guest's resolver, forwarding to an upstream).
  • ICMP echo NAT via an unprivileged datagram socket (the guest can ping out).
  • Guest transport over a device file descriptor (Slirp_eio.transport_of_fd), the host side of an Apple Virtualization.framework file-handle network device.
  • End-to-end: a real Linux VM boots through the gateway, DHCPs an address from it, and curls a host service out through the TCP NAT, no sudo (the signed @integration tests, macOS / Apple Silicon).
  • Host-service forwarding on the gateway address (tcp_forwards).
  • Guest transports: a device fd (transport_of_fd, vz), a tap/two-way flow (transport_of_flow), and a Solo5 net device (transport_of_netif).
  • IPv6, no DHCPv6 or ARP: the gateway answers the guest's Neighbor Solicitations (RFC 4861) and its Router Advertisements hand the guest an on-link autonomous prefix and a recursive DNS server (RFC 8106 RDNSS), so a guest with no DHCPv6 forms its own address by SLAAC (RFC 4862) and still resolves names; the gateway also re-advertises unprompted so a guest that missed the reply still configures. Outbound TCP, UDP (DNS included) and ICMPv6 echo (ping) NAT out the same way as IPv4, over one flow table keyed on the address family; and inbound (host-to-guest) forwarding resolves the guest by Neighbor Solicitation, the reverse direction over v6.

Booting a VM through the gateway#

Slirp_eio.transport_of_fd drives the gateway over any device file descriptor that carries one Ethernet frame per datagram -- including the host side of an Apple Virtualization.framework file-handle network device. The vz launcher (macOS) wires the two together: it backs a VM's virtio-net with one end of a datagram socketpair and runs the gateway over the other, so the guest boots with ip=dhcp, leases its address from the gateway and reaches the network through ordinary host sockets -- no host routing change and no root.

vz -p 5201:5201 <kernel> [initrd]

-p HOST:GUEST publishes a TCP port into the VM, using the same notation as Docker; repeat it for multiple services. The gateway gives this single-guest runner a fixed 192.168.65.2 DHCP lease so every published port has a stable target.

vz defaults to an IP MTU of 65521, advertised to the guest by DHCP: with the 14-byte Ethernet header this fills, but does not exceed, the transport's 65535-byte frame envelope. This matches the jumbo synthetic link used by Docker Desktop host networking while retaining Ethernet framing. Pass --mtu=1500 when measuring conventional Ethernet instead.

The signed @integration test (test/integration/, macOS / Apple Silicon) proves the path end to end: it boots a real linuxkit arm64 kernel with ip=dhcp, networked only through the gateway, and checks the kernel's in-boot DHCP completes against it -- a DISCOVER and then a REQUEST, the REQUEST only sent once the kernel has accepted the gateway's OFFER. Run it with:

dune build @ocaml-slirp/test/integration/integration

Performance#

Measured, not asserted. Three benches isolate the tiers; run them yourself.

The pure gateway core (bench/bench.ml -- no sockets, no scheduler) segments a bulk TCP download and reports allocation per delivered byte. At the 1500-byte Ethernet MTU it sustains ~560 MiB/s; once the guest raises its MTU (the gateway advertises it via DHCP option 26, so a jumbo guest uses big frames), the same core does ~1960 MiB/s (~15 Gbit/s) at 0.2 allocation bytes per delivered byte.

The device-fd edge (bench/edge_bench.ml -- a real datagram socketpair through Slirp_eio.transport_of_fd, the vz file-handle path) is the end-to-end ceiling:

frame size throughput round-trip
1514 B ~1 Gbit/s ~38 us
8974 B ~7 Gbit/s --
61454 B ~26-37 Gbit/s ~51 us

At the 1500-byte MTU the edge is syscall-bound at ~1 Gbit/s. Big frames attack the syscalls-per-byte cost from the other side (fewer, larger datagrams) and take the edge to 26-37 Gbit/s -- above the core, so with big frames the end-to-end ceiling is the gateway core, not the edge.

For reference, at a comparable 65520-byte MTU: passt measures 19.6 Gbit/s guest-to-host and 19.2 host-to-guest over sockets, and 26.6 / 43.6 with vhost-user (passt's own continuous benchmarks); slirp4netns reaches 9.21. Quote MTU alongside any of these -- the same stacks measure 1.07 ( slirp4netns) and 4.7 (passt) at 1500, so a figure without its MTU says very little. For vpnkit no official throughput figure is published at all; the one third-party measurement is slirp4netns's table -- 514 Mbit/s at MTU 1500, with a jumbo MTU unsupported -- from a 2018 CI run, so treat it as indicative rather than current.

Treat same-machine figures above ~50 Gbit/s with suspicion: several published numbers for macOS VM stacks are loopback transfers with segmentation offload, which exceed any wire rate and are not comparable to a gateway's throughput.

Untrusted input is fuzzed for crash-safety and output well-formedness (fuzz/fuzz_slirp.ml): arbitrary and crafted frames, and whole sequences of them, never crash the handler and never make it emit a malformed frame to the guest.

On features, the gateway matches the state of the art. IPv6 uses the same Neighbor Discovery plus Router Advertisement (SLAAC) autoconfiguration that libslirp and vpnkit do, with TCP, UDP and ICMPv6-echo NAT over v6 outbound and Neighbor-Discovery-resolved host-to-guest forwarding inbound -- the dual-stack feature set those implementations carry, in both directions. On throughput the big-frame path (above) already clears the libslirp/vpnkit range and reaches passt's regime.

Credits#

The design is adapted from moby/vpnkit's hostnet user-mode network stack, re-shaped around an I/O-free core.