March 3, 2006

Selectively firewalling OpenVPN users

This whole entry is very nearly a brain dump. You’ve been warned.

First, some notes on the use of learn-address scripts.

Something to understand about the use of the “update” event with an OpenVPN learn-address script. The update event is only called when a previously learned source address is seen on a different connection than the recorded one. Thus if a user changes their MAC, you don’t see an update event with the same common name (CN) but a different MAC; for that you would get a new add event with the new MAC. But if a user connects and has the same MAC as another connection (note that it may or may not be the same user; maybe it’s a reconnection and OpenVPN hasn’t dropped the dead connection) you’ll get an update event with the MAC and the new CN. As I alluded to above, I think you’re most likely to get an update event in the case of a user stealing another user’s MAC, or in the case of a user reconnecting and their MAC being redirected to their new connection.

For my purposes, I can treat “update MAC CN” as “delete MAC” then “add MAC CN.”


The use of learn-address scripts is important for my project, selectively firewalling OpenVPN users. To clarify what I’m attempting to do: some users need to have access only to certain hosts. I don’t predetermine their MAC address (I’m using bridged mode) though you could: have the learn-address script exit non-zero if the CN doesn’t match the MAC you have listed in some table. I also don’t predetermine their IP address, though it would be a simple matter to determine their MAC and then drop any packets with a MAC/IP mismatch.

To accomplish this I wrote my own learn-address script in Python (2.3; sigh, no generator expressions). It reads a file with a series of mappings between a CN and another file name. This other file name contains a list of iptables rules that should be used for connections from this CN.

You are expected to give the script its own chain (referred to as the “dispatch chain”), which needs to be called from the FORWARD chain. The script then makes rules that dispatch from the dispatch chain to per-client chains it creates on demand and fills with the rules from the files that were mapped to.

Put another way, and in more detail:

  1. The learn-address script, firewaller, gets called to add a new address.
  2. It reads the mapping file, looking for the CN (passed on the command line; see the OpenVPN manual page). If it doesn’t find the CN, no further action is taken and the address is learned.
  3. Assuming the CN is found, firewaller creates a new chain (the name of this chain is the MAC address, or the IP address learned in routed mode). This per-client chain is filled with rules from the file pointed to by the mapping in the previous step.
  4. firewaller then makes a rule in the chain you have set aside for the script. This rule matches on the client’s (MAC or IP) address and jumps to the per-client chain when it matches.

On delete, firewaller deletes the per-client chain and deletes the rule from the script’s dispatch chain. On update, firewaller acts like it received a “delete” event followed by an “add.”

More exact details on usage follow. But first, here’s firewaller.py in case you want to look at it (and its possibly helpful comments) as we go along.

First I make the dispatch chain with something like:

iptables -N openvpn-firewaller
iptables -I FORWARD 1 -m physdev --physdev-in ovpntap0 -j openvpn-firewaller

Note that I only jump to the dispatch chain if the packet actually came in through the OpenVPN TAP device. You have to use the physdev match because ovpntap0 is bridged to the rest of the LAN. In fact, for reference, my usual setup is simple:

LAN---Linux box---Internet

So the Linux box bridges the OpenVPN TAP device with the Ethernet adapter facing the LAN. Also note that I usually make the first rule in my FORWARD chain something like -m state --state ESTABLISHED,RELATED. Doing a state lookup first thing might be expensive, but I’ve never had performance problems that I’ve been able to attribute to iptables, and it sure makes the rest of the rules simple to keep track of (allow the initial packet of a connection and don’t worry about the rest of it, including the return packets). However, I put the dispatch chain call before the ESTABLISHED,RELATED rule because I don’t want the user to somehow trick another host on the network into initiating a connection to the VPN user’s machine, and then have that connection allowed by the ESTABLISHED,RELATED rule.

Next, the map file:

George_Clooney__County General_ test-policy

Note the CN has to be modified as described in both firewaller and in the OpenVPN man page. (In this case the CN prior to modification might have been “George Clooney (County General)”.) A possible firewall rules file:

[root@gateway scripts]# cat firewaller-rules/test-policy
-d 10.0.0.5 -j RETURN
-j DROP

So Mr. Clooney only has access to 10.0.0.5. The RETURN is used to bounce back to the dispatch chain, after which it will presumably return from the dispatch chain and go on through the FORWARD chain.

That’s all there is to it. Kind of.

Since I’m bridged, there’s more to worry about. Thankfully, I’m only doing IP. iptables, of course, doesn’t get a whack at non-IP traffic. So we need to block anything that’s not IP with commands such as:

[root@gateway scripts]# ebtables -L --Lx
ebtables -t filter -N openvpn-ip-only
ebtables -t filter -A FORWARD -i ovpntap0 -j openvpn-ip-only
ebtables -t filter -A openvpn-ip-only -p IPv4 -j RETURN
ebtables -t filter -A openvpn-ip-only -p ARP -j RETURN
ebtables -t filter -A openvpn-ip-only -j DROP

