blog.robur.coop

The Robur cooperative blog.
Back to index

Zostera, our new implementation of WireGuard in OCaml

2026-10-08

Since June, we have received confirmation from NLnet that we are working on a WireGuard implementation in OCaml. And today we are delighted to announce that our implementation is entering its final phase, as we have reached the stage where we are currently using our implementation to communicate with WireGuard nodes. We will therefore be starting an audit of our implementation with Jason’s help. In this article, we will explain what is involved in implementing a VPN in OCaml and how one can take advantage of a typed functional language.

Not the first!

This is not the first time an attempt has been made to implement a cryptographic protocol. In addition, we have developed:

Concepts such as handshakes, ciphers and even timing attacks hold no secrets for us! The advantage is that we have virtually complete mastery of all the software building blocks relating to cryptography (we are, in particular, maintainers of mirage-crypto and digestif). If you’re satisfied with our work (either directly or indirectly), you can sponsor us in our maintenance work (which is becoming increasingly time-consuming with the arrival of security issues generated by AI which need to be evaluated).

In short, beyond simply knowing how to implement a cryptographic protocol, we also know how to maintain it over the long term.

The WireGuard protocol

We won't describe the WireGuard protocol in detail, as its white paper is very well written. The C implementation of WireGuard in the Linux kernel is also very clear once you start to recognise the concepts. There is, however, one interesting feature of the WireGuard protocol. The handshake is limited (if all goes well) to just two packets:

And the handshake takes place between the initiator and the responder! However, the responder has not yet confirmed the handshake on its end, and it is only upon receiving a packet from the initiator that the handshake can be confirmed on the responder's side. In this case, we can have a bit of fun with the types:

type identity
(** Our private key *)

type initiator = private [ `initiator ]
type responder = private [ `responder ]

type 'role role =
  | Initiator : initiator role
  | Responder : responder role

type pending
type confirmed

type ('role, 'state) handshake
type ('role, 'state) session

Here, we define some fairly basic types, but what's interesting is how we manipulate them:

val step0 : identity -> remote -> (initiator, pending) handshake * msg1
(** Let's send the first packet [msg1] to the responder as an initiator. *)

val step1 : identity -> msg1 -> (responder, confirmed) handshake * msg2
(** Let's send the second packet from [msg1] to the initiator. *)

val step2 :
     identity
  -> (initiator, pending) handshake
  -> msg2
  -> (initiator, confirmed) session
(** The initiator can confirms the handshake from the [msg2] received. *)

val session_of_responder :
     (responder, confirmed) handshake
  -> (responder, pending) session
(** Here, we create a **not confirmed** session from the responder's handshake. *)

val confirm :
     (responder, pending) session
  -> string
  -> (responder, confirmed) session
(** When we receive something from the initiator, we can confirm our handshake
    and get a confirmed session. *)

(** Reading and Writing through the WireGuard tunnel! *)

val recv : ('role, confirmed) session -> string -> string
val send : ('role, confirmed) session -> string -> string

The use of types here allows us to closely follow the execution path described by WireGuard and to confirm our handshake only at a specific stage (for the initiator upon receipt of the msg2 packet and for the responder upon receipt of a WireGuard packet). At a higher level, this enables us to validate the path using a slightly more advanced state machine that incorporates the concept of time (and which triggers states to renegotiate sessions, for example).

This is, in particular, what I have already described in relation to spoke and bob at ICFP 2024.

Free from schedulers

As has been mentioned several times, when it comes to implementing protocols and formats, we ensure that we do not rely on any particular scheduler (such as our own, Miou). Above all, this allows us to focus on the protocol itself. However, some protocols, such as WireGuard, require the concept of time.

It is for this reason that all the operations introduced above take a ~now value, which corresponds to a precise value from a monotonically increasing clock.

Our state machine, introduced by the Bruit module, is described as follows:

type t
(** Our state machine. *)

val create : identity -> t

val add : t -> now:int -> address -> public -> action list
(** Here we can add a new WireGuard peer via its public key. *)

val rem : t -> public -> unit
(** We can also delete it. *)

val recv : t -> now:int -> string -> action list
(** We received something from the network, what [action] does this involve? *)

val send : t -> now:int -> public -> string -> action list
(** We want to send something to the network, what [action] should I take? *)

val tick : t -> now:int -> action list
(** Based on the time given now, what action should I take? *)

val deadline : t -> int option
(** Can you tell me how long the state remains unchanged? After that, I will
    call [tick]. *)

What is interesting here is that all interactions with a potential system (such as checking the time) are delegated to the user. In this way, we can use our library with any system (such as unikernels) and scheduler (such as Miou). This approach to developing the protocol also allows us to 'simulate' situations (such as, for example, determining the actions we should take after a 25-second jump).

Time

A brief digression on time also allows me to clarify one point regarding this value. At present - and this has always been the case with Solo5 - we have two sources for obtaining the time:

The WireGuard protocol also distinguishes between the two, requiring that now be monotonic whilst also ensuring there is another source corresponding to the true time, capable of protecting a peer against a DDoS attack.

The usefulness of these two clocks has always been obvious to us, and allowing the user to choose between them remains fundamental; Rust, in this instance, also makes this choice explicit - and it is all the more important when it comes to cryptography.

However, this stance has been the subject of debate with regard to schedulers. Furthermore, Miou has no concept of time, and only miou.unix (for simple applications) or mkernel (for our unikernels) provide a way to sleep. This ensures, in particular, that we can distinguish between Mkernel.sleep and Mkernel.wakeup. And it is precisely this that has put us at opposite ends of the argument with picos, which wanted to place the concept of time (and therefore a single clock) at the very heart of the scheduler.

Tests

We have, of course, started writing unit tests (which can be found in the wireguard-go implementation). The fact that we are independent of schedulers also allows us to simulate scenarios (without relying on a network or even a clock) in order to check how our state machine reacts. You can them here and here

Next steps!

At this stage, we are also very fortunate to be able to work with Jason, the creator of WireGuard, to carry out an audit of our implementation.

We have also already taken the next step by offering (already!) a unikernel that can replace a WireGuard server. Setting it up is fairly straightforward, but the network configuration can be complex. We are currently testing the VPN to assess its memory usage and the throughput achievable when several people are connected.

This will also enable us to begin work on utcp, mnet and Solo5 to achieve better throughput performance, as we have also secured funding for these projects.

In short, you can follow these various projects and get in touch with us if you’re interested in a WireGuard VPN in the form of a unikernel!