How to set up TCP/UDP transparent proxy with iptables

enev_turg_f1651983412d040

enev turg

Posted on October 3, 2024

How to set up TCP/UDP transparent proxy with iptables

Abstract

We sometimes want to transparently proxy TCP or UDP traffic of Linux machines. This article explains how to do this with iptables' "TPROXY" function.

SOCKS5

SOCKS is a type of proxy protocol, and its version 5 is called SOCKS5. Here we first set up SOCKS5 proxy.

SOCKS5 supports "UDP Associate" function, which can be used to proxy UDP traffic. UDP Associate works as following: 1) the client request a UDP port (relay port) for proxying 2) the SOCKS5 server replies a port number 3) the client starts sending UDP packets to the port 4) the server deliver them to the original destination and send replied packets (if any) back to the port.

This mean that the client machine should be able to access all UDP ports of the server machine that are used for UDP associate relay ports, and there should not be a NAT between the client the server which may cause the server to reply an IP inaccessible from the client.

Also, not all SOCKS5 servers support UDP associate. Dante, 3proxy and gost support it, but ssh -D doesn't.

  • As for Dante, we need udpassociate option, and when Full cone NAT (EIM/EIF) behavior is required (such situations are not so frequent), we need udp.connectdst: no option, in its config file.

Here we assume a SOCKS5 server with UDP associate support is running at 192.168.31.2:3129.

Transparent proxy softwares