(BTW: ebtables for CentOS 4 at RPMforge.) Note that here I’m blocking everything except ARP and IP coming from the OpenVPN device. I’m doing this for every user, not just some users (i.e., the users that I restrict with firewaller). No problem: like I said, only doing IP. It is important to let through ARP though.

Also, note that I’m blocking Ethernet frames using 802.3 framing (with or without SNAP) here. There’s an important reason for this: ebtables says it can’t look inside 802.3 frames. So if you somehow got an 802.3 frame, I’m sketchy on what matches would and wouldn’t work in iptables. It certainly sounds like you can’t look at the destination IP (as I do). Can you look at the source MAC (as I also do)? Depending on how you write your rules, using this kind of framing might be a way around it. I have the feeling it’s up to Win32-TAP to determine the framing, and I suspect it defaults to Ethernet II and not 802.3, but why take a chance? Block it until you have a good reason to allow it. (Note that RFC 1122 says that hosts should support 802.3 framing. Of course, it says that they must support Ethernet II framing.)

I’ve tested firewaller lightly and it seems to work. There could be some problems. Obviously, something could happen/change with your iptables rules; basically they could somehow get out of sync. firewaller, by default, actually tries to work around this (basically deleting anything that gets in its way). However, it’s a fragile system since firewaller doesn’t really look at the iptables rules. It depends on the exit status of iptables to figure out if a command worked or not. It’s possible one iptables command could succeed, but then a following related command could fail, and the first command hangs around. The good news is that if firewaller exits with an error the address isn’t learned and I believe OpenVPN drops the packet. (Do watch your log file sizes as it keeps dropping packets, though; OpenVPN is kind of noisy.)

Also, another catch: be careful when using service iptables save while an OpenVPN client is connected. That’s a good way to start out with some stale rules after a reboot. In fact, the script I keep my iptables rules in deletes all rules and re-adds them when I run it, so using that while OpenVPN clients are connected could have a more dangerous impact: it would likely allow all currently connected OpenVPN clients to go anywhere they’d like. Just keep factors like these in mind when working with your iptables setup.

A few problems, or potential problems, remain with this setup. For one thing, the way I’m using firewaller, and the way my rules are constructed, no traffic destined for the OpenVPN server (which is also the firewall; see diagram above) is filtered. In other words, a restricted OpenVPN client can get to everything on the firewall that a user on the LAN could get to. In my case this isn’t a big deal. At worst they could query DNS, maybe make some DHCP requests or connect to SSH. It is something to consider, though.

Traffic coming in to the client isn’t filtered. This means you could still potentially have packets coming in to the client that you didn’t really authorize. You’d either have to lock it down with MACs (which is a management burden—remember to change your OpenVPN rules when you reinstall the TAP-Win32 device, or when you switch the NIC in your server!) or else take more drastic measures in order to be able to use iptables to filter. For example, you’d need to verify that a CN is only using one MAC or something like that, then make sure that a given source/destination MAC (depending on whether traffic is coming from or going to the restricted client) always corresponds to the IP that was allocated to the client. There is an ifconfig_pool_remote_ip environment variable that I think you should be able to read from a learn-address script (at least on the add event, and probably for an update as well) to see what IP was allocated to the “connection.” Assuming you’re letting OpenVPN assign IPs and not your DHCP server. Once you know that a connection is always using a single MAC, and that single MAC is always being referred to with the right IP address, you can lock down with iptables.

The notion that another host on the network might be passing packets into the restricted client is not such a big deal, probably. After all, they shouldn’t be able to address return traffic to a host they’re not supposed to be talking to (see above notes about ESTABLISHED,RELATED). I can think of one possible exploit, in conjunction with another hole in the filtering. We don’t filter the contents of ARP messages, so perhaps a malicious restricted client could spoof some ARP messages and get traffic redirected to it that the restricted client shouldn’t be seeing. Again, the only way to filter ARP messages would be to ensure the mapping of the CN to the MAC to the IP, and then probably use ebtables to peek inside the ARP messages to make sure they’re kosher. An alternative might be to restrict ARP messages from OpenVPN clients and instead proxy ARP for them. Or do something “like proxy ARP” since proxy ARP is kind of neutered in any recent Linux kernel. Might require a separate daemon.

All of this is kind of pointing to “use routed OpenVPN if you can.” No ARP messages from the client. No worries about Ethernet framing types. No worries about protocols other than IP. Just normal ol’ iptables like you’re used to. I guess if you can get away with routed (you don’t need broadcast traffic, don’t need to pass protocols other than IP, and you can get all the Windows stuff to work—because I know how fickle Windows seems to be when it can’t broadcast) use it.

One final note. The service ebtables save command from the RPMforge ebtables RPM saves into some kind of binary file using some kind of “atomic” save/load commands in ebtables. I actually suspect ebtables distributes the initscript that does this. I’m a little scared of this. What’s wrong with the nice format you get from ebtables -L --Lx, as demonstrated above? What happens if this binary file gets corrupted? I hope the kernel code checks the validity of this binary data before using it. (Surely it does. Right?)