How to run tailscale in a FreeBSD jail
This article will go over what a jail is and how to create one that can run tailscale.
Installing FreeBSD
Instructions on how to install the operating system would be beyond the scope of this article, but it is a very simple process that consists of following Chapter 2: Installing FreeBSD of the FreeBSD handbook. This resource will be referenced repeatedly throughout this article and it is recommended to read it for more detailed information that is omitted in this article.
About tailscale
Tailscale is a zero trust network access solution that addresses the problem of exposing services to the public-facing internet by making them accessible through a VPN without routing all client traffic through a relay.
It is a seemingly magical piece of software that relies on tailscale's DERP servers to establish peer-to-peer connections between tailscale devices in a mesh network (known as tailnet).
To read more about tailscale and how it works; visit the tailscale documentation.
While tailscale does not officially support FreeBSD, it is still possible to use it from the community maintained port. A list of all supported operating systems can be found in the documentation for installing tailscale.
Without getting ahead of the article, the challenge that will be covered in this is about installing tailscale inside a jail, which requires its own network stack.
About jails
Chapter 17: Jails and Containers of the FreeBSD handbook goes over what a jail is and how it can be created, used, and managed.
In essence; a jail is a feature-rich chroot (change-root) environment. That means that it's a container that uses the system kernel, but a different userspace for a separate root directory. Instead of requiring the user to manually run chroot, jails can be managed using FreeBSD's init system and other utilities.
This makes a jail something between a traditional chroot environment and an LXC (Linux container), because while it has extensive management tools, it still runs on the native filesystem. Unlike LXC, jails come with FreeBSD out of the box and do not require additional packages.
Jail types
The documentation goes over multiple types of jails, but they are better understood as different configurations. FreeBSD does not offer jail presets, so to speak, that let the user choose between jail types. The simplest form of a jail is a so-called thick jail. It can only see its own root directory, making it highly isolated from the host system.
Jails that have their own network stack and do not share an IP address with the host are called VNET jails. This configuration will be required in order to run tailscale inside a jail.
System preparation
Before configuring a jail, it is recommended to first prepare the system by creating a bridge, enabling jail services and downloading a jail userspace among other things.
In order to set a static IP address for the host system, it is necessary to identify the network interface in use. This can be done by looking at the output of ifconfig and searching for an interface named something similar to em0 or re0 and then adding the following line to /etc/rc.conf:
Please note that re0 represents the name of the network interface used by the host system. The first IP address indicates the desired static IP and the last IP address denotes the network mask.
ifconfig_re0="inet 192.168.1.100 netmask 255.255.255.0"
The static IP should be within the router's subnet. The command ifconfig shows the subnet prefix next the local IP address under the network interface in use as a trailing slash followed by a number. If it is /24, then that means the user may choose any available IP address from 1 to 254 as the last octet. In this case, the chosen IP address is 192.168.1.100 on the subnet 192.168.1.0, which means the subnet mask is 255.255.255.0.
Since VNET jails are virtual systems and do not have a physical network interface of their own, it is necessary to create a bridge between the host system's network interface and each jail's network stack.
Please note that bridge0 represents the name of the bridge to be created.
cloned_interfaces="bridge0"
ifconfig_bridge0="inet 192.168.1.100 addm re0 up"
The above configuration is merely an example and depends on the previously identified network interface as well as the static IP address set in the preceding step.
In order to make all jails start on boot, the following two options should be enabled inside of /etc/rc.conf:
jail_enable="YES"
jail_parallel_start="YES"
They can also be added by using `sysrc` as root:
sysrc jail_enable="YES"
sysrc jail_parallel_start="YES"
Next, it will be required to create a directory to contain the jail's userspace. Technically speaking, no specific directory is expected by the system and so it can be chosen freely. The documentation provides examples for /usr/local/jails but also lists /jail and /usr/jail as commonly used directories. This article will follow the documentation's example.
mkdir -p /usr/local/jail
It is also recommended to create a dedicated directory for saving a compressed userspace tarball.
mkdir -p /usr/local/jail/media
With the directory in place, it is time to download a userspace to it, which can be found under FreeBSD downloads. There, select the system's architecture, a release, and finally a base.txz file. This may change in the future so it is recommended to cross verify with Chapter 17.4.1: Creating a Classic Jail of the official documentation.
At the time of writing the article, the command should look as follows:
Please note that 99.9-RELEASE is not a real version and should be replaced with the lastest release.
fetch https://download.freebsd.org/ftp/releases/amd64/amd64/99.9-RELEASE/base.txz -o /usr/local/jails/media/99.9-RELEASE-base.txz
This concludes the preparation required in order to extract, configure, and start a jail.
Creating a jail
A jail needs its own folder that will be set as its root directory.
Please note that foo represents the name of the jail.
mkdir -p /usr/local/jail/foo
Technically speaking, the path to the jail's root directory does not need to correspond to the jail's name, but that will be presumed in the following configuration provided by the documentation.
Add the following section to /etc/jails.conf:
Please note that the variable name corresponds to the name of the function; in this case foo. The variable bridge should correspond to the bridge's name that was created in the previous section of this article. The variable id represents the last octet for the jail's desired static IP address.
foo {
# STARTUP/LOGGING
exec.consolelog = "/var/log/jail_console_${name}.log";
# PERMISSIONS
allow.raw_sockets;
exec.clean;
mount.devfs;
devfs_ruleset = 20;
# PATH/HOSTNAME
path = "/jails/containers/${name}";
host.hostname = "${name}";
# VNET/VIMAGE
vnet;
vnet.interface = "${epair}b";
# NETWORKS/INTERFACES
$id = "101";
$ip = "192.168.1.${id}/24";
$gateway = "192.168.1.1";
$bridge = "bridge0";
$epair = "epair${id}";
# ADD TO bridge INTERFACE
exec.prestart = "/sbin/ifconfig ${epair} create up";
exec.prestart += "/sbin/ifconfig ${epair}a up descr jail:${name}";
exec.prestart += "/sbin/ifconfig ${bridge} addm ${epair}a up";
exec.start += "/sbin/ifconfig ${epair}b ${ip} up";
exec.start += "/sbin/route add default ${gateway}";
exec.start += "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
exec.poststop = "/sbin/ifconfig ${bridge} deletem ${epair}a";
exec.poststop += "/sbin/ifconfig ${epair}a destroy";
}
In this example, the chosen static IP is 192.168.1.101 with the hostname being the function's name foo.
The jail is now ready to be started using:
service jail start foo
Alternatively, all configured jails can be started at once:
service jail start
If jail_enable and jail_parallel_start are enabled in /etc/rc.conf, a reboot will automatically start all jails configured in /etc/jails.conf. If those options have not been enabled, a jail can still be started using onestart:
service jail onestart foo
Managing a jail
Since a jail is a chroot environment at its core, it can be managed fully from within. To access the jail's shell, the following command can be used:
Please note that foo represents the name of the jail.
jexec -u root foo
It is also possible to manage a jail from the host system by using some of the following exemplary commands.
Executing a command without opening the jail's shell:
jexec -l foo uname -a
Managing a jail's packages from the host system without installing pkg in the jail:
pkg -j foo upgrade
To see a list of all running jails:
jls
Installing tailscale
All difficult steps required in order to install tailscale were already done in the section System preparation. Downloading the package can be done either through FreeBSD ports or through the official tailscale Installation script (which downloads the package from the same repository).
Please note that the following command should be executed from outside the jail since pkg is not available in a jail out of the box.
pkg -j foo install tailscale
Then enter the jail using:
jexec -u root foo
Enabling tailscale using sysrc:
sysrc tailscaled_enable="YES"
Starting tailscale using service:
service tailscaled start
From here on out, tailscale can be managed using the command line interface:
tailscale login
If everything is working correctly, a link to a login page should appear.
Closing words
Congratulations, tailscale should now be running inside a jail!
tailscale status