Isolated IP Management

Updated: August 25, 2023 at 4:15 PM

Originally Posted: August 17, 2023 at 12:00 PM

Edited on 2023-08-25 to add how to start the jail on each boot.

The latest iteration of my home firewall has a spare interface. Many switches these days have a dedicated management interface, and I wanted something similar for my firewall. I want an interface that I can plug in, get an IP via DHCP, and I can ssh into and it’ll “just work” even if there’s a misconfiguration, or an IP conflict.

On FreeBSD, vnet jails are a way to isolate an interface such that it won’t interfere (or receive interference) from other interfaces. Some may have just used epair and assigned IPs, but this could cause conflict, while using a unix domain socket keeps things isolated, and FreeBSD ships w/ everything needed to make this work. A package like socat isn’t needed.

Configuring the host was straight forward, make a directory for the socket:

mkdir /var/mgmt

And then add a line to /etc/inetd.conf to listen to incoming connections on the socket and launch sshd:

/var/mgmt/mgmt.ssh.sock stream  unix    nowait  root    /usr/sbin/sshd  sshd -i

The -i option to sshd tells it to run in “inetd” mode, which means that the stdin and stdout of the process is the socket to use for communication.

The next harder part was getting a jail configured that would accept incoming connections and forward them to the unix domain socket.

First the jail configuration, which goes in /etc/jail.conf or similar location:

mgmt {
        host.hostname = mgmt;                           # Hostname

        vnet ="new";
        vnet.interface="mgmt0";

        path = "/usr/jails/mgmt/root";                   # Path to the jail
        mount.fstab="/usr/jails/mgmt/fstab";             # mount spec

        mount.devfs;                                    # Mount devfs inside the jail
        devfs_ruleset = "101";

        exec.start = "/bin/sh /etc/rc";                 # Start command
        exec.stop = "/bin/sh /etc/rc.shutdown";         # Stop command
}

There isn’t anything special in this. It’s pretty standard jail configuration, the differences are the vnet configuration, and the devfs_ruleset.

The devfs_ruleset is necessary in order to expose the bpf interface used by dhclient. This required the following lines in /etc/devfs.rules:

[mydevfsrules_jail=100]
add include $devfsrules_hide_all
add include $devfsrules_unhide_basic
add include $devfsrules_unhide_login

[devfsrules_jail_dhcp=101]
add include $mydevfsrules_jail
add path 'bpf*' unhide

Note that after adding the above lines, you need to run:

service devfs start

to load the rules (per devfs(8) man page).

Note: I learned my mistake not to number my blocks immediately after the standard defaults (from /etc/defaults/devfs.rules). I had done that befure but there’s now a conflict, so I skip ahead a bit to get a unique range.

Now I needed to make a number of directories for the jail:

mkdir -p /usr/jails/mgmt/root
mkdir -p /usr/jails/mgmt/etc
mkdir -p /usr/jails/mgmt/var/mgmt
mkdir -p /usr/jails/mgmt/tmp

I needed to setup the fstab for the jail:

# Device                Mountpoint      FStype  Options         Dump    Pass#
/                       /usr/jail/mgmt/root     nullfs  ro      0       0
/usr/jail/mgmt/etc      /usr/jail/mgmt/root/etc nullfs  rw      0       0
/usr/jail/mgmt/var      /usr/jail/mgmt/root/var nullfs  rw      0       0
/var/mgmt               /usr/jail/mgmt/root/var/mgmt    nullfs  ro      0       0
/usr/jail/mgmt/tmp      /usr/jail/mgmt/root/tmp nullfs  rw      0       0

This is a little bit more tricky, It first nullfs mounts the root system. I’m using ZFS boot environments, so this is a pretty clean FreeBSD install without much host specific data. It then mounts some jail specific directories for etc, tmp and var and finally mounts the shared directory w/ the unix domain socket to the host system. Also note that a couple of the mounts are read-only to prevent the jail from modifying the system.

The etc directory was populated from the system etc via:

tar -cf - -C /etc . | tar -xf - -C /usr/jails/mgmt/etc

Then the jail was configured, first /usr/jails/mgmt/etc/rc.conf:

hostname="mgmt.funkthat.com"

sendmail_enable="NONE"
sendmail_submit_enable="NO"
sendmail_outbound_enable="NO"
sendmail_msp_queue_enable="NO"

# Management port
ifconfig_mgmt0="DHCP"

# necessary as devd can't be run in a jail
synchronous_dhclient="YES"

inetd_enable="YES"              # Run the network daemon dispatcher (YES/NO).

The key part of this configuration that took me a while to figure out was the synchronous_dhclient line. It used to be that netif would start dhclient, but in order to better handle USB ethernet devices and other removable interfaces, it was moved to devd. The only problem is that devd hasn’t been jail’ified, and you can’t run it to get things like link notifications that would normally launch dhclient. Setting this to yes, makes sure it gets launched when the jail starts.

And then the following line was added to /usr/jails/mgmt/etc/inetd.conf:

ssh     stream  tcp     nowait  root    /usr/bin/nc     nc -N -U /var/mgmt/mgmt.ssh.sock

This is the part that will forward incoming connections to the ssh port on to the unix domain socket.

Now that everything is configured, a simple jail -c mgmt will get the jail running and accepting connections.

To get the jail to start every boot, add the following to the host’s /etc/rc.conf:

jail_enable="YES"       # Set to NO to disable starting of any jails
jail_list="mgmt"

This was testing and deployed on a FreeBSD 14-CURRENT build as of August 8th, 2023, or more specifically, from main-n264621-09c20a29328, but it should work on all currently supported FreeBSD releases.

| Home |