Context

After reading the latest FreeBSD Journal “Netgraph for the Rest of Us” by Daniel J. Bell, I discovering the tool ngbuddy(8), and immediately wanted to try it out myself. I actually wanted to experiment with Netgraph for a while now, but I could never figure out how to configure it manually, as it is rather complicated. However, this seemed like the perfect opportunity, so I installed FreeBSD 14.3-RELEASE in a fresh VM on my second Proxmox VE and created a couple of Bastille VNET jails, which I modified to use Netgraph.


Steps

Here are the steps I’ve used to get everything up and running, minus setting up the FreeBSD hosts itself (pkg repos, doas, ssh, etc.):

Installing the necessary packages:

doas pkg install -y bastille ngbuddy

Setting up ngbuddy:

doas service ngbuddy enable
ngbuddy_enable:  -> YES
Adding default public and private bridges.
ngbuddy_public_if:  -> bridge0
ngbuddy_private_if:  -> nghost0

Starting ngbuddy:

doas service ngbuddy start
Starting ngbuddy.
Created 3 links.

Running bastille setup:

doas bastille setup

Disabling pf, as I don’t need NAT with bastille0 right now:

doas service pf onedisable

Bootstrapping and patching FreeBSD 14.3:

doas bastille bootstrap 14.3-RELEASE update

Creating a VNET jail:

doas bastille create -V jail05 14.3-RELEASE DHCP vtnet0

Stopping the VNET jail:

doas bastille stop jail05

Editing the VNEt jail’s jail.conf:

doas bastille edit jail05
jail05 {
  $if_name = "$name";
  $bridge = "public";
  enforce_statfs = 2;
  devfs_ruleset = 13;
  exec.clean;
  exec.consolelog = /var/log/bastille/jail05_console.log;
  exec.prestart = "service ngbuddy jail $if_name $bridge";
  exec.start = '/bin/sh /etc/rc';
  exec.stop = '/bin/sh /etc/rc.shutdown';
  exec.prestop = "service ngbuddy unjail $if_name $name";
  host.hostname = jail05;
  mount.devfs;
  mount.fstab = /usr/local/bastille/jails/jail05/fstab;
  path = /usr/local/bastille/jails/jail05/root;
  securelevel = 2;
  osrelease = 14.3-RELEASE;
  vnet;
  vnet.interface = "$if_name";
}

Editing the VNET jail’s rc.conf:

doas bastille edit jail05 root/etc/rc.conf
ifconfig_jail05_name="vnet0"
ifconfig_vnet0="SYNCDHCP"
syslogd_flags="-ss"
sendmail_enable="NO"
sendmail_submit_enable="NO"
sendmail_outbound_enable="NO"
sendmail_msp_queue_enable="NO"
cron_flags="-J 60"

Starting the netgraph VNET jail:

doas bastille start jail05
[jail05]:
jail05: created

Checking if DHCP is working:

