Pasteur is an unikernel which provides a mini-website to paste snippets and save them onto a block device. The goal of this project is to be a full example of: how to write a website as a Solo5 unikernel today?
This version is built directly on the robur.coop effect-based stack
(no MirageOS tooling anymore - a plain dune build is enough):
- the miou scheduler and the mkernel runtime for Solo5
- the mnet TCP/IP stack (with utcp)
- the vif web framework (
vifu) and mhttp/h1 for HTTP - a FAT32 filesystem on a block device with mfat
- client-side encryption with
js_of_ocamland brr (the Web Crypto API)
The project needs OCaml >= 5.3.0. A live instance is available here:
https://paste.osau.re/
Pasteur is a single Solo5 hvt unikernel that declares two devices (see
manifest.json):
- a network interface named
service(NET_BASIC) - a block device named
pasteur(BLOCK_BASIC), formatted as FAT32, where the snippets are stored
The unikernel serves plain HTTP (default port 80). It does not
terminate TLS itself. The snippets are encrypted/decrypted in the browser
through the Web Crypto API (crypto.subtle). We must point out that the Web
Crypto API is only available in a secure context (localhost or via a TLS
connection). However, Pasteur does not provide a server with TLS. Users can
add the unikernel’s IP address as a trusted source in their web browser or (and
this is likely what will be required in production) deploy the unikernel behind
a TLS-terminating reverse proxy (such as contruno, nginx with
certbot or Caddy).
The whole build is driven by dune. The dune-workspace declares a solo5
cross-compilation context, so a plain build produces the hvt binary:
$ git clone https://github.com/dinosaure/pasteur
$ cd pasteur
# fetch the (pinned) sources of the robur-coop stack into ./vendors
$ ./source.sh
$ dune build --profile=releaseThe convenience Makefile wraps this and gives you a ready-to-deploy artifact
named pasteur.hvt:
$ make # produces ./pasteur.hvt (stripped)
$ file pasteur.hvt
pasteur.hvt: ELF 64-bit LSB executable, x86-64, ... interpreter /nonexistent/solo5/ ...Alternatively, you can let OPAM build & install it for you (it runs make all
under the hood and installs pasteur.hvt into $(opam var bin)):
$ opam install .
$ file $(opam var bin)/pasteur.hvtThe snippets are stored on the pasteur block device, which must be a FAT32
image. The simplest way is to create an empty file and format it with the
standard mkfs.fat tool (from dosfstools, available everywhere):
# a 50 MiB image
$ truncate -s 50M disk.img # or: dd if=/dev/zero of=disk.img bs=1M count=50
$ mkfs.fat -F 32 disk.imgIf you prefer to stay within the OCaml toolchain, mfat can format it too
(-s is the number of 512-byte sectors, so 100000 ≈ 50 MiB):
$ opam install mfat
$ mfat make disk.img -s 100000
Formatted disk.img: 100000 sectors (51200000 bytes), FAT32disk.img is the file you will hand over to the unikernel as its pasteur
block device in every deployment method below. Its content (the saved pastes)
persists across reboots.
The unikernel binary takes its configuration on the command line:
| Argument | Default | Description |
|---|---|---|
--ipv4=<CIDR> |
(required) | IPv4 address of the unikernel, e.g. 10.0.0.2/24. |
--ipv4-gateway=<IPv4> |
(none) | IPv4 gateway, e.g. 10.0.0.1. |
--ipv6=<IPv6|eui64|random> |
eui64 |
IPv6 configuration. |
-p, --port=<PORT> |
80 |
HTTP port the unikernel listens on. |
-m, --max=<SIZE> |
65535 |
Maximum snippet size (accepts 64KiB, 1MiB, ...). |
--length=<LENGTH> |
4 |
Length of the generated snippet IDs. |
--title=<STRING> |
Pasteur |
Title shown on the front page. |
--subtitle=<STRING> |
past-isserie |
Subtitle shown on the front page. |
--auth=<USER:PASSWORD> |
(none) | Protect the whole service with HTTP basic auth. |
--color=<always|never|auto> |
auto |
Colorise the logs. |
-l <REGEXP>, -v |
- | Log filtering / verbosity (see --help). |
The two device names, service (net) and pasteur (block), are fixed by
manifest.json and must match whatever you wire up on the host side.
All resources have their limits, and the hard drive (formatted as FAT32) is no exception. Pasteur maintains an LRU cache of the files available on the block device and treats the storage capacity of that block device as a limit. If this limit is reached, the oldest files are deleted to make room for new ones.
The oldest files are initially determined by their creation dates, and subsequently by their last access dates. If a snippet has not been read for a long time, it is highly likely that it will be deleted if the specified limit is reached.
It should be noted that, as the format is FAT32, it may be necessary, due to
fragmentation, to delete several files in order to add just only new one. Users
may be advised to defragment (using mfat defrag) the block device if they
have the opportunity to restart the unikernel.
Three ways are described below, from the most "production-ready" to the most "hands-on":
- with albatross - the recommended way to run Solo5 unikernels;
- with aussi - an OCI runtime, to drive unikernels from Docker;
- directly with
solo5-hvt(andtmux) - handy for local testing.
The unikernel binary is the same in all cases; what differs is how the host
provides its service network interface and pasteur block device.
albatross and the bare solo5-hvt tender attach the unikernel to a host
bridge; you therefore need that bridge, plus IPv4 forwarding + NAT so the
guest can reach (and be reached from) the network. (The Docker method below
does not need this — aussi/Docker set the networking up for you.)
. [ pasteur ]
[ eth0 ] <- NAT -> [ service ] -|
` [ ... ]
$ sudo sysctl -w net.ipv4.ip_forward=1
$ sudo ip link add name service type bridge
$ sudo ip addr add 10.0.0.1/24 dev service
$ sudo ip link set service up
# NAT outgoing traffic and allow forwarding (replace eth0 with your uplink)
$ sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
$ sudo iptables -A FORWARD -i service -o eth0 -j ACCEPT
$ sudo iptables -A FORWARD -i eth0 -o service -m state --state RELATED,ESTABLISHED -j ACCEPTIf you want the website reachable from the outside on :80, also DNAT the
incoming traffic to the unikernel's private address (here 10.0.0.2):
$ sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT \
--to-destination 10.0.0.2:80Note: remember to terminate TLS in front of the unikernel (the in-browser encryption requires a secure context). A common setup is to publish
:443with a reverse-proxy that forwards cleartext HTTP to10.0.0.2:80.
albatross is a daemon dedicated to deploying Solo5 unikernels. It
takes care of creating the TAP interface and attaching it to your bridge. Follow
the Robur tutorial to install the albatrossd service first.
Once the daemon is running and the service bridge exists, register the block
device from the FAT image we prepared and launch the unikernel:
# import disk.img as an albatross block named "pasteur" (size in MB)
$ albatross-client create-block --data disk.img pasteur 50
# launch the unikernel
$ albatross-client create \
--mem=64 \
--net=service:service \
--block=pasteur:pasteur \
pasteur pasteur.hvt \
--arg='--ipv4=10.0.0.2/24' \
--arg='--ipv4-gateway=10.0.0.1'--net=service:servicemaps the unikernel'sserviceinterface (left) to the hostservicebridge (right).--block=pasteur:pasteurmaps the unikernel'spasteurblock (left) to the albatross block we just created (right).
Inspect and follow it with:
$ albatross-client info
$ albatross-client console pasteuraussi is an OCI-compatible runtime for Solo5 hvt unikernels. Once
registered with Docker, it lets you build and run the unikernel as a regular
container image: aussi allocates the TAP interface and wires up the networking
for you.
$ opam pin add https://github.com/robur-coop/aussiRegister aussi as a Docker runtime:
$ cat >/etc/docker/daemon.json<<EOF
{
"runtimes": {
"solo5": { "path": "$(opam var bin)/aussi" }
}
}
EOF
$ systemctl restart dockerBuild an image that ships the unikernel plus a solo5.json using the docker
network type (aussi wires the container's eth0 to the guest), and bakes the
FAT image in as the pasteur block:
Dockerfile:
FROM ocaml/opam:debian-12-ocaml-5.4 AS builder
USER root
RUN apt-get update && apt-get install -y --no-install-recommends \
pkg-config m4 build-essential libgmp-dev libseccomp-dev \
&& rm -rf /var/lib/apt/lists/*
USER opam
RUN opam update && opam install -y solo5 ocaml-solo5 mfat
RUN git clone https://github.com/dinosaure/pasteur pasteur
WORKDIR /home/opam/pasteur
RUN opam pin -yn .
RUN opam install --deps-only pasteur
RUN opam exec -- make all
RUN opam exec -- mfat make disk.img -s 100000
FROM scratch
COPY --from=builder /home/opam/pasteur/pasteur.hvt /pasteur.hvt
COPY --from=builder /home/opam/pasteur/disk.img /disk.img
COPY solo5.json /solo5.json
ENTRYPOINT ["/pasteur.hvt"]solo5.json:
{
"version": 1,
"type": "solo5.config",
"mem": 64,
"nets": { "service": { "type": "docker", "iface": "eth0" } },
"blocks": { "pasteur": { "path": "/disk.img" } },
"argv": [
"--ipv4=%{solo5.net.service.ip}",
"--ipv4-gateway=%{solo5.net.service.gw}",
"--color=always"
]
}And you can run these commands where above files are:
$ docker build -t pasteur .
$ docker run -d --runtime=solo5 -p 8080:80 pasteur
$ curl http://localhost:8080/For local testing you can skip any orchestrator and run the hvt tender
yourself. Create a TAP interface, attach it to the service bridge, then start
the unikernel. The --net: and --block: flags use the device names from
manifest.json:
$ sudo ip tuntap add tap0 mode tap
$ sudo ip link set tap0 master service
$ sudo ip link set tap0 up
$ sudo solo5-hvt \
--net:service=tap0 \
--block:pasteur=disk.img \
pasteur.hvt \
--ipv4=10.0.0.2/24 \
--ipv4-gateway=10.0.0.1solo5-hvt runs in the foreground, so use tmux to keep it alive after you
disconnect:
$ tmux new -s pasteur # start a session
# ... run the solo5-hvt command above ...
# detach with: Ctrl-b d
$ tmux attach -t pasteur # come back later to see the logsYou can now reach the unikernel at http://10.0.0.2/. To stop it, re-attach to
the tmux session and Ctrl-c the tender (or tmux kill-session -t pasteur).
The CSS and front-page assets are embedded in the binary at build time (see the
mcrunch rules in dune). To change the theme, edit pastisserie.css /
highlight.css and rebuild the unikernel.