Porting a C++20 Finger Daemon to FreeBSD

April 20, 2026

I wrote an updated finger daemon in modern c++ several months ago to run on a different domain of mine. That server is running docker on top of Debian, so I'd written it with an aim for that platform. With my recent move to operate more internet services from the 90s, I wanted to port my service over to FreeBSD and get it running inside of a jail on my main BSD server.

The Build System Problem

The project uses meson. The meson.build for the main executable had these link args:

link_args : ['-static', '-static-libgcc', '-static-libstdc++'],

Those are GCC-isms. FreeBSD ships clang, and clang doesn't have -static-libgcc or -static-libstdc++ -- it uses libc++ and handles its runtime differently. Trying to build with those flags just dies at link time. The fix was simple: remove the whole link_args line. We don't need a statically linked binary inside a jail anyway.

Boost ASIO and pthreads

After stripping the bad flags the linker still failed, this time with a pile of undefined symbols:

ld: error: undefined symbol: pthread_condattr_init
>>> referenced by posix_event.ipp:41
ld: error: undefined symbol: pthread_create
>>> referenced by posix_thread.ipp:60

Boost ASIO uses pthreads internally for its event loop. On Linux with GCC and the old static flags, this got pulled in implicitly. On FreeBSD with clang it does not -- you have to ask for it explicitly. The boost-libs package even tells you this in its install message: "Don't forget to add -pthread to your linker options when linking your code." I missed it the first time.

The right way to do this in meson is a threads dependency:

threads_dep = dependency('threads')

Then add threads_dep to the dependencies list for every build target. The final meson.build for the main executable looks like:

executable('finger',
  'main.cpp','handler.cpp',
  dependencies : [boost_dep, threads_dep],
  install : true)

After that, clean configure and compile:

meson setup builddir
meson compile -C builddir
meson install -C builddir

Binary ends up at /usr/local/bin/finger. The C++ source itself needed zero changes -- coroutines, std::filesystem, ASIO, all of it compiled clean under clang 19.

The Jail

I created a thin Bastille jail called finger at 192.168.1.104. The daemon reads plan files from /var/finger/users/ -- one file per username, contents returned verbatim. Rather than manage those files from inside the jail, I put them on the host at /srv/finger/users/ and nullfs bind-mounted that directory in:

/srv/finger/users  /usr/local/bastille/jails/finger/root/var/finger/users  nullfs  rw  0  0

That goes in the jail's fstab. Now I can add or edit plan files from the host without touching the jail at all:

echo "Just another hacker." > /srv/finger/users/pete

Port 79 forwarding is handled by a one-liner in Bastille's rdr.conf:

dual re0 any any tcp 79 79

Bastille translates that into a pf rdr rule on reload.

The Service

The daemon runs in the foreground -- no self-daemonizing. FreeBSD's daemon(8) handles that. The rc.d script is minimal:

#!/bin/sh
# PROVIDE: fingerd
# REQUIRE: NETWORKING
# KEYWORD: shutdown

. /etc/rc.subr

name="fingerd"
rcvar="fingerd_enable"
command="/usr/sbin/daemon"
command_args="-f -p /var/run/fingerd.pid /usr/local/bin/finger"
pidfile="/var/run/fingerd.pid"

load_rc_config $name
: ${fingerd_enable:=NO}

run_rc_command "$1"

Drop that in /usr/local/etc/rc.d/fingerd, chmod 755 it, then:

sysrc fingerd_enable=YES
service fingerd start

Result

The whole thing works. The C++ code was perfectly portable -- no Linux assumptions baked in. The only friction was the build system carrying GCC baggage and the implicit pthread link that Linux let slide. Two changes to meson.build, a jail, a bind mount, and an rc script.

$ finger pete@peteftw.com
Just another hacker.

back