Bypassing a VPN Using Cgroups
Posted on Thu 30 June 2022 in Hackery
Changes
2022-08-27
- Fixed small bug in /usr/local/bin/if-up-novpn script
2022-08-10
- Added ~/bin/bash-novpn script example.
- Made /usr/local/bin/if-up-novpn script more robust
Setting up a VPN bypass for applications
You use a VPN and all your traffic gets routed through it. But what if you want some traffic to bypass it?
In the past I've just used SSH to tunnel traffic to another machine acting as a SOCKS proxy. Then it's just a matter of setting the application's proxy to point to it. Example:
$ ssh -D 8080 me@kids-linux-machine
Now there is a SOCK5 proxy at socks://localhost:8080 as long as that SSH connection is up. Set your application's proxy settings to this and it works great. For some applications. And only if they support using a proxy.
But what if the application is complex? Maybe it uses a lot of non HTTP-based protocols, uses UDP, runs other applications all which you need to bypass the VPN?
In this tutorial I'll be using the Control Groups(cgroups) kernel feature to control which routes an application's network data uses.
Assumptions
This tutorial assumes:
- you are using a recent version of Debian. I'm using Debian Bullseye.
- You are running all the commands as root.
- You are using a VPN like openVPN. Others should work also but they have not been tested.
Routing table
Create a routing table by creating a file named /etc/iproute2/rt_tables.d/novpn-route.conf containing:
11 novpn
Like this:
echo 11 novpn | sudo tee /etc/iproute2/rt_tables.d/novpn-route.conf
Add routes and policy to the routing table
You will need to find a place to add two commands for creating the bypass route. Where you do this depends on your particular setup.
ip rule add fwmark 11 table novpn
ip route add default via YOUR_GATEWAY_IP table novpn
The first one routes all packets marked with "11" to the "novpn" routing table. The second line does the actual routing.
In order to do this automatically I created the following script and put it in /etc/network/if-up.d:
/usr/local/bin/if-up-novpn:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Notice that a route also needs to be added for the local LAN. My novpn routing table looks like this:
default via 192.168.1.1 dev wlan0 src 192.168.1.100
192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.100 metric 600
Mark packets
Then we need to mark all packets coming from the cgroup classid of 0x00110011 with mark "11". This will cause the packets to use the "novpn" routing table we created above.
iptables -t mangle -A OUTPUT -m cgroup --cgroup 0x00110011 -j MARK --set-mark 11
How you do this really depends on the firewall you are using. I use Firehol. I just added the line to my firehol.conf file above the interface stanza.
Depending on how restrictive your firewall rules are, you may also need to add a rule to allow these marked packets through. With Firehol I add this to the appropriate interface stanza: client4 all accept rawmark 11
cls_group module
Load the cls_group module as we'll need to to be present for the next step:
modprobe cls_group
You may need to load the cls_cgroup kernel module on boot by creating a file named /etc/modules-load.d/cls_cgroup-novpn.conf with a sigle line in it: cls_cgroup
What is this module? Net_cls tags packets so that the Linux traffic controller can identify packets coming from specific cgroup processes. Packets tagged with net_cls can then be acted upon by iptables/net_filter.
Putting applications into cgroups
Now we need a way of putting running applications into cgroups. There is a simple daemon that will do this automatically based on rules listed in a file. It's called cgrulesengd. It is part of libcgroup. In debian cgrulesengd is found in the cgroup-tools package. Lets install that along with daemontools which we'll use to start cgrulesengd. (FIXME: create a working systemd unit file)
apt-get install cgroup-tools daemontools-run
Set up the cgroup and net_cls controller. Create /etc/cgconfig.conf:
mount {
net_cls = /sys/fs/cgroup/net_cls;
}
group novpn {
net_cls {
net_cls.classid = "0x00110011";
}
}
Create a rule for testing in /etc/cgrules.conf:
*:/tmp/traceroute net_cls novpn/
This example will put any process whose command path is /tmp/traceroute into the novpn/ group of the net_cls controller. We'll test this later on.
Also remove any other rules that were in the cgrules.conf file. I strongly suggest you read the cgrules.conf man page to get an idea of how to set up your rules.
Create the file daemontools needs to set up and start cgrulesengd. Create /etc/service/cgrulesengd/run:
1 2 3 4 5 |
|
Set it executable and it should start up immediately:
chmod +x /etc/service/cgrulesengd/run
cgconfigparser loads the /etc/cgconfig.conf file and mounts the net_cls filesystem under /sys/fs/cgroups.
You can add -v, -vv, or -vvv switches for increased logging verbosity. I found that the logging of when it actually changes a cgroup only happens when maximum verbosity(-vvv) is turned on. Not ideal. I turn off all verbosity when I am sure cgrulesengd is doing what it should. Otherwise it really logs a lot of junk.
A note about cgrulesengd
From the man page:
The list of rules is read during the daemon startup is are cached in the daemon's memory. The daemon
reloads the list of rules when it receives SIGUSR2 signal. The daemon reloads the list of templates when
it receives SIGUSR1 signal.
This can be done like this:
killall -SIGUSR1 cgrulesengd
killall -SIGUSR2 cgrulesengd
Testing
Turn on your VPN if you have not already.
Open a terminal and do a traceroute to some IP:
traceroute -n 4.2.2.2
Now try this:
cp `which traceroute` /tmp
/tmp/traceroute -n 4.2.2.2
You should see that the traceroute produced by the traceroute binary located in /tmp bypassed your VPN.
In syslog ( if you have run cgrulesengd with -vvv) something like this:
Apr 4 18:15:41 ebk CGRE[505289]: EXEC Event: PID = 505321, tGID = 505321
Apr 4 18:15:41 ebk CGRE[505289]: Scanned proc values are 0 0 0 0
Apr 4 18:15:41 ebk CGRE[505289]: Scanned proc values are 0 0 0 0
Apr 4 18:15:41 ebk CGRE[505289]: Found matching rule * for PID: 505321, UID: 0, GID: 0
Apr 4 18:15:41 ebk CGRE[505289]: Executing rule * for PID 505321...
Apr 4 18:15:41 ebk CGRE[505289]: Will move pid 505321 to cgroup 'novpn/'
Apr 4 18:15:41 ebk CGRE[505289]: Adding controller net_cls
Apr 4 18:15:41 ebk CGRE[505289]: OK!
Apr 4 18:15:41 ebk CGRE[505289]: Cgroup change for PID: 505321, UID: 0, GID: 0, PROCNAME: /tmp/traceroute OK
Debugging
Stop cgrulesengd:
svc -d /etc/service/cgrulesengd
Run it in debug mode in the foreground:
cgrulesengd -d
You should see your processes that match the rules in /etc/cgrules.conf handled by cgrulesengd.
Check for processes in the "novpn" cgroup:
ps -e -o pid,user,cmd,cgroup | grep novpn
Check the routing rules and tables:
# ip rou show table novpn
default via 192.168.100.1 dev wlp9s0 src 192.168.100.5
# ip rule show
0: from all lookup local
32765: from all fwmark 0xb lookup novpn
32766: from all lookup main
32767: from all lookup default
To help with troubleshooting(and I found this is really helpful in general) I created a script to get me a bash prompt:
~/bin/bash-novpn:
1 2 |
|
Then I added a rule for it: *:/home/myusername/bin/bash-novpn net_cls novpn/
Potential problems
If your VPN software forces the system to use different DNS servers and these DNS servers refuse to service queries from outside the VPN address space you will need to use some method of changing the DNS servers in use on a per-process basis. I found the proxc script to work well for this.
Example - ~/bin/firefox-novpn:
1 2 3 4 5 6 7 8 |
|
If there is no route going to the VPN interface(tun0) it will just run normally. Otherwise Firefox will be run via proxc using 4.2.2.2 as the DNS server. Use your ISP DNS server or any other public access DNS server.
Additional sources:
- https://serverfault.com/questions/669430/how-to-bypass-openvpn-per-application
- https://translate.yandex.ru/translate?url=https%3A%2F%2Fyurinb.livejournal.com%2F55588.html&lang=ru-en
--
This work is licensed under CC BY-NC 4.0 - https://creativecommons.org/publicdomain/zero/1.0/