doas bastille cmd jail05 ifconfig vnet0
[jail05]:
vnet0: flags=1008843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
        options=28<VLAN_MTU,JUMBO_MTU>
        ether 58:9c:fc:10:ff:dd
        inet 10.2.70.131 netmask 0xffffff00 broadcast 10.2.70.255
        media: Ethernet autoselect (1000baseT <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>

Conclusion

One nice side effect of using netgraph instead of epair and bridge interfaces is that the output of ifconfig on the FreeBSD host isn’t nearly as cluttered:

vtnet0: flags=1008943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
        options=4800bb<RXCSUM,TXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,LINKSTATE,TXCSUM_IPV6>
        ether bc:24:11:5c:4d:b5
        inet 10.2.70.107 netmask 0xffffff00 broadcast 10.2.70.255
        media: Ethernet autoselect (10Gbase-T <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
lo0: flags=1008049<UP,LOOPBACK,RUNNING,MULTICAST,LOWER_UP> metric 0 mtu 16384
        options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
        inet 127.0.0.1 netmask 0xff000000
        inet6 ::1 prefixlen 128
        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2
        groups: lo
        nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>

For comparison, here is the output of ifconfig on another FreeBSD jail host which uses epair and a bridge:

vtnet0: flags=1008943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
        options=c00b9<RXCSUM,VLAN_MTU,VLAN_HWTAGGING,JUMBO_MTU,VLAN_HWCSUM,VLAN_HWTSO,LINKSTATE>
        ether bc:24:11:44:8a:6d
        media: Ethernet autoselect (10Gbase-T <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
lo0: flags=1008049<UP,LOOPBACK,RUNNING,MULTICAST,LOWER_UP> metric 0 mtu 16384
        options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
        inet 127.0.0.1 netmask 0xff000000
        inet6 ::1 prefixlen 128
        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x2
        groups: lo
        nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
bridge0: flags=1008943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
        options=0
        ether 58:9c:fc:10:ff:db
        inet 10.2.70.109 netmask 0xffffff00 broadcast 10.2.70.255
        id 00:00:00:00:00:00 priority 32768 hellotime 2 fwddelay 15
        maxage 20 holdcnt 6 proto rstp maxaddr 2000 timeout 1200
        root id 00:00:00:00:00:00 priority 32768 ifcost 0 port 0
        member: e0a_jail04 flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
                ifmaxaddr 0 port 14 priority 128 path cost 2000
        member: e0a_jail03 flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
                ifmaxaddr 0 port 11 priority 128 path cost 2000
        member: e0a_jail02 flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
                ifmaxaddr 0 port 8 priority 128 path cost 2000
        member: e0a_jail01 flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
                ifmaxaddr 0 port 5 priority 128 path cost 2000
        member: vtnet0 flags=143<LEARNING,DISCOVER,AUTOEDGE,AUTOPTP>
                ifmaxaddr 0 port 1 priority 128 path cost 2000
        groups: bridge
        nd6 options=9<PERFORMNUD,IFDISABLED>
e0a_jail01: flags=1008943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
        description: vnet0 host interface for Bastille jail jail01
        options=8<VLAN_MTU>
        ether 02:d6:ec:ad:52:0a
        groups: epair
        media: Ethernet 10Gbase-T (10Gbase-T <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
e0a_jail02: flags=1008943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
        description: vnet0 host interface for Bastille jail jail02
        options=8<VLAN_MTU>
        ether 02:10:ec:6c:e7:0a
        groups: epair
        media: Ethernet 10Gbase-T (10Gbase-T <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
e0a_jail03: flags=1008943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
        description: vnet0 host interface for Bastille jail jail03
        options=8<VLAN_MTU>
        ether 02:21:90:a4:5f:0a
        groups: epair
        media: Ethernet 10Gbase-T (10Gbase-T <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
e0a_jail04: flags=1008943<UP,BROADCAST,RUNNING,PROMISC,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
        description: vnet0 host interface for Bastille jail jail04
        options=8<VLAN_MTU>
        ether 02:6b:4b:c6:ea:0a
        groups: epair
        media: Ethernet 10Gbase-T (10Gbase-T <full-duplex>)
        status: active
        nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>

To see what kind of speeds I could achieve, I ran iperf3 on two of the Netgraph VNET jails, and got ~10 Gbit/s of traffic as a result. In comparison, the same VM with epair/bridge VNET jails only reached ~6 Gbit/s, but with significantly lower CPU load (~45% vs ~99%). Since benchmarks with virtual Network interfaces such as vtnet often come with weird performance issues, take this result with a grain of salt.

Another thing that worked well is CARP, which still causes problems with FreeBSD jails whose epair interfaces are connected to a bridge. For example, here is a snippet of /var/log/messages of one of my FreeBSD NAS servers:

Aug 13 20:06:16 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:01 vlan 0 moved from e0a_adguard03 to lagg0.60
Aug 13 20:06:16 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:02 vlan 0 moved from e0a_adguard03 to lagg0.60
Aug 13 20:06:16 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:03 vlan 0 moved from e0a_unbound03 to lagg0.60
Aug 13 20:06:16 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:04 vlan 0 moved from e0a_unbound03 to lagg0.60
Aug 13 20:06:17 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:02 vlan 0 moved from lagg0.60 to e0a_adguard03
Aug 13 20:06:18 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:03 vlan 0 moved from e0a_unbound03 to lagg0.60
Aug 13 20:06:18 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:01 vlan 0 moved from e0a_adguard03 to lagg0.60
Aug 13 20:06:18 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:02 vlan 0 moved from e0a_adguard03 to lagg0.60
Aug 13 20:06:18 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:04 vlan 0 moved from e0a_unbound03 to lagg0.60
Aug 13 20:06:18 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:01 vlan 0 moved from lagg0.60 to e0a_adguard03
Aug 13 20:06:19 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:01 vlan 0 moved from e0a_adguard03 to lagg0.60
Aug 13 20:06:19 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:02 vlan 0 moved from e0a_adguard03 to lagg0.60
Aug 13 20:06:19 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:03 vlan 0 moved from e0a_unbound03 to lagg0.60
Aug 13 20:06:19 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:04 vlan 0 moved from e0a_unbound03 to lagg0.60
Aug 13 20:06:20 freebsd-nas-2 kernel: bridge60: mac address 00:00:5e:00:01:01 vlan 0 moved from lagg0.60 to e0a_adguard03

Update (2025-10-04) This problem was caused by missing firewall rules on the jails, which caused both jails’s CARP VIP to become MASTER. After allowing CARP traffic through, the log messaged stopped and failover works flawlessly.

With Netgraph, there were no such log entries, and ping to the CARP VIP configure on two of the Netgraph VNET jails worked without a hitch.

As you can generate Graphs to visualize Netgraph deployments, I went ahead and made one myself:

doas ngctl dot

Since the command ngctl dot | dot -T png -o netgraph.png I found on the Klara Systems article “Using Netgraph for FreeBSD’s Bhyve Networking” only threw me the error “dot: graph is too large for cairo-renderer bitmaps. Scaling by 0.00850981 to fit”, I used a online version of graphviz and converted the SVG file to a PNG: ngctl dot output rendered with graphviz

Some other useful commands are:

doas ngctl list
There are 8 total nodes:
  Name: jail02          Type: eiface          ID: 00000080   Num hooks: 1
  Name: vtnet0          Type: ether           ID: 00000001   Num hooks: 2
  Name: jail04          Type: eiface          ID: 00000062   Num hooks: 1
  Name: public          Type: bridge          ID: 00000006   Num hooks: 7
  Name: ngctl82484      Type: socket          ID: 00000089   Num hooks: 0
  Name: jail05          Type: eiface          ID: 0000006a   Num hooks: 1
  Name: jail03          Type: eiface          ID: 00000015   Num hooks: 1
  Name: jail01          Type: eiface          ID: 00000079   Num hooks: 1
doas service ngbuddy status
public
  jail02: RX 942B, TX 64.44 KB
  jail01: RX 56.41 KB, TX 38.32 KB
  jail05: RX 384B, TX 104.07 KB
  jail04: RX 42B, TX 117.36 KB
  jail03: RX 4.88 GB, TX 9.65 GB
  vtnet0 (upper): RX 2.33 MB, TX 2.37 MB
  vtnet0 (lower): RX 2.28 MB, TX 2.39 MB

In conclusion, thanks to the ngbuddy (Netgraph Buddy) package, it was very easy to get started with Netgraph. Compared to running VNET jails with bridge and epair, I found this approach much cleaner, so I may end up using this in future instead. I am looking forward to trying it out with FreeBSD installed on physical hardware to see what performance I can achieve.


Commands

The commands I have used:

  • ngbuddy(8) - Simplified netgraph(4) manager for jail(8) and bhyve(8)
  • bastille(8) - Bastille is an open-source system for automating deployment and management of containerized applications on FreeBSD.
  • doas(1) - execute commands as another user
  • ngctl(8) - netgraph control utility