openvpntechThis tutorial by user Felix J. Ogris shows us how to get OpenVPN routing with BIRD set up on FreeBSD.


If you run OpenVPN as an unprivileged user and/or in a chroot environment, it can’t dynamically modify routes. This becomes a problem if you run multiple OpenVPN daemons, no matter whether they run on the same box or on different servers. When a client disconnects from one instance and later connects to another instance, you have to update your internal routing information for that client. To solve this, I’ve been using the BIRD Internet Routing Daemon.
The relevant part of my /usr/local/etc/openvpn.conf looks like this:

mode server
chroot /usr/local/etc/openvpn/chroot
client-connect /bin/
client-disconnect /bin/
script-security 2
user openvpn
group openvpn

Note that the location of the client-connect and client-disconnect script /bin/ is relative to the chroot directory /usr/local/etc/openvpn/chroot, which contains three subdirectories:

drwxr-xr-x  2 root  wheel    bin
drwxr-xr-x  2 root  wheel    ccd
drwxrwxr-x  2 root  openvpn  tmp
  • bin contains three tools:
    -r-xr-xr-x  1 root  wheel
    -r-xr-xr-x  2 root  wheel  nc
    -r-xr-xr-x  2 root  wheel  sh

    I copied sh from /rescue/sh, while nc was hardlinked to sh. All binaries in /rescue are statically linked, so they’ll work even in a chroot environment.

  • ccd contains my client config files, each containing an ifconfig-push and optionally one or more iroute statements for a particular client. Those files are owned and writeable by root only.
  • tmp contains the control socket for BIRD, and a dynamically created config file for each OpenVPN client.

When an OpenVPN client connects, reads its ip address and routes from the config file in ccd, writes this information in BIRD compatible syntax to the config file in tmp, and informs BIRD to reload its configuration.
When a client disconnects, just empties the config file in tmp, and reloads BIRD.
My /usr/local/etc/bird.conf looks like this:

router id;
log syslog all;
filter rfc1918 {
  if net ~ then accept;
  else reject;
protocol kernel {
  scan time 0;
  import all;
  export all;
protocol device {
  scan time 0;
  import all;
  export all;
protocol direct {
protocol static {
  include "/usr/local/etc/openvpn/chroot/tmp/*.conf";
  import filter rfc1918;
  export none;

Usually, BIRD creates its control socket in /var/run. In order to have it somewhere else, you have to tweak /etc/rc.conf:

bird_config="/usr/local/etc/bird.conf -s /usr/local/etc/openvpn/chroot/tmp/bird.ctl"

Of course you can download my script and put it to /usr/local/openvpn/chroot/bin.