By Richard Mortier - 2024-04-19
First, make sure you have followed the installation instructions to get a working MirageOS installation. The examples below are in the mirage-skeleton repository. Begin by cloning and changing directory to it:
$ git clone https://github.com/mirage/mirage-skeleton.git
Cloning into 'mirage-skeleton'...
$ cd mirage-skeleton
The mirage-skeleton
repository classifies its examples into three groups.
This document refers to unikernels in the tutorial
directory.
Note: Before we begin, if you aren't familiar with the Lwt library (and the
>>=
operator it provides), you may want to read at least the start of
the Lwt tutorial first.
Additional note: Throughout the tutorial, we'll use mirage configure -t unix
to demonstrate building MirageOS applications. If you're using a macOS
machine, you should use mirage configure -t macosx
instead.
Before we try and do anything complicated, let's build a unikernel that starts
and then exits, without doing anything else. The code for this is, as you might
hope, fairly short.
Before we can build our noop
unikernel, we must define its configuration
telling Mirage what jobs the unikernel is to run.
In this example we define a unikernel with zero jobs by passing the empty list.
We do this by writing a config.ml
file:
$ cat tutorial/noop/config.ml
let()"noop"
Finally, we declare the entry point to OCaml in the usual way (let () = ...
),
register
ing our unikernel entry points (in this case the empty list) and with a
name of the unikernel("noop"
in this case) we will build.
Building our unikernel is then simply a matter of:
Evaluating its configuration:
$ cd tutorial/noop
$ mirage configure -t unix
This step will generate a number of build configuration files, including a
Makefile
defining targets that we will use in the next step.
Installing dependencies:
$ make depends
The depends
target in the generated Makefile
will run commands to:
NOTE: while performing the lock step, an additional repository
https://github.com/mirage/opam-overlays.git is added in your opam switch.
This repository contains packages that have been changed to use the dune build
system. The --extra-repo
argument in mirage configure
changes the additional
repository to use. --no-extra-repo
can be used to disable the extra repository,
but the lock step might fail because of dependencies that are not using the
dune build system.
Compiling:
$ make build
You can combine steps (2) and (3) by running the default make
target.
Once all the dependencies are installed, you can also just run dune build
to build the unikernel (no need to call the slow make depends
every time!):
$ mirage configure -t unix
$ dune build
Because we set the configuration target to be Unix (the -t unix
argument to
the mirage configure
command), the result is a standard Unix ELF located in
dist/noop
that can be executed:
$ dist/noop
$ echo $?
0
Congratulations! You've just built and run your very first unikernel!
Except for the noop example above, all unikernels have at least one
job. A job is a module with a start
function as entrypoint that
performs some task. Most jobs will depend on some system devices
which they use to interact with the environment. This tutorial will
cover examples timer devices, key-value stores, block devies and
network interfaces. In this section, we illustrate how to define a
unikernel with a job that depends on a timer device.
Mirage unikernels use functors to specify abstract device dependencies that are not dependent on the particular details of an environment. In OCaml, a functor is a module that takes other modules as parameters. Functors are used widely throughout Mirage and we will explain the basic idea and provide examples in this tutorial. For a proper introduction into the core concepts, you may see Real World OCaml, Ch.9 (and also Ch.10, First-Class Modules).
Functors act as functions from modules to modules. They allow us to pass
dependencies into a unikernel, so that the program running inside can interact
with the environment (read files, send packets, etc) without needing to care
whether it will eventually be built to target Unix, Xen, KVM, or something else
entirely. The modules that are passed into the unikernel in this way must
satisfy type signatures that are specified when the unikernel job
value is
created in the config.ml
file.
In this section, we present a simple example of a unikernel that uses a functor
to depend on a device for reading the system's time. We will build and run the
MirageOS "Hello World" unikernel that prints a log message with the word
hello
, sleeps for 1 second, and repeats this 4 times before finally
terminating. The output will look like this:
2017-02-08 09:54:44 -01:00: INF [application] hello
2017-02-08 09:54:45 -01:00: INF [application] hello
2017-02-08 09:54:46 -01:00: INF [application] hello
2017-02-08 09:54:47 -01:00: INF [application] hello
Let's start by looking at the code:
$ cat tutorial/hello/unikernel.ml
open Lwt.Infix
module Hello (:) = struct
let start _time =
let rec loop = function
| 0 -> Lwt.return_unit
| n ->
Logs.info (>"hello");
Time.sleep_ns () >>= fun () -> loop ()
in
loop 4
end
We define a main Hello
module parameterised by a module Time
, of type
Mirage_time.S
. The Time
module provides the functionality enabling us to
interact with the environment clock.
Our start
function, which is the entrypoint of the job, also takes a parameter
_time
(an instance of a time
which is ignored). This parameterization of a
unikernel's main module and its start
function is the basic structure required
to make a MirageOS unikernel that can be built to run on any supported
environment, rather than a standard OCaml POSIX application.
The module type for our Time
module, Mirage_time.S
, is defined in an
external package mirage-time. The name
pattern Foo.S
for "the signature of Foo
modules" is a common OCaml
convention (comparable to naming the most-used type in a module t
). There are
many packages defining module types for use in Mirage.
The concrete implementation of Time
will be supplied at compile time,
depending on the target that you are compiling for. This calls for some
additional configuration in config.ml
, so let's take a look:
$ cat tutorial/hello/config.ml
open Mirage
let main =
main "Unikernel.Hello" (>) ~packages:[ package "duration" ]
let()"hello"
First we open the Mirage
module to save on typing. Next, we define
a value main
. This name is only a convention, and you should feel
free to change it if you wish. main
calls the Mirage.main
function, passing two parameters. The first is a string declaring the
module name that contains our entry point — in this case, standard
OCaml compilation behaviour means that the unikernel.ml
file
produces a module named Unikernel
. Again, there's nothing special
about this name, and if you want to use something else here, simply
rename unikernel.ml
accordingly. The @->
combinator is used to
add a device driver to the list of functor arguments in the job
definition and the final value of this combinator should always be a
job
. The named argument ~packages
defines extra OCaml package
dependencies that the job depends on. In this case we depend on the
duration
library for converting from seconds to nanoseconds.
Notice that we refer to the module name as a string ("Unikernel.Hello"
) when
calling main
, instead of directly as an OCaml value. The mirage
command-line
tool evaluates this configuration file at build time and outputs a main.ml
that has the concrete values filled in for you depending on which target you
selected during configuration (e.g. Unix or Xen).
MirageOS mirrors the unikernel model on Unix as far as possible: your application is built as a unikernel which needs to be instantiated and run whether on Unix or on a hypervisor backend like Xen or KVM. When your unikernel is run, it starts much like a conventional OS does when run as a virtual machine, and so it must be passed references to devices such as the console, network interfaces and block devices on startup.
In general, a config.ml
file is a normal OCaml module that calls register
to
register one or more jobs, each of which represent a process (with a start/stop
lifecycle). Each job depends on some device drivers; all the
available device drivers are defined in the Mirage
module (see the Mirage
module documentation).
In this case, this simple hello world
example requires some notion of time,
so we register a single job
consisting of the Unikernel.Hello
module
(and, implicitly its start
function) and pass it references to a
timer.
When we call Mirage.main
we specify the devices our Unikernel.Hello
program
depends on (a time
device) and when we call Mirage.register
, we provide
instructions about how to satisfy those dependencies (it will be given a
default_time
device, suitable for the target it's eventually built for).
Let's test all of this by first configuring, building, and running the resulting unikernel under Unix:
$ cd tutorial/hello
$ mirage configure -t unix
mirage configure
generates a Makefile
with all the build rules included from
evaluating the configuration file and a mirage
directory. The mirage
directory includes generated files to run and build your program, including
main.ml
that represents the entry point of your unikernel,hello-unix.opam
file with a list of the packages necessary to build the
unikernel.Install the dependencies with
$ make depends
And build the unikernel with
$ dune build
Our Unix binary is built as dist/hello
. Note that make
simply
calls mirage build
which itself turns into a simple dune build
command. If
you are familiar with dune
it is possible to inspect the build rules for the
unikernel generated in dune.build
.
To run your application, execute the binary — and observe the exciting log messages that our loop is generating:
$ dist/hello
2024-04-14T14:15:22+02:00: [INFO] [application] hello
2024-04-14T14:15:23+02:00: [INFO] [application] hello
2024-04-14T14:15:24+02:00: [INFO] [application] hello
2024-04-14T14:15:25+02:00: [INFO] [application] hello
Note: The following sections of this tutorial use the
Solo5-based hvt
backend as an
example. This backend is supported on Linux, FreeBSD, and OpenBSD systems with
hardware virtualization. Please see the Solo5 documentation for the support
status
of further backends such as spt
(for deployment on Linux using a strict
seccomp sandbox), virtio
(for deployment on e.g. Google Compute Engine) and
muen
(for deployment on the Muen Separation Kernel). On
supported platforms, Solo5 will be installed automatically when make depends
is run.
To build a Solo5-based unikernel that will run on a host system with
hardware virtualization, re-run mirage configure
and ask for the
hvt
target instead of unix
.
$ cd tutorial/hello
$ mirage configure -t hvt
You can then install its dependencies and build it with:
$ make depends
$ dune build
Everything else remains the same! The set of dependencies required to build,
the generated main.ml
, and the generated Makefile
have changed, but since
the source code of your application was parameterised over the Time
module, it
doesn't matter — you do not need to make any changes for your code to run when
linked against the Solo5 console driver instead of Unix.
When you build the hvt
version, you'll see a new artifact in the dist
directory: a file called hello.hvt
. Additionally, a solo5-hvt
binary will
be installed by OPAM on your $PATH
. This binary is a tender, responsible for
loading your unikernel, attaching to host system devices and running it. To try
running hello.hvt
, pass it as an argument to solo5-hvt
:
$ solo5-hvt tutorial/hello/dist/hello.hvt
| ___|
__| _ \\ | _ \\ __ \\
\\__ \\ (||(|)|\\_|\\_()()()()><<()
We get some additional output from the initialization of the unikernel and its successful boot, then we see our expected output, and Solo5's report of the application's successful completion.
It's very common to pass additional runtime information to a program via command-line options or arguments. But a unikernel doesn't have access to a command line, so how can we pass some information to it at runtime?
Mirage provides a nice abstraction for this in the form of
runtime_args
.
We declare that our unikernel will receive a runtime argument hello
,
using the function runtime_arg
. Once we've created our list of
runtime arguments, we pass it to the unikernel by passing it to
Mirage.main
in the runtime_args
parameter.
For an example, let's have a look at hello-key
:
$ cat tutorial/hello-key/config.ml
open Mirage
let runtime_args = [ runtime_arg ~pos:__POS__ "Unikernel.hello" ]
let packages = [ package "duration" ]
let main = main ~runtime_args ~packages "Unikernel.Hello" (>)
let()"hello-key"
(Note: you must always provide ~pos:__POS__
to the runtime_arg
function -
__POS__
is value from the OCaml Standard Library that represents the current location
in the file, which here used for error reporting.)
Below, the function hello
(which uses the cmdliner
package) declares the
type of the runtime argument as string
and sets a default value of "Hello World!"
,
for the case that the argument is not provided at runtime. See the Cmdliner.Arg module
documentation
for more details.
Then, we receive the value corresponding to the runtime argument hello
as parameter hello
on the start
function, as shown below.
$ cat tutorial/hello-key/unikernel.ml
open Lwt.Infix
open Cmdliner
let hello =
let doc = Arg.info ~doc:"How to say hello." [ "hello" ] in
Arg.(&"Hello World!")
module Hello (:) = struct
let start _time hello =
let rec loop = function
| 0 -> Lwt.return_unit
| n ->
Logs.info (>"%s");
Time.sleep_ns () >>= fun () -> loop ()
in
loop 4
end
Let's configure the example for Unix and build it (do not forget to call make depends
if you have not done it yet for this tutorial):
$ cd tutorial/hello-key
$ mirage configure -t unix
$ dune build
When the target is Unix, Mirage will use an implementation for
runtime arguments that looks at the contents of OS.Env.argv
. In
other words, it looks directly at the command line that was used to
invoke the program. If we call hello
with no arguments, the default
value is used:
$ dist/hello-key
2024-04-17T16:13:44+02:00: [INFO] [application] Hello World!
2024-04-17T16:13:45+02:00: [INFO] [application] Hello World!
2024-04-17T16:13:46+02:00: [INFO] [application] Hello World!
2024-04-17T16:13:47+02:00: [INFO] [application] Hello World!
but we can ask for something else:
$ tutorial/hello-key/dist/hello-key --hello="Bonjour!"
2024-04-17T16:13:48+02:00: [INFO] [application] Bonjour!
2024-04-17T16:13:49+02:00: [INFO] [application] Bonjour!
2024-04-17T16:13:50+02:00: [INFO] [application] Bonjour!
2024-04-17T16:13:51+02:00: [INFO] [application] Bonjour!
When the target is Unix, it's also possible to get useful hints by
calling the generated program with --help
.
When configured for non-Unix backends, other mechanisms are used to
pass the runtime information to the unikernel. solo5-hvt
, which we
used to run hello.hvt
in the non-keyed example, will pass runtime
arguments specified on the command line to the unikernel when invoked:
$ cd tutorial/hello-key
$ mirage configure -t hvt
$ dune build
$ solo5-hvt -- dist/hello-key.hvt --hello="Hola!"
| ___|
__| _ \\ | _ \\ __ \\
\\__ \\ ( | | ( | ) |
____/\\___/ _|\\___/____/
Solo5: Memory map: 512 MB addressable:
Solo5: unused @ (0x0 - 0xfffff)
Solo5: text @ (0x100000 - 0x1e8fff)
Solo5: rodata @ (0x1e9000 - 0x220fff)
Solo5: data @ (0x221000 - 0x2d1fff)
Solo5: heap >= 0x2d2000 < stack < 0x20000000
2018-06-21 12:18:03 -00:00: INF [application] Hola!
2018-06-21 12:18:04 -00:00: INF [application] Hola!
2018-06-21 12:18:05 -00:00: INF [application] Hola!
2018-06-21 12:18:06 -00:00: INF [application] Hola!
Solo5: solo5_exit(0) called
Most useful unikernels will need to obtain data from the outside world, so we'll explain this subsystem next.
The device-usage/block/
directory in mirage-skeleton
contains an example of attaching a raw block
device to your unikernel.
The Mirage_block
interface signature contains the operations that are possible on a block device:
primarily reading and writing aligned buffers to a 64-bit offset within the
device.
On Unix, the development workflow to handle block devices is by mapping them
onto local files.
On solo5 on the other hand, the block devices are mapped to alphanumeric names.
The solo5 tender then at runtime maps the names onto local files. The
config.ml
for the block example contains some logic for handling
this difference. The expression if_impl Key.is_solo5 (block_of_file "storage") (block_of_file "disk.img")
detects if we are on the solo5
target. If so, we emit a block device backed by the name storage
.
Otherwise, we emit a block device backed by the file disk.img
.
Remember, the name has to be alphanumeric on Solo5, so the dot in
disk.img
will not work on Solo5.
open Mirage
let main = main "Unikernel.Main" (block @-> job)
let img =
if_impl Key.is_solo5 (block_of_file "storage") (block_of_file "disk.img")
let () = register "block_test" [ main $ img ]
The main
binding looks much like the earlier hello
example, except for the
addition of a block
device in the list..
As an aside, if you have your editor configured with OCaml mode, you should
be able to see the inferred types for some of the variables in the
configuration file. The @->
and $
combinators are
designed such that any mismatches in the declared device driver types and
the concrete registered implementations will result in a type error at
configuration time.
Build this on Unix in the same way as the previous examples:
$ cd device-usage/block
And configure and build the project (do not forget to use make depends
if you
have not done so already):
$ mirage configure -t unix
$ dune build
Now, with the unikernel built we can run it. However, the unikernel expects a block device. We can create a disk image of all zeroes before we run the unikernel:
$ dd if=/dev/zero of=disk.img count=100000 # only needed once
100000+0 records in
100000+0 records out
51200000 bytes transferred in 0.216251 secs (236761911 bytes/sec)
$ ./dist/block_test
2024-04-17T16:13:53+02:00: [INFO] [block] { Mirage_block.read_write = true;
sector_size = 512;
size_sectors = 100000L }
reading 1 sectors at 100000
reading 12 sectors at 99989
2024-04-17T16:13:53+02:00: [INFO] [block] Test sequence finished
2024-04-17T16:13:53+02:00: [INFO] [block] Total tests started: 10
2024-04-17T16:13:53+02:00: [INFO] [block] Total tests passed: 10
2024-04-17T16:13:53+02:00: [INFO] [block] Total tests failed: 0
block_test
will write a series
of patterns to the block device and read them back to check that they are the
same (the logic for this is in unikernel.ml
within the Block_test
module).
We can build this example for another backend too:
$ mirage configure -t hvt
$ dune build
Now we just need to boot the unikernel with solo5-hvt
as before. We should see
the same output after the VM boot preamble, but now MirageOS is linked against the
Solo5 block device driver and is
mapping the unikernel's block requests directly through to it, rather than
relying on the host OS (the Linux or FreeBSD kernel).
If we tell solo5-hvt
where the disk image for the name storage
is, it will provide that disk image to the unikernel:
$ solo5-hvt --block:storage=disk.img ./dist/block_test.hvt
| ___|
__| _ \\ | _ \\ __ \\
\\__ \\ (||(|)|\\_|\\_()()()()><<{true;;()
The earlier block device example shows how very low-level access can work. Now let's move up to a more familiar abstraction: a key/value store that can retrieve buffers from string keys. This is essential for many common uses such as retrieving configuration data or website HTML and images.
The
device-usage/kv_ro directory
in mirage-skeleton
contains a simple key/value store example. The
subdirectory t/
contains a few files, one of which the unikernel
will compare against a known constant.
The config.ml
might look familiar after the earlier block and console
examples:
open Mirage
let disk = generic_kv_ro "t"
let main = main "Unikernel.Main" (kv_ro @-> job)
let () = register "kv_ro" [ main $ disk ]
We construct the kv_ro
device disk
by using the generic_kv_ro
function. This takes a single directory as its argument, and will do its best
to provide the content of that directory to the unikernel by whatever means
make sense given the target provided at configuration time. The best choice might be an
implementation that calls functions from OCaml's Unix
module (referred to as
direct
by the mirage
tool), or perhaps a function
that transforms an entire directory into a static ML file that can expose
the file contents directly from memory (called crunch
). crunch
removes the need to have
an external block device entirely and is very convenient indeed for small files.
Using generic_kv_ro
in your config.ml
causes Mirage to automatically create a
configuration key, kv_ro
, which you can use to request a specific implementation
of the key-value store's implementation. To see documentation, try:
$ cd device-usage/kv_ro
$ mirage help configure
Under the "UNIKERNEL PARAMETERS" section, you should see:
--kv_ro=KV_RO (absent=crunch)
Use a fat, archive, crunch or direct pass-through implementation
for the unikernel.
More documentation is available at the Mirage
module documentation for generic_kv_ro.
Let's try a few different kinds of key-value implementations. First,
we'll build a Unix version. If we don't specify which kind of kv_ro
we want, we'll get a crunch
implementation, the contents of which we
can see at _build/default/static_t.ml
, the file being generated by
dune
rules described in dune.build
:
$ mirage configure -t unix
$ dune build
Generating Static_t.ml
Generating Static_t.mli
$ ls _build/default/Static_t.ml # the generated filesystem
_build/default/Static_t.ml
And you can check this runs as expected:
$ dist/kv_ro
2024-04-17T16:13:54+02:00: [INFO] [application] Contents of extremely secret vital storage confirmed!
We can use the direct
implementation with the Unix target as well:
$ mirage configure -t unix --kv_ro=direct
$ dune build
$ dist/kv_ro
2024-04-17T16:13:55+02:00: [INFO] [application] Contents of extremely secret vital storage confirmed!
You may have noticed that, unlike with our hello_key
example, the
kv_ro
key can't be specified at runtime — it's only understood as an
argument to mirage configure
. This is because the kv_ro
implementation we choose influences the set of dependencies that are
assembled and baked into the final product. If we choose direct
,
we'll get a different set of software than if we choose crunch
. In
either case, no code that isn't required will be included in the final
product.
You should now be seeing the power of the MirageOS configuration tool: We have built several applications that use fairly complex concepts such as filesystems and block devices that are independent of the implementations (by virtue of our application logic being a functor), and then are able to assemble several combinations of unikernels via relatively simple configuration files and options passed at compile time and runtime.
You built multiple MirageOS unikernels, and at some point you may want them to communicate with the external world. We use network for that.
Please note this document only explains how to communicate from your host system to your unikernel. For having your unikernel being able to communicate with the entire Internet, you will need to setup firewalling (NAT).
For MirageOS unikernels running as Unix application, we have the option to use the host system stack (Unix sockets API). For any other target, we must use the OCaml network stack. On Unix, we can also use the OCaml network stack by utilizing a tuntap interface.
Which of the two network stacks to use can be specified via command-line arguments,
just as we configured the key-value store in the previous example. The example in
the device-usage/network
directory of
mirage-skeleton
is illustrative:
open Mirage
let runtime_args = [ runtime_arg ~pos:__POS__ "Unikernel.port" ]
let main = main ~runtime_args "Unikernel.Main" (stackv4v6 @-> job)
let stack = generic_stackv4v6 default_network
let () = register "network" [ main $ stack ]
We have a runtime argument defining which TCP port to listen
for connections on. The network device is derived from
default_network
, a function provided by Mirage which will choose a
default based on the target the user chooses to pass to
mirage configure
- just like the default provided by
generic_kv_ro
in the previous example.
generic_stackv4v6
builds a network stack on top of
the physical interface given by default_network
.
Let's get the network stack compiling using the Unix target and using the host
system network stack first. This is the default when using the Unix target -
the --net socket
is superfluous.
$ cd device-usage/network
And, as previously, configure and build the unikernel (if needed, use
make depends
to install the required dependency):
$ mirage configure -t unix --net socket
$ dune build
And run it:
$ dist/network
This Unix application is now listening on TCP port 8080,
and will print to the console information about data received.
Let's try talking to it using
the commonly available netcat nc(1)
utility. From a different console
execute:
$ echo -n hello tcp world | nc -nw1 127.0.0.1 8080
You should see log messages documenting your connection from 127.0.0.1
in the console running dist/network
. You may have noticed that some
information that you may have expected to see after looking at unikernel.ml
isn't being output. That's because we haven't specified the log level for
dist/network
, and it defaults to info
. Some of the output for this application
is sent with the log level set to debug
, so to see it, we need to run dist/network
with a higher log level for all logs:
$ dist/network -l "*:debug"
The program will then output the debug-level logs, which include the content of any messages it reads. Here's an example of what you might see:
$ dist/network -l "*:debug"
2017-02-10 17:23:24 +02:00: INF [tcpip-stack-socket] Manager: connect
2017-02-10 17:23:24 +02:00: INF [tcpip-stack-socket] Manager: configuring
2017-02-10 17:23:27 +02:00: INF [application] new tcp connection from IP 127.0.0.1 on port 36358
2017-02-10 17:23:27 +02:00: DBG [application] read: 15 bytes:
hello tcp world
Next, let's try using the OCaml network stack with the Unix target. This is done
by passing --net direct
to mirage configure
. It will be
necessary to run these programs with sudo
(or doas
) or as the root user, as
they need direct access to a tap
network device. The IPv4 address defaults to
10.0.0.2, and can be configured via the --ipv4=10.0.42.2/24
runtime argument.
$ mirage configure -t unix --net direct
$ dune build
You need to construct a tap interface, and configure an IP address in the same network segment on your host system to communicate with the unikernel:
$ sudo modprobe tun
$ sudo tunctl -u $USER -t tap0
$ sudo ifconfig tap0 10.0.0.1 up
And run it:
$ sudo dist/network -l "*:debug"
This will output once it successfully started up that it constructed the TCP/IP stack successfully.
You are now able to communicate to the unikernel - via control messages (ICMP), and also netcast, as shown before.
Now you should be able to ping the unikernel's interface:
$ ping 10.0.0.2
PING 10.0.0.2 () 56() bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=38 time=0.527 ms
64 bytes from 10.0.0.2: icmp_seq=2 ttl=38 time=0.367 ms
64 bytes from 10.0.0.2: icmp_seq=3 ttl=38 time=0.291 ms
^C
--- 10.0.0.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 0.291/0.395/0.527/0.098 ms
Finally, you can then execute the same nc(1)
commands as before (modulo the
target IP address) to interact with the running unikernel:
$ echo -n hello tcp world | nc -nw1 10.0.0.2 8080
And you will see the same output in the unikernel's terminal:
read: 15 "hello tcp world"
Let's make a network-enabled unikernel with hvt
! The IP
configuration should be similar to what you've set up in the previous
examples, but instead of -t unix
or -t macosx
, build with a hvt
target. If you need to specify a static IP address, remember that it
should go at the end of the command in which you invoke solo5-hvt
,
just like the argument to hello
in the hello-key
example.
$ cd device-usage/network
$ mirage configure -t hvt
$ make depends
$ make build
$ solo5-hvt --net:service=tap100 -- dist/network.hvt --ipv4=10.0.0.10/24
| ___|
__| _ \\ | _ \\ __ \\
\\__ \\ (||(|)|\\_|\\_()()()()><<
See the Solo5 documentation on running Solo5-based
unikernels
for details on how to set up the tap100
interface used above for hvt
networking.
To have your unikernel being able to communicate with the entire Internet, you will need to setup a firewall (and NAT).
The MirageOS network stack supports
DHCP, have
a look into --dhcp true
if you're interested.
There are a number of other examples in device-usage/
which show
some simple invocations of various devices like consoles and clocks.
You may also be interested in the applications/
directory of the
mirage-skeleton
repository, which contains examples that use
multiple devices to build nontrivial applications, like DNS, DHCP, and
HTTPS servers.
The real MirageOS website (which is itself a unikernel) may also be of interest to you! Documentation is available at mirage-www, and the source code is published in a public GitHub repository.