Skip to content
 

OpenVPN’s built-in packet filter

This is an obscure and undocumented feature of OpenVPN, which however can be useful.

It turns out that OpenVPN has an internal packet filter, which can be used to apply simple firewall rules to clients. Apparently it has been available for quite some time even, although the documentation makes no mention of it whatsoever. In this article, we'll see how to use it to limit client-to-client connectivity in bridged mode (something that, I wrote earlier, seemed impossible), but the mechanism is more general than that, and can be used as a simple generic firewalling mechanism.

General ideas

Filtering happens at the server, which is the hub for all client connections, and the packet filter rules are per-client. A file should be provided for each client, containing the rules to apply to that client. The rules, in turn, are divided into rules allowing/denying connectivity to other clients (identified by common name), and rules generically allowing/denying connectivity to IP subnets. Here's a sample rule file for a client:

[CLIENTS ACCEPT]
-john
-nick
[SUBNETS DROP]
+10.0.4.0/24
[END]

The file must end with the [END] line. Note that since clients are identified by their common name, this feature cannot be used when duplicate-cn is in use (that, however, shouldn't be used anyway).

In this example, the client to which these rule are applied will be allowed to connect to any client by default ([CLIENTS ACCEPT]), except john and nick, which are explicitly denied. Alternatively, one could set a default deny policy by using [CLIENTS DROP] and then explicitly allowing only certain clients using +client. The same syntax applies to subnet rules.

Rules apply to client traffic going in both directions so, for a client to be allowed to connect to another client, communication must be allowed by the rules of both clients.

If denying communication is not enough for you, it's also possible to use [KILL] to terminate the client instance in case it tries to do something it shouldn't do.

In our example, we will assume a simple deployment with four clients: their common names are john, nick, bob and max. Who can connect to who is shown in the following table:

john nick bob max
john - yes yes no
nick yes - no yes
bob yes no - no
max no yes no -

So here are the four rule templates:

$ cat /etc/openvpn/john.pf
[CLIENTS DROP]
+nick
+bob
[SUBNETS ACCEPT]
[END]
$ cat /etc/openvpn/nick.pf
[CLIENTS DROP]
+john
+max
[SUBNETS ACCEPT]
[END]
$ cat /etc/openvpn/bob.pf
[CLIENTS DROP]
+john
[SUBNETS ACCEPT]
[END]
$ cat /etc/openvpn/max.pf
[CLIENTS DROP]
+nick
[SUBNETS ACCEPT]
[END]

Making it work

Sounds easy so far eh? BUT! We have said nothing yet about how OpenVPN is supposed to be told of these rules, and nothing is mentioned in the docs either. The only clues are in the sources.

Under certain circumstances (explained below), when a client connects OpenVPN sets an environment variable pf_file that contains a string which is the name of the file where OpenVPN expects to find the packet filter rules for the client. Obviously, the variable has different values for different clients. As an example, pf_file may contain something like openvpn_pf_baa933844c673b8b7c649ce933172abc.tmp.

So it's possible for a script to inspect that variable and create a file called $pf_file with the rules for the client (whose common name is also available in the environment variable common_name). The perfect place to do so is in the --client-connect script. Here is a sample script that creates the rule file for the connecting client:

#!/bin/sh

# /etc/openvpn/client-connect.sh: sample client-connect script using pf rule files

# rules template file
template="/etc/openvpn/${common_name}.pf"

# create the file OpenVPN wants with the rules for this client
if [ -f "$template" ] && [ ! -z "$pf_file" ]; then
  cp -- "$template" "$pf_file"
else
  # if anything is not as expected, fail
  exit 1
fi

Plugin

Now, here's the catch. There are a number of things that have to be in place for the above to happen (these are the "certain circumstances" I mentioned earlier).

Let's (only apparently) divert, and briefly introduce the concept of plugin in OpenVPN. In the current version of OpenVPN, plugins are dynamic objects (.so files) that contain a number of functions, that are invoked by OpenVPN at predetermined hooks in the program flow. At the very minimum, the plugin has to provide:

  • An initialization function. This can be named either openvpn_plugin_open_v1(), or openvpn_plugin_open_v2(); they differ in the number of arguments they accept, but OpenVPN is able to detect which one the plugin provides. This function, besides initializing any internal variable, data structure or state the plugin needs, also must tell OpenVPN which of the many possible hooks it's interested in. Possible values are listed in openvpn-plugin.h in the OpenVPN source distribution.
  • A worker function, named openvpn_plugin_func_v2(). This is the function that does the real work, and is invoked by OpenVPN when it reaches one of the hooks the plugin declared its interest in. Since this means that the function may then be called many times for different hooks, one of its arguments ("type") has a value that specifies what hook is being invoked, so the worker function can test it and behave appropriately.
  • A shutdown/cleanup function (openvpn_plugin_close_v1()), which is called when OpenVPN terminates to give a chance to the plugin to perform the cleanup it needs.

Besides those mentioned, the plugin can implement other optional functions, and OpenVPN detects if they are present and call them when appropriate.

In fact, most of the hooks used for the plugin mechanism have an equivalent script hook (--up, --down, --tls-verify, etc.). The main difference is that invoking a function in the plugin does not need to execute external processes.

However, the particular hook that enables OpenVPN's internal packet filter (OPENVPN_PLUGIN_ENABLE_PF) is available only to plugins.

Now, what must happen for the packet filter to be enabled is as follows:

  • At initialization time, the plugin must register for the OPENVPN_PLUGIN_ENABLE_PF hook;
  • When the worker function is called with type set to OPENVPN_PLUGIN_ENABLE_PF, which happens upon client connection and periodically after that, it must return success (OPENVPN_PLUGIN_FUNC_SUCCESS) for OpenVPN to know that filtering must be applied to this client.

The fact that the worker function is invoked at client connection and then is re-invoked periodically makes it possible to dynamically change or disable the filtering policy for a client (OpenVPN will re-read the rule file if it changes); in our example we just return success always.

A plugin is declared in OpenVPN's configuration file as follows:

plugin /path/to/the/plugin.so

Our example

So, here's the code for a minimalistic plugin (minimal_pf.c) that registers for OPENVPN_PLUGIN_ENABLE_PF and returns success when OpenVPN calls its worker function to know whether the packet filter should be enabled for the specific client. The code is commented.

/* minimal_pf.c 
 * ultra-minimal OpenVPN plugin to enable internal packet filter */
#include <stdio.h>
#include <stdlib.h>

#include "openvpn-plugin.h"

/* dummy context, as we need no state */
struct plugin_context {
  int dummy;
};

/* Initialization function */
OPENVPN_EXPORT openvpn_plugin_handle_t openvpn_plugin_open_v1 (unsigned int *type_mask, const char *argv[], const char *envp[]) {
  struct plugin_context *context;
  /* Allocate our context */
  context = (struct plugin_context *) calloc (1, sizeof (struct plugin_context));

  /* Which callbacks to intercept. */
  *type_mask = OPENVPN_PLUGIN_MASK (OPENVPN_PLUGIN_ENABLE_PF);

  return (openvpn_plugin_handle_t) context;
}

/* Worker function */
OPENVPN_EXPORT int openvpn_plugin_func_v2 (openvpn_plugin_handle_t handle,
            const int type,
            const char *argv[],
            const char *envp[],
            void *per_client_context,
            struct openvpn_plugin_string_list **return_list) {
  
  if (type == OPENVPN_PLUGIN_ENABLE_PF) {
    return OPENVPN_PLUGIN_FUNC_SUCCESS;
  } else {
    /* should not happen! */
    return OPENVPN_PLUGIN_FUNC_ERROR;
  }
}

/* Cleanup function */
OPENVPN_EXPORT void openvpn_plugin_close_v1 (openvpn_plugin_handle_t handle) {
  struct plugin_context *context = (struct plugin_context *) handle;
  free (context);
}

Download and unpack the OpenVPN source tarball (strictly speaking, you need just openvpn-plugin.h from it), and build the plugin with the following commands:

INCLUDE="-I/path/to/openvpn/source"         # CHANGE THIS!!!!
CC_FLAGS="-O2 -Wall -g"
NAME=minimal_pf
gcc $CC_FLAGS -fPIC -c $INCLUDE $NAME.c && \
gcc $CC_FLAGS -fPIC -shared -Wl,-soname,$NAME.so -o $NAME.so $NAME.o -lc

If there were no errors, this should leave you with minimal_pf.so in the directory where you ran the build commands. I'm assuming you will then copy it to /etc/openvpn on the server.

This is a sample server configuration file using tap (bridged mode), declaring the just-built plugin and client-to-client to allow client to client communication (modulo the restrictions imposed by the internal packet filter):

local 172.28.0.1
port 1194
proto udp
dev tap
topology subnet
script-security 2
up /etc/openvpn/up.sh
plugin /etc/openvpn/minimal_pf.so
client-connect /etc/openvpn/client-connect.sh
ca /etc/openvpn/rootCA.crt
cert /etc/openvpn/server.crt
key /etc/openvpn/server.key
dh /etc/openvpn/dh1024.pem
ifconfig-pool-persist ipp.txt
server-bridge 10.180.0.1 255.255.255.0 10.180.0.2 10.180.0.100
client-config-dir /etc/openvpn/ccd
client-to-client
keepalive 10 120
comp-lzo
persist-key
persist-tun
status openvpn-status.log
verb 3

The "up" script just brings up the tap interface and sets its IP address to 10.180.0.1/24. The /etc/openvpn/ccd directory contains four files, one per client, where each is assigned a static IP address:

john 10.180.0.2
nick 10.180.0.3
bob 10.180.0.4
max 10.180.0.5

When OpenVPN starts on the server, the log should show that the plugin was recognized and registered for the PF hook:

Sat Jun 12 15:14:17 2010 PLUGIN_INIT: POST /etc/openvpn/minimal_pf.so '[/etc/openvpn/minimal_pf.so]' intercepted=OPENVPN_PLUGIN_ENABLE_PF

Let's start the clients, and verify that communication is indeed only allowed between the intended pairs of clients:

# on john's machine, ping bob (should work)
john:~$ ping 10.180.0.4
PING 10.180.0.4 (10.180.0.4) 56(84) bytes of data.
64 bytes from 10.180.0.4: icmp_seq=1 ttl=64 time=14.6 ms
64 bytes from 10.180.0.4: icmp_seq=2 ttl=64 time=2.40 ms
64 bytes from 10.180.0.4: icmp_seq=3 ttl=64 time=2.32 ms
^C
--- 10.180.0.4 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2026ms
rtt min/avg/max/mdev = 2.329/6.468/14.674/5.802 ms

# ping max (should fail)
john:~$ ping 10.180.0.5
PING 10.180.0.5 (10.180.0.5) 56(84) bytes of data.
From 10.180.0.2 icmp_seq=1 Destination Host Unreachable
From 10.180.0.2 icmp_seq=2 Destination Host Unreachable
From 10.180.0.2 icmp_seq=3 Destination Host Unreachable
^C
--- 10.180.0.5 ping statistics ---
4 packets transmitted, 0 received, +3 errors, 100% packet loss, time 3009ms
, pipe 3

And the results will be confirmed by repeating the test on the other clients.

27 Comments

  1. Traffic says:

    On the OpenVPN server, when using --client-to-client, intra-client packets do not pass through iptables and so cannot be filtered on a server by iptables .. If you run multiple servers instances on the same machine then packets from one server IP to another (eg: c1 on s1:10.8.0.0/24 -> c1 on s2:10.9.0.0/24) will be filtered by iptables .. but not clients on the same server instance.
    Which is probably with this filter mechanism was introduced.

    • waldner says:

      Well, if you use OpenVPN at layer 3, you can force packets through iptables, see here for example. At layer 2 instead (ie when using tap) you can either use the internal packet filter as described here, or use the new proxy_arp_pvlan feature as described here (I haven't tried it, but the OP said it worked).

  2. tspf says:

    Great that this plugin is still working as of OpenVPN 2.3.2!

    Really helped me out on my layer2 vpn to establish something like VLANs.

  3. Tom says:

    Is it still necessary to use a plugin or you can use management-client-pf setting?

    • waldner says:

      You can use either way, the result should be the same.

      • Tom says:

        I just tested with the last version 2.3.2 from wheezy backports and it didn't see pf_file env variable. I tried with and without management-client-pf setting on the server

        • waldner says:

          That variable is only defined if you use a plugin and register for the OPENVPN_PLUGIN_ENABLE_PF hook. If you don't use a plugin, then you have to use the management interface, but it is done differently (without temporary file), see the management interface documentation for the details.

      • Traffic says:

        After extensive testing (Openvpn 2.3.6 gnu linux) --management-client-pf is still not implemented correctly. Do _not_ use --management-client-pf at this time to use this plugin successfully.

  4. Tim says:

    This has now been fixed and released in 2.3.2. Good news.

  5. Jekader says:

    Hi, that's a cool way to filter traffic.

    The only hing I dont't get is: does OpenVPN filter traffic by default? I.e. If I push routes A and B to a client, and he manually adds route C via the OpenVPN tun adapter - will this traffic be permitted or dropped? I made some test and it seems that such traffic successfully reaches the other end of the pipe.

    • waldner says:

      By default everything is allowed. To filter, either use iptables (or equivalent on non-linux systems) or OpenVPN's builtin mechanism described in the article. Filtering using iptables is easier.

  6. Willem says:

    Thanks Waldner.

    Fix for the segmentation fault works, but it wasn't included in any OpenVPN version, so we had to add and compile it by ourself.

  7. Leonardo says:

    Yes, I enabled the client-to-client option in the configuration file and I can ping from one client to the other and I see traffic in both tun interfaces, but no traffic in the OpenVPN server. I see just the traffic that goes to the server, if I ping it then I see the ICMP traffic in the OpenVPN's tun interface.

  8. Leonardo says:

    Hi,

    I have just started working with OpenVPN, I've been reading and trying to set a VPN for two days; so far I have managed to have an OpenVPN server working with three clients and everything seems to be ok. Now the thing is that I would like to apply some firewall rules to some clients but I can't filter the interface tun0 in the OpenVPN server because apparently there's no traffic passing through it (I used the tcpdump command), I also followed this post http://backreference.org/2010/05/02/controlling-client-to-client-connections-in-openvpn but still nothing.

    I would really appreciate if any of you guys could give some advice so I can accomplish what I want to do.

    Thanks a lot

  9. Tim says:

    This is a great find. I set up using your instructions and all seemed to work well.

    Several hours into using this in a quasi production setting, i began getting segfaults in openvpn:

    kernel: openvpn[2632]: segfault at 0000000000000000 rip 00000000004400e3 rsp 00007fff529b02d0 error 4

    once i disabled the mminimal_pf.so plugin, all returned to normal.

    any thoughts? version:

    OpenVPN 2.1.4 x86_64-redhat-linux-gnu [SSL] [LZO2] [EPOLL] [PKCS11] built on Apr 24 2011

    • waldner says:

      Hi Tim,

      sure, may well be a bug in the code. That was meant as an example and was not tested extensively, although most of it was taken from the file plugin/examples/simple.c that comes with the vanilla tarball. I would suggest performing the usual debugging actions, ie building with debug symbols, running in gdb, get a backtrace, etc.

    • Willem says:

      Tim,

      Same segfault problem here. Did you found a solution for this?

      Thanks,
      Willem

  10. lin says:

    does this mechanism work with tun mode?

    • waldner says:

      It does, however unless you have very special needs it's probably easier and quicker to use normal firewall rules (eg iptables) when in tun mode.