Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

mnet is a library that provides the networking foundation for unikernels. It lets you build services ranging from public-facing web servers to more specialized tools such as a DNS resolver (to circumvent censorship) or a DNS blocker (to filter out advertising). In this short book, we will walk through several practical examples that show what unikernels in OCaml can do.

What is a unikernel?

A unikernel is a specialized, single-purpose operating system that bundles your application code with only the OS components it actually needs. Nothing more. Instead of running your OCaml application on top of a general-purpose OS such as Linux (which ships with thousands of features you will never use), a unikernel compiles your code together with only the minimal set of libraries needed for networking, storage, and memory management.

The result is a single bootable image that runs inside a sandboxed environment. There is no shell, no unnecessary drivers, and no multi-user support. It is just your application and the bare minimum required to run it.

In practice, building an OCaml unikernel relies on two key components. Solo5 provides the sandboxed execution environment: it defines a minimal, stable interface between your unikernel and the underlying host (whether that host is a hypervisor such as KVM, or a sandboxed Linux process using seccomp). Solo5 handles the low-level details of how your unikernel boots, accesses network interfaces, and reads from block devices. On top of Solo5, mkernel is a library that lets you write unikernels in OCaml using the Miou scheduler. It exposes the devices that Solo5 provides (network interfaces and block storage) and gives you a familiar OCaml programming model for building your application.

When you compile your OCaml code with mkernel, the build system produces a standalone image that can be launched using a Solo5 tender (a small host-side program such as solo5-hvt). The practical benefits are significant: a smaller attack surface, faster boot times (often measured in milliseconds), a reduced memory footprint, and simpler deployment, since the entire system is a single artifact.

The ecosystem for OCaml unikernels

mnet is part of a broader ecosystem of OCaml libraries that our cooperative maintains for unikernel development. This ecosystem provides pure OCaml reimplementations of essential components (networking, cryptography, and more) so that you can build fully self-contained applications without relying on C bindings or system libraries. Here are some of the libraries we use throughout this tutorial.

  1. At the foundation, mkernel provides the runtime, including hypercalls for network and block devices, clock access, and integration with the Miou scheduler.
  2. For networking, utcp is a pure OCaml implementation of the TCP protocol, used internally by mnet. It originated from a manual extraction of a HOL4 specification of the TCP state machine (described in detail in this paper).
  3. ocaml-solo5 is a variant of the OCaml compiler that targets Solo5, making cross-compilation possible.
  4. On the cryptography side, mirage-crypto provides our cryptographic primitives, and some of its operations are derived from formally verified proofs in Rocq/Coq via the fiat project.

We will encounter more of these libraries as we go.

Prerequisites

Unikernels require a different build process than standard executables. We are actively improving the development workflow for mkernel, but it is still evolving. Everything described in this tutorial is accurate and functional, but you can expect the process to become smoother over time. To get started, you will need:

  • OCaml version 5.0.0 or later,
  • along with OPAM, the OCaml package manager (you can find installation instructions here).
  • You will also need ocaml-solo5, which lets you compile an OCaml project as a unikernel, as well as the Solo5 tools (solo5-hvt or solo5-spt) for running unikernels.
  • Finally, you will need access to a hypervisor such as KVM, BHyve, or VMM.

You can install everything you need using these commands:

$ opam switch create 5.4.0
$ eval $(opam env)
$ opam install solo5
$ opam install ocaml-solo5
$ opam install mkernel
$ opam install mnet

To run a unikernel, you need access to a hypervisor or a sandboxing mechanism. On Linux, the simplest option is KVM (Kernel-based Virtual Machine). You can check whether your system supports it by running:

$ ls /dev/kvm

If this device exists, you are ready to go. You may need to add your user to the kvm group so that you can access it without root privileges:

$ sudo usermod -aG kvm $USER

After running this command, log out and log back in for the change to take effect. Once KVM is available, you can run your unikernel with the solo5-hvt tender, which uses KVM to execute your image in an isolated virtual environment.

Your first unikernel

A unikernel is an executable that must be cross-compiled. This means it is built using the ocaml-solo5 compiler rather than the regular host compiler. Because of this, the build configuration looks slightly different from what you might be used to:

$ cat >dune<<EOF
(executable
 (name main)
 (modules main)
 (link_flags :standard -cclib "-z solo5-abi=hvt")
 (libraries mkernel)
 (foreign_stubs
  (language c)
  (names manifest)))

(rule
 (targets manifest.c)
 (deps manifest.json)
 (enabled_if
  (= %{context_name} "solo5"))
 (action
  (run solo5-elftool gen-manifest manifest.json manifest.c)))

(rule
 (targets manifest.c)
 (enabled_if
  (= %{context_name} "default"))
 (action
  (write-file manifest.c "")))

(vendored_dirs vendors)
EOF

To boot as quickly as possible, a unikernel does not perform device discovery: it never asks the tender which devices are available. Instead, it contains a static list of the devices it requires. This list is written as a JSON file, which is then compiled into the manifest.c file that becomes part of your unikernel:

$ cat >manifest.json<<EOF
{"type":"solo5.manifest","version":1,"devices":[]}
EOF

To cross-compile your executable with ocaml-solo5, you need to define a new build context in the dune-workspace file:

$ cat >dune-workspace<<EOF
(lang dune 3.0)
(context (default))
(context (default
 (name solo5)
 (host default)
 (toolchain solo5)
 (disable_dynamically_linked_foreign_archives true)))
EOF

Cross-compilation requires that the source code of your dependencies (in this case, mkernel) is available locally. You can fetch it with opam source:

$ mkdir vendors
$ opam source mkernel --dir vendors/mkernel

You can now create your unikernel:

$ cat >dune-project<<EOF
(lang dune 3.0)
EOF
$ cat >main.ml<<EOF
let () = Mkernel.(run []) @@ fun () ->
  print_endline "Hello World!"
EOF
$ dune build ./main.exe

Launching a unikernel is different from launching a regular executable, because it runs as a virtual machine. You need to use a tender to start it. Here, we use solo5-hvt:

$ solo5-hvt -- _build/solo5/main.exe --solo5:quiet
Hello World!

Congratulations, you have just created your first unikernel! In the next chapter, we will build a small echo server using mnet and set up networking for your unikernel. Unikernels come with their own concepts and constraints that are important to understand. The mkernel documentation covers these fundamentals in depth, explaining how Solo5 and OCaml fit together.

Important constraints

There are two essential things to keep in mind when building unikernels.

The first is that the Unix module is not available. The ocaml-solo5 compiler does not provide the unix.cmxa library. Since there is no underlying operating system, system calls like Unix.openfile or Unix.socket simply do not exist. This means that any library, including transitive dependencies, that relies on the Unix module cannot be used in a unikernel. In practice, this is why our ecosystem relies on pure OCaml reimplementations of protocols and services (networking, DNS, TLS, and so on) rather than wrappers around C system libraries.

The second is that dependencies must be vendored. Nearly all of your dependencies need their source code present locally in a vendors/ directory. This is because cross-compilation with ocaml-solo5 requires compiling C stubs (if any) with the Solo5 toolchain, which is only possible when dune has direct access to the source files. You can vendor a dependency with opam source:

$ opam source <package> --dir vendors/<package>

This must be done for every dependency that your unikernel uses, not just your direct dependencies, but their transitive dependencies as well.