We are going to use mirage-router as a unikernel. Unikernel can be used to highlight a possible network configuration for your computer. This configuration consists of having 2 interfaces:

  1. one to retrieve all the packets you want to send to Internet
  2. one to send all encrypted packets to your VPN server only
    Gunikernelunikerneltap0tap010.8.0.3unikernel->tap0tap1tap110.0.0.2unikernel->tap1br0br010.8.0.2tap0->br0br1br110.0.0.1tap1->br1OpenVPN serverOpenVPN serverbr0->OpenVPN serverComputerComputerComputer->br1

The unikernel will then encrypt all the packets received and send them back to the VPN server. The server can then decrypt them and send them to Internet. The reverse will also be true.

One disadvantage of this method is the assignment of IP addresses. They can only be fixed between the client and the server.

Network configuration

As with the virtualisation of any system, a network configuration stage is necessary so that the virtualised systems can communicate. The general idea is to create a 'bridge' to which we can attach the virtual interfaces used by our systems. In our configuration, 2 bridges are required. However, we need to use the same IP address as the one our VPN server will allocate to us. In our previous configuration, we assigned alice the IP 10.8.0.2.

Create our bridges

Let's start by creating our bridge:

$ sudo ip link add name br0 type bridge
$ sudo ip link set dev br0 up
$ sudo ip address add 10.8.0.2/24 dev br0
$ sudo ip link add name br1 type bridge
$ sudo ip link set dev br1 up
$ sudo ip address add 10.0.0.1/24 dev br1

Create TAP interfaces

A solo5 unikernel needs a tap interface. This is a virtual interface on which our unikernel will define its IP address and connect to the network. Here's how to create a tap interface.

$ sudo ip tuntap add mode tap tap0
$ sudo ip link set dev tap0 up
$ sudo ip link set tap0 master br0
$ sudo ip tuntap add mode tap tap1
$ sudo ip link set dev tap1 up
$ sudo ip link set tap1 master br1

A tap interface (unlike tun) requires the Ethernet frame. Some will note that we have configured our OpenVPN server with tun (dev tun). However, our unikernel does not transmit packets to the OpenVPN server without the Ethernet frame - so our unikernel is compatible with a server using a tun interface.

This choice is based on our observation that there are few OpenVPN server configurations with tap interfaces.

Firewall

As with the server, we need to enable our virtual machines to communicate with the outside world:

$ sudo sysctl net.ipv4.ip_forward=1
$ sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -j MASQUERADE

The aim of our last command is to let our unikernel communicate with the outside world even if it is using a private IP address (in our case 10.0.0.2). Note that this type of configuration becomes incompatible with OpenVPN as a client.

Persistence

This configuration is not persistent, meaning that the next time you reboot your computer, br{0,1} and tap{0,1} will disappear. However, your system can manage the creation of these elements at boot time. In Debian, for example, you can modify the /etc/network/interfaces file to create your bridges at boot time:

$ cat >>/etc/network/interfaces <<EOF
auto br0
iface br0 inet static
  address 10.8.0.2
  netmask 255.255.255.0
  broadcast 10.8.0.255
  bridge_ports none
  bridge_stp off
  bridge_fd 0
  bridge_maxwait 0

auto br1
iface br1 inet static
  address 10.0.0.1
  netmask 255.255.255.0
  broadcast 10.0.0.255
  bridge_ports none
  bridge_stp off
  bridge_fd 0
  bridge_maxwait 0
EOF

Other distributions (like Archlinux) use netctl, for example. Simply create a new profile such as:

$ cat >/etc/netctl/openvpn-bridge<<EOF
Description="OpenVPN Bridge"
Interface=br0
Connection=bridge
IP=static
Address='10.8.0.2/24'
SkipForwardingDelay=yes
EOF
$ cat >/etc/netctl/kvm-bridge<<EOF
Description="KVM Bridge"
Interface=br1
Connection=bridge
IP=static
Address='10.0.0.1/24'
SkipForwardingDelay=yes
EOF

And enable it:

$ sudo netctl enable openvpn-bridge
$ sudo netctl enable kvm-bridge

As far as tap interfaces are concerned, their management is often relegated to virtual machine managers such as libvirt, Xen or albatross. On this last point, we are going to present these different solutions later, particularly QubesOS, which uses Xen.

MirageVPN configuration

We can now start configuring our client. The materials required for our client are:

  1. our ca.crt
  2. our client's certificate alice.crt
  3. her private key alice.key
  4. the ta.key file we generated on our server

Our unikernel has no file system. The idea is to create an image of our configuration which can then be used by our unikernel. Fortunately, OpenVPN allows you to put the content of our materials directly into the configuration file:

