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

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