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:
- The
learn-address script, firewaller, gets called to add a new
address.
- 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.
- 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.
- 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?)