$ export OPENVPN_IP=<ipv4> # Must be set!
$ cat >config.sh<<EOF
#!/bin/bash

CA_FILE=\$1
CRT_FILE=\$2
KEY_FILE=\$3
TA_FILE=\$4
OUTPUT_FILE=\$5

cat >\$OUTPUT_FILE<<PRELUDE
client
proto tcp
remote $OPENVPN_IP 1194
nobind
persist-key
cipher AES-256-CBC
remote-cert-tls server
PRELUDE

function extract() {
  cat \$2 | sed -ne "/-BEGIN \${1}-/,/-END \${1}-/p"
}

echo "<ca>" >> \$OUTPUT_FILE
extract "CERTIFICATE" \$CA_FILE >> \$OUTPUT_FILE
echo "</ca>" >> \$OUTPUT_FILE

echo "<cert>" >> \$OUTPUT_FILE
extract "CERTIFICATE" \$CRT_FILE >> \$OUTPUT_FILE
echo "</cert>" >> \$OUTPUT_FILE

echo "<key>" >> \$OUTPUT_FILE
extract "PRIVATE KEY" \$KEY_FILE >> \$OUTPUT_FILE
echo "</key>" >> \$OUTPUT_FILE

echo "tls-auth [inline] 1" >> \$OUTPUT_FILE
echo "<tls-auth>" >> \$OUTPUT_FILE
extract "OpenVPN Static key V1" \$TA_FILE >> \$OUTPUT_FILE
echo "</tls-auth>" >> \$OUTPUT_FILE

SIZE=\$(stat --printf="%s" \$OUTPUT_FILE)
truncate -s \$(( ( ( \$SIZE + 512 - 1 ) / 512 ) * 512 )) \$OUTPUT_FILE
EOF

In this little script, you need to set $OPENVPN_IP to the public IP address of your OpenVPN server. This little script will generate a compatible configuration file for our unikernel. Just run it this way:

$ scp root@$OPENVPN_IP:/root/easy-rsa/pki/ca.crt .
$ scp root@$OPENVPN_IP:/root/easy-rsa/pki/issued/alice.crt .
$ scp root@$OPENVPN_IP:/root/easy-rsa/pki/private/alice.key .
$ scp root@$OPENVPN_IP:/etc/openvpn/server/ta.key .
$ chmod +x config.sh
$ ./config.sh ca.crt alice.crt alice.key ta.key alice.config

The last command creates a file which can be used as a "block" by our unikernel. The constraint is that its size must be a multiple of 512 and that the extra bytes must correspond to '\000'.

How to launch your MirageVPN client

You can download a reproducible binary. Get the bin/ovpn-router.hvt artifact.

We can now launch our unikernel. This involves using our Solo5 "tender" and defining the right routes to redirect all our traffic to br1 and to ensure that all encrypted traffic leaving our br1 goes to our OpenVPN server.

The Solo5 tender is available via apt or on GitHub.

$ apt install gnupg
$ curl -fsSL https://apt.robur.coop/gpg.pub | gpg --dearmor > /usr/share/keyrings/apt.robur.coop.gpg
$ echo "deb [signed-by=/usr/share/keyrings/apt.robur.coop.gpg] https://apt.robur.coop ubuntu-20.04 main" > /etc/apt/sources.list.d/robur.list
# replace ubuntu-20.04 with e.g. debian-11 on a debian buster machine
$ apt update
$ apt install solo5

We need to know 2 pieces of information: the interface used to communicate with Internet and our gateway.

$ export INTERFACE=$(ip route | grep default | cut -f5 -d' ')
$ export GATEWAY=$(ip route | grep default | cut -f3 -d' ')

We can therefore specify that any packets destined for our OpenVPN server must pass through our interface via our gateway.

$ sudo ip route add $OPENVPN_IP via $GATEWAY dev $INTERFACE

Now we can launch our unikernel. It should be able to initialise a tunnel with your OpenVPN server. Here's the command to launch the unikernel with our Solo5 tender:

$ solo5-hvt --block:storage=alice.config \
  --net:service=tap1 --net:private=tap0 -- ovpn-router.hvt \
  --private-ipv4=10.8.0.3/24 --private-ipv4-gateway=10.8.0.2 \
  --ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1

All we need to do now is redirect all our traffic to our unikernel. In this case, the unikernel uses the IP address 10.8.0.3. So we're going to redirect all the packets to this IP address.

$ sudo ip route add 0.0.0.0/1 via 10.8.0.3 dev br0

And that's it! All our traffic uses our unikernel to connect directly to our OpenVPN server. We can confirm that, from the outside, we are recognised by the public IP address of our OpenVPN server rather than our real public IP address.

$ test $(curl ifconfig.me) = $OPENVPN_IP
$ echo $?
0