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
In this example, the client to which these rule are applied will be allowed to connect to any client by default (
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
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,
So it's possible for a script to inspect that variable and create a file called
#!/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() , oropenvpn_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 inopenvpn-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 (
However, the particular hook that enables OpenVPN's internal packet filter (
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 * 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
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
This is a sample server configuration file using tap (bridged mode), declaring the just-built plugin and
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
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.
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.
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).
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.
Is it still necessary to use a plugin or you can use management-client-pf setting?
You can use either way, the result should be the same.
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
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.
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.
Thanks for sharing.
@Traffic: Can you be more specific? What doesn't work?
This has now been fixed and released in 2.3.2. Good news.
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.
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.
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.
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.
Yes, if you enable client-to-client packets are managed internally by OpenVPN and you don't see any traffic on the tun interface. If you want to see packets on the tun interface and be able to use iptables to firewall them, you have to disable the client-to-client option, as explained here: http://backreference.org/2010/05/02/controlling-client-to-client-connections-in-openvpn/
Whether it is disabled or not, I can't see anything (I might have been doing something wrong) but thanks for your help anyway, now it's working since I applied this patch: https://community.openvpn.net/openvpn/ticket/163 and compiled it from the source code.
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
Well, does tcpdump show traffic at least at the client tun interface?
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
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.
Tim,
Same segfault problem here. Did you found a solution for this?
Thanks,
Willem
AFAICT it's been fixed, see https://community.openvpn.net/openvpn/ticket/163. However I don't know whether the fix is already included in stable.
Glad to see it's been fixed.
I decided to run multiple instances of openvpn and added firewall rules so the packet filtering occurs outside openvpn. that was an easy and straight forward solution. openvpn seems to run with minimal overhead, so it's not a drain on the server to run two or three instances.
it also keeps all the firewall rules under one roof.
That's way the beetsst answer so far!
does this mechanism work with tun mode?
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.