FreeBSD: Route jail traffic through VPN

published on on FreeBSD, Security, NAS

I wanted to expose a single jail of my FreeBSD NAS to a network of a client via OpenVPN while it's reachable both from my network and from the clients' network. It should send all of its traffic through that VPN tunnel so that it appears like it is just another computer on that foreign network.

Luckily FreeBSD offers a great way to solve this by creating a separate routing table apart from my main routing table that is used when starting OpenVPN (so that it can populate it's routes there) and when starting the jail (the jail in fact will consider that routing table as the only routing table available and therefore use it for anything).

I assume a setup using FreeBSD 10 or 11, ezjail for jail management, OpenVPN as a VPN technology and pf as a firewall.

First of all, install OpenVPN on the host (pkg install openvpn should do), configure it so that you can connect from the host and set up your jail using ezjail. Just give it an internal IP, I use a bridged setup for my jails (see FreeBSD NAS: System Setup chapter 7 for more information on how to set it up), so my jail has the IP on my local network.

Setting up a second routing table

FreeBSD has support for multiple routing tables that is called FIB (Forwarding Information Base). Therefore there is a tool called setfib that can be used to wrap a command to bind it to a given routing table. But first we have to set up a second routing table. Because this is a kernel startup configuration we have to extend our /boot/loader.conf file to include the following:


The provided number is the amount of FIBs that should be set up. Here it's to because I only need my normal one for my network and a second one for the VPN network. Afterwards reboot:


Be sure to have pf and gateway functionality enabled as well as your router (your real one to be able to establish a VPN connection) configured.



Start pf if not already done.

Establish the VPN connection

I'm lazy, therefore I don't use a rc-script. I establish my OpenVPN connection in rc.local in a screen to have it running in background and to be able to monitor it.

My /etc/rc.local looks like this, please not that the FIB 1 is the VPN routing table, while 0 is my main routing table for the rest of my stuff:


# Add default route to be able to establish a VPN connection to the clients' endpoint
setfib -F 1 /sbin/route add default

# Start a screen with "setfib -F 1 openvpn ...", OpenVPN will use FIB 1 and therefore not interfere your normal traffic on FIB 0
/usr/local/bin/screen -d -m setfib -F 1 /usr/local/sbin/openvpn --config /usr/local/etc/openvpn/my-vpn.conf

To check if that works without rebooting, just run:

sh /etc/rc.local

Check if there's a screen (screen -ls) and if the connection is established correctly by attaching to it to check it.

Configure NAT on pf

OpenVPN will create a device called tun0 at startup which represents the network device used for tunneling the network traffic. Now we have to set up a NAT between the jail and tun0 so that all traffic is sent to that device.

My /etc/pf.conf looks like this:

scrub in all

# = jail IP
nat on tun0 from to any -> ( tun0 )
# If you only want to put outgoing traffic through the VPN, you might also want to block any incoming traffic, this was not necessary for my use case ;-)
# block in on tun0 all

Restart pf (or start it), and we're ready to go:

service pf restart

Test the VPN setup

For a easy test, we can now run curl on our VPN routing table to see if we still have the public IP address of the normal internet connection or if it is the public IP provided by the VPN:

# This should give your normal public IP
setfib -F 0 curl

# This should tive the public IP of the VPN
setfib -F 1 curl

For me, the public address of the VPN was returned, so the setup worked.

Configure the jail

I always recognize in FreeBSD that the tools integrate very well with each other, so of course ezjail provides support for FIBs.

This is how my /usr/local/etc/ezjail/myjail config looks like:

export jail_myjail_ip=""
export jail_myjail_fib="1"

After restarting the jail using ezjail-admin restart myjail, the connection should be operational. To prove this, run curl one time from the host, and one time from within the jail. In the jail it should return the VPN public IP, while outside it should do everything as usual.

Further references