As far as I know, there are two transparent proxy softwares that can make SOCKS5 proxy available as transparent proxy supporting TCP and UDP: redsocks (forked from old redsocks, which doesn't support TCP transparent proxy) and hev-socks5-tproxy.
This is an example config file for hev-socks5-tproxy to host TCP transparent proxy on port 22222 and UDP transparent proxy on port 22224. Debug output is enabled.

misc:
    log-file: stderr
    log-level: debug

socks5:
  # Socks5 server port
  port: 3129
  # Socks5 server address
  address: 192.168.31.2
  # Socks5 UDP relay mode (tcp|udp)
  udp: 'udp'
  # Socks5 handshake using pipeline mode

tcp:
  # TCP port
  port: 22222
  # TCP address
  address: '::'

udp:
  # UDP port
  port: 22224
  # UDP address
  address: '::'
Enter fullscreen mode Exit fullscreen mode

As for redsocks, see documentation.
Note that transparent proxy usually requires root privilege.

Proxy traffic from other devices

Now we can move on to setting up iptables and iproute2. First, let's see cases of proxying traffic from other devices, since it's easier than proxying local traffic.
Here we assume there is an external computer 192.168.1.90, whose traffic we want to proxy.
First, run the following commands.

sudo iptables -t mangle -A PREROUTING -p tcp -s 192.168.1.90 -j TPROXY --on-ip 127.0.0.1 --on-port 22222
sudo iptables -t mangle -A PREROUTING -p udp -s 192.168.1.90 -j TPROXY --on-ip 127.0.0.1 --on-port 22224
Enter fullscreen mode Exit fullscreen mode

These TPROXY targets force all traffic from 192.168.1.90 to go to 22222 (TCP) or 22224 (UDP).
However, this is insufficient. We need to send packets onto the loopback device to use transparent proxies (see documentation for detail).
So we also need this (after editing /etc/iproute2/rt_tables, if needed):

sudo ip route add local default dev lo table 100
sudo ip rule add from 192.168.1.90 lookup 100
Enter fullscreen mode Exit fullscreen mode
  • ip_forward=1 is not required. Now transparent proxy should work. I recommend testing with a stun client like https://www.stunprotocol.org/, because it's lightweight, quick, informative (respond external IP and port), and available both in TCP and UDP.

We can also do the same thing with fwmark, like this:

sudo iptables -t mangle -A PREROUTING -p tcp -s 192.168.1.90 -j TPROXY --on-ip 127.0.0.1 --on-port 22222 --tproxy-mark 0x111
sudo iptables -t mangle -A PREROUTING -p udp -s 192.168.1.90 -j TPROXY --on-ip 127.0.0.1 --on-port 22224 --tproxy-mark 0x111
sudo ip rule add fwmark 0x111 lookup 100
Enter fullscreen mode Exit fullscreen mode

--tproxy-mark works similarly to --set-mark.

tcpdump result of UDP transparent proxy

This is a sample result of tcpdump for UDP transparent proxy. SOCKS5 is hosted at 127.0.0.3 (sorry it's not 192.168.31.2). The first UDP packet is actually sent to 127.0.0.1:22224, but the number 22224 is not present here. That's why debug output of transparent proxy software is very helpful.
127.0.0.3:39410 is the relay port of UDP associate.

09:18:24.316021 enp3s0 In  IP 192.168.1.90.42874 > 3.132.228.249.3479: UDP, length 28
09:18:24.316771 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [S], seq 3104424580, win 65495, options [mss 65495,sackOK,TS val 591900081 ecr 0,nop,wscale 7], length 0
09:18:24.316810 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.51066: Flags [S.], seq 1895898628, ack 3104424581, win 65483, options [mss 65495,sackOK,TS val 1315171637 ecr 591900081,nop,wscale 7], length 0
09:18:24.316842 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [.], ack 1, win 512, options [nop,nop,TS val 591900081 ecr 1315171637], length 0
09:18:24.316970 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [P.], seq 1:4, ack 1, win 512, options [nop,nop,TS val 591900081 ecr 1315171637], length 3
09:18:24.317006 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.51066: Flags [.], ack 4, win 512, options [nop,nop,TS val 1315171637 ecr 591900081], length 0
09:18:24.317145 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.51066: Flags [P.], seq 1:3, ack 4, win 512, options [nop,nop,TS val 1315171637 ecr 591900081], length 2
09:18:24.317176 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [.], ack 3, win 512, options [nop,nop,TS val 591900081 ecr 1315171637], length 0
09:18:24.317295 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [P.], seq 4:14, ack 3, win 512, options [nop,nop,TS val 591900081 ecr 1315171637], length 10
09:18:24.317469 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.51066: Flags [P.], seq 3:13, ack 14, win 512, options [nop,nop,TS val 1315171637 ecr 591900081], length 10
09:18:24.317803 lo    In  IP 127.0.0.1.44277 > 127.0.0.3.39410: UDP, length 38
09:18:24.317938 enp3s0 Out IP 192.168.1.9.49280 > 3.132.228.249.3479: UDP, length 28
09:18:24.358632 lo    In  IP 127.0.0.1.51066 > 127.0.0.3.3130: Flags [.], ack 13, win 512, options [nop,nop,TS val 591900123 ecr 1315171637], length 0
09:18:24.489589 enp3s0 In  IP 3.132.228.249.3479 > 192.168.1.9.49280: UDP, length 68
09:18:24.489823 lo    In  IP 127.0.0.3.39410 > 127.0.0.1.44277: UDP, length 78
09:18:24.490063 enp3s0 Out IP 3.132.228.249.3479 > 192.168.1.90.42874: UDP, length 68
Enter fullscreen mode Exit fullscreen mode

Caveat: ufw blocks TPROXY packets

There is a caveat relating a popular Linux firewall, ufw. It has a chain called ufw-not-local, which (by default) denies packets that are routed to the machine and whose destination doesn't match the local machine's. Therefore we need a command like:

sudo iptables -I ufw-not-local -s 192.168.1.90 -j ACCEPT
Enter fullscreen mode Exit fullscreen mode

(Use fwmark if needed)
Note that usual commands like ufw(route) allow 192.168.1.90 doesn't work.

Proxy traffic of local machine itself

TPROXY target is only allowed in mangle table of PREROUTING chain, and locally generated packets don't go through it. However, if we send local packets to the loopback device, then they go out from and return back to the machine, so can be processed by PREROUTING chain.

We have to be careful not to create a dead loop, that is, applying proxy to packets from transparent proxy or SOCKS5 server. Normal way to avoid it is filtering packets with destination IP or using iptables matches like --uid-owner or --cgroup, but here we assign a new private IP address to some device and apply proxy to all packets from the IP.
I strongly recommend trying this first to make sure other things are going well.

First, assign a private IP.

sudo ip addr add 192.168.5.5 dev lo
Enter fullscreen mode Exit fullscreen mode

Then, add iproute2 and iptables settings as in the previous section.

sudo ip rule add from 192.168.5.5 lookup 100
sudo iptables -t mangle -A PREROUTING -p tcp -s 192.168.5.5 -j TPROXY --on-ip 127.0.0.1 --on-port 22222
sudo iptables -t mangle -A PREROUTING -p udp -s 192.168.5.5 -j TPROXY --on-ip 127.0.0.1 --on-port 22224
Enter fullscreen mode Exit fullscreen mode

(sudo ip route add local default dev lo table 100 is also necessary if not done)
That's all.

We can use fwmark here too, but tproxy-mark is useless, because we have to mark packets before they go into the machine.
A possible setting will be like this:

sudo iptables -t mangle -A OUTPUT -p tcp -s 192.168.5.5 -j MARK --set-mark 0x111
sudo iptables -t mangle -A OUTPUT -p udp -s 192.168.5.5 -j MARK --set-mark 0x111
sudo iptables -t mangle -A PREROUTING -p tcp -m mark --mark 0x111 -j TPROXY --on-ip 127.0.0.1 --on-port 22222
sudo iptables -t mangle -A PREROUTING -p udp -m mark --mark 0x111 -j TPROXY --on-ip 127.0.0.1 --on-port 22224
Enter fullscreen mode Exit fullscreen mode

(sudo ip rule add fwmark 0x111 lookup 100 is also necessary if not done)

This is a tcpdump result for this case.

10:13:38.897424 lo    In  IP 192.168.5.5.48202 > 3.132.228.249.3479: UDP, length 28
10:13:38.897566 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [S], seq 188947714, win 65495, options [mss 65495,sackOK,TS val 841486027 ecr 0,nop,wscale 7], length 0
10:13:38.897596 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.60698: Flags [S.], seq 1277796748, ack 188947715, win 65483, options [mss 65495,sackOK,TS val 3344603128 ecr 841486027,nop,wscale 7], length 0
10:13:38.897623 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [.], ack 1, win 512, options [nop,nop,TS val 841486028 ecr 3344603128], length 0
10:13:38.897722 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [P.], seq 1:4, ack 1, win 512, options [nop,nop,TS val 841486028 ecr 3344603128], length 3
10:13:38.897739 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.60698: Flags [.], ack 4, win 512, options [nop,nop,TS val 3344603128 ecr 841486028], length 0
10:13:38.897955 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.60698: Flags [P.], seq 1:3, ack 4, win 512, options [nop,nop,TS val 3344603128 ecr 841486028], length 2
10:13:38.897977 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [.], ack 3, win 512, options [nop,nop,TS val 841486028 ecr 3344603128], length 0
10:13:38.898047 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [P.], seq 4:14, ack 3, win 512, options [nop,nop,TS val 841486028 ecr 3344603128], length 10
10:13:38.898129 lo    In  IP 127.0.0.3.3130 > 127.0.0.1.60698: Flags [P.], seq 3:13, ack 14, win 512, options [nop,nop,TS val 3344603128 ecr 841486028], length 10
10:13:38.898253 lo    In  IP 127.0.0.1.49384 > 127.0.0.3.53298: UDP, length 38
10:13:38.898317 enp1s0 Out IP 192.168.1.13.52394 > 3.132.228.249.3479: UDP, length 28
10:13:38.939622 lo    In  IP 127.0.0.1.60698 > 127.0.0.3.3130: Flags [.], ack 13, win 512, options [nop,nop,TS val 841486070 ecr 3344603128], length 0
10:13:39.061935 enp1s0 In  IP 3.132.228.249.3479 > 192.168.1.13.52394: UDP, length 68
10:13:39.062134 lo    In  IP 127.0.0.3.53298 > 127.0.0.1.49384: UDP, length 78
10:13:39.062243 lo    In  IP 3.132.228.249.3479 > 192.168.5.5.48202: UDP, length 68
Enter fullscreen mode Exit fullscreen mode

Apply proxy for specific apps

iptables has "uid-owner", "gid-owner" or "cgroup" match. These are useful for sending traffic from specific apps through transparent proxy (or VPN or other NICs).

References

💖 💪 🙅 🚩
enev_turg_f1651983412d040
enev turg

Posted on October 3, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related