Defunctorising VPNKit [vpnkit-upgrade]
- February 28, 2025
VPNKit is a core part of the Docker for Mac/Windows stack. It is tasked with translating network activity from the host to the Linux container.
This short post discusses some of the recent changes to MirageOS and how they impact VPNKit.
Dune Virtual Libraries [dune-virt-libs]
Dune, for quite some time, has supported virtual libraries. This allows library authors to define a signature without an implementation (similar to OCaml's module type S = struct ... end). The package is something other dune libraries can build against, still without choosing a particular implementation of the interface. An application author can make the decision about which implementation they would like to build into their program.
This feature has been quite widely used in OCaml. For example, the excellent digestif library of hashing algorithms provides both C and OCaml implementations individually packaged into digestif.c and digestif.ocaml respectively. Consider developing a browser application that needs some hashing algorithm (e.g. Irmin in the browser). We can explicitly use the OCaml backend for digestif which js_of_ocaml will happily compile for us.
There are two distinct limitations to this approach.
Applications can only link exactly one implementation.
Implementations cannot expose additional functionality easily (see dune#5597).
Functors in MirageOS
- February 28, 2025
MirageOS is a framework for building unikernels in OCaml. It relies heavily on OCaml's functors to abstract implementation from interface. The mirage tool will then select appropriate instantiations of these functors depending on the backend that is targeted (e.g. unix, hvt etc.)
Recently, there has been a push to use dune virtual libraries instead.
VPNKit's Fake Clock & Time
- February 28, 2025
We need to be able to overcome the limitation that implementations cannot expose additional functionality very easily using dune virtual libraries. As it stands, we need to make Mirage's virtual libraries unwrapped (i.e. set (wrapped false)). This allows implementers to expose additional modules alongside the implementation of the virtual library.
For VPNkit, we need this to port some of the DNS forwarding tests which use a fake clock and sleep module. Previously, they could be provided directly as functor arguments to the various DNS modules e.g.
let module R =
Dns_forward.Resolver.Make(Proto_client)(Fake.Time)(Fake.Clock)
In this new world of dune virtual libraries, our Resolver is significantly more simple.
let module R = Dns_forward.Resolver.Make(Proto_client)
The flexibility of time and clock implementation has been pushed to a build-time decision in a dune file. This means that in order to provide the same fake clock and time we will need to create a private dune library that (a) implements the respective signatures of the virtual libraries and (b) provides the extended interface to allow the test suite to modify the internals of the implementations.
The first thing I had to do was make the mirage-mtime and mirage-sleep virtual libraries unwrapped. This allowed me to make a new private dune library called fake_time with two public modules: the normal interface required by mirage-mtime and another module called Fake_time_state containing:
let timeofday = ref 0L
let c = Lwt_condition.create ()
let advance nsecs =
timeofday := Int64.add !timeofday nsecs;
Lwt_condition.broadcast c ()
let reset () =
timeofday := 0L;
Lwt_condition.broadcast c ()
I did the same for fake_sleep which depended on the state from the fake_time library (something a normal library wouldn't).
Once I had the implementations for the virtual libraries in place, all that remained was to tell the executable (in this case the test suite) to select these new implementations instead of the defaults. Whereas before, all the tests could live in the same test suite, now they had to be split into those that needed the fake implementation and those that did not. This is another slight limitation of this virtual library approach. You can only link one implementation at a time to an executable (or test). Thus, the dune rules for running the dns_forward test suite became:
; Uses the default implementations provided by virtual libraries
(executable
(name test)
(libraries dns_forward alcotest))
; Override the defaults and select the private fake libraries
(executable
(name test_fake)
(libraries dns_forward alcotest fake_sleep fake_time))
With all of this machinery in place, I could now update test_fake to use the new modules that would provide the custom functionality for the clock and time implementations.
(* The bad server should be marked offline and no-one will wait for it *)
Fake_time_state.reset ();
(* avoid the timeouts winning the race with the actual result *)
Fake_time_state.advance Duration.(of_ms 500);
Conclusion
- February 28, 2025
The switch to dune virtual libraries seems to be a good one. A majority of applications only require a single clock or time implementation. However, it has become harder to inject custom implementations of various resources into your MirageOS-based applications.