DEV Community


Transparent proxy on Arch Linux, with iptables and systemd slice

outloudvi profile image Outvi V ・3 min read

Long story short. To build a transparent proxy, we need to redirect every outbound request to the proxy. It seems easy with iptables... wait a second, no. Outbound requests from the proxy should not be redirected, or nothing can be sent out. How to do that? Usually an exception on the proxy server IP is set. But what if you have multi proxy servers? What if your proxy servers are changing overtime? Wouldn't it be a pain editing iptables rules?

Well, we have cgroups. We can put our proxy program in a cgroup, and give an exception to this cgroup on iptables. On Arch Linux, the easist way to create cgroups is via systemd. Therefore, we are picking the systemd PANTS.

Here shows an example for a TCP IPv4 transparent proxy.

Create a slice, and put stuffs in

If your proxy program runs as a systemd service, you can add this "Slice=" line to its configurations:


After daemon-reloading and restarting, your proxy program will be in the "bypass.slice" group.

You can also see a new directory named bypass.slice in /sys/fs/cgroup/unified/:

# ls /sys/fs/cgroup/unified/bypass.slice/
cgroup.controllers  cgroup.max.descendants  cgroup.threads  io.pressure       cgroup.procs            cgroup.type     memory.pressure
cgroup.freeze       cgroup.stat             cpu.pressure    run-u1925.scope/
cgroup.max.depth    cgroup.subtree_control  cpu.stat

If your proxy program is not a systemd service, run it with systemd-run

sudo systemd-run --slice bypass.slice --scope clash -c /etc/clash/config.yaml

We use --scope so that you can see the stdouts and manipulate on stdins. It requires root permission (or not if you are using cgroups v2?). I would think you have got the root permission, since we're editing iptables later!

The same way if you want to start an application which bypasses the proxy:

sudo systemd-run --slice bypass.slice --scope firefox

Or a shell which bypasses the proxy:

sudo systemd-run --slice bypass.slice --scope -S

Have a try

Let's run a ping in a slice:

$ sudo systemd-run --slice test2.slice --scope -S
Running scope as unit: run-r7865e1749deb48e4bcf797f1f403f396.scope
$ ping

The ping is running here. Now we block all outbound packets from this slice:

iptables -A OUTPUT -m cgroup --path "test2.slice" -j DROP

The ping will start to fail:

ping: sendmsg: Operation not permitted

Okay. We see that iptables are working with our slice (cgroup). Now delete the rule:

iptables -L OUTPUT --line-numbers
# Find something like
#  "2 DROP   all  --  anywhere  anywhere  cgroup test2.slice"
# and remember its number
iptables -D OUTPUT 2

iptables' time!

Parts of this paragraph comes from Dreamacro/clash#158 and this.

# Create a chain on the nat table.
# Why nat table? Because we are doing redirects later.
iptables -t nat -N TP-TCP

# On this chain:
# 1. Everything from the bypass.slice should be where they were
iptables -t nat -A TP-TCP -m cgroup --path "bypass.slice" -j RETURN
# 2. Everything to local & loopback address should be where they were
iptables -t nat -A TP-TCP -d -j RETURN
iptables -t nat -A TP-TCP -d -j RETURN
iptables -t nat -A TP-TCP -d -j RETURN
iptables -t nat -A TP-TCP -d -j RETURN
iptables -t nat -A TP-TCP -d -j RETURN
iptables -t nat -A TP-TCP -d -j RETURN
iptables -t nat -A TP-TCP -d -j RETURN
iptables -t nat -A TP-TCP -d -j RETURN
# 3. Everything else should go thourh the proxy port
iptables -t nat -A TP-TCP -p tcp -j REDIRECT --to-ports 7892
# 4. All output packets should go through TP-TCP chain
iptables -t nat -A OUTPUT -p tcp -j TP-TCP

Do some curls and see if they are going through the proxy. Also, run

iptables -L TP-TCP -t nat -v -n 

to see if any pkts and bytes is going through. If it works, we're done! Hooray! 🎉


Editor guide