As mentioned in my previous
post, the Reddit widget returned HTTP 403 errors on my
Glance dashboard when it was deployed on a VPS. This is because Reddit
blocks access to its REST API from known VPS subnets.
Is there a way to serve requests from a different IP address? Yes, through an HTTP proxy. Take a look at this cURL command:
curl -x http://203.0.113.20 http://ifconfig.me
The -x flag tells cURL to make this request by first
proxying via the server at the address you specify. GET
requests to ifconfig.me simply return the IP address from
where the request came from. Now, 203.0.113.20 is a
reserved IP address intended for use in documentation (see RFC 5735), but replace
this with an actual proxy and you’ll see that IP address returned.
Remove the -x and its argument and you’ll see your actual
IP address.
Cool! So I could route the Reddit requests through a proxy so they don’t get blocked, but I’ll need the static IP address of a proxy server. If you use a VPN plan and can select servers (I’m using ProtonVPN), that’s one solution. Does that mean that all traffic should be routed through the VPN?
Maybe, but in my particular case, no. Some sites take issue with VPN subnets, and Lobsters seems to be one of them. This brings me to the crux of the issue: I want to selectively route some traffic through a proxy, but send all other traffic directly.
This might be useful in the future, so I’m going to set up a local proxy as well. If I proxy a request through it, the request should ultimately go through my VPN tunnel. If I don’t, the request should bypass the VPN tunnel. To make things easier, I’ll use these IP addresses as a reference for the remainder of this post. Substitute these with your own wherever you see them.
203.0.113.10203.0.113.20I’ll be running a local proxy at 127.0.0.1:8888. If
everything is up correctly, I would expect to see different IP addresses
when executing each of these cURL commands:
curl http://ifconfig.me
# Expect to see the machine's IP address here (203.0.113.10)
curl -x http://127.0.0.1:8888 http://ifconfig.me
# Expect to see the VPN server's IP address here (203.0.113.20)
I’m performing this on Debian 13 and will need to install two
packages: 1. tinyproxy: the local proxy 2.
wireguard: a VPN tunnel
Setting up tinyproxy is simpler so I’ll start there. It should
already have a default configuration file at
/etc/tinyproxy/tinyproxy.conf, but I want to override a few
things:
# Set the port to whatever port you want to expose internally
Port 8888
# Crucially, we only want to route traffic originating internally
# This is for security since the proxy isn't authenticated
Listen 127.0.0.1
Allow 127.0.0.1
# This is optional
# To outsiders, requests won't appear as if they've been proxied
DisableViaHeader YesEven though this configuration only permits local traffic, it’s worth
adding a firewall rule to not expose 8888 externally.
ufw deny 8888
If you’re using ProtonVPN, their docs explain how to generate a WireGaurd configuration file. It’ll spit out something like this:
[Interface]
PrivateKey = <your_private_key>
Address = 10.2.0.2/32
DNS = 10.2.0.1
[Peer]
PublicKey = <your_public_key>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <your_vpn_server_ip_addr>When I first applied this configuration, I was connected to my VPS
over SSH. As written, this will effectively hijack all traffic and stall
the SSH connection. One VPS restart later, I found out that the fix is
simple: add Table = off to the [Interface]
block. Furthermore, I don’t want any changes to DNS, so I remove that
line. Save this file to /etc/wireguard/wg0.conf and apply
the configuration (and a chmod 600 /etc/wireguard/wg0.conf
for the security-conscious).
wg-quick up wg0
Now comes a deep-dive into somewhat esoteric networking. I’ll take this step by step, starting with inspecting our current routing table.
$ ip route show
default via 203.0.113.1 dev enp1s0 proto dhcp src 203.0.113.10 metric 1002
203.0.113.0/23 dev enp1s0 proto dhcp scope link src 203.0.113.10 metric 1002
198.51.100.10 via 203.0.113.1 dev enp1s0 proto dhcp src 203.0.113.10 metric 1002
This shows that all networking traffic is currently going through the
default enp1s0 interface. First, I add a new routing table
named vpn.
mkdir -p /etc/iproute2
echo "200 vpn" | sudo tee -a /etc/iproute2/rt_tables
Then, I add a new default route (which will match all
destinations) on the wg0 interface to the new
vpn table. All network traffic would use this table if it
was the default table, but since it’s separate, it’s currently unused.
There must also be an explicit route for WireGuard to route via the
normal internet. The second route (the route to the VPN endpoint) is
more specific than the default route, so it takes precedence for packets
destined for the VPN server itself and they will still use the physical
interface, bypassing the tunnel. If they were routed through the tunnel,
they’d get stuck in a loop.
ip route add default dev wg0 table vpn
ip route add 203.0.113.20 via 203.0.113.1 dev enp1s0 table vpn
I then create a rule that routes all packets marked 1
(packets will get marked in the next step) should use the
vpn routing table. Marks are just a number attached to
individual packets.
ip rule add fwmark 1 table vpn
This is the magic part where we identify and mark packets. I use the
iptables tool to mangle (modify, which in this
case means marking) packets which are generated by us and egressing (we
append to the OUTPUT chain), but only for packets from
processes running as the tinyproxy user.
iptables -t mangle -A OUTPUT -m owner --uid-owner tinyproxy -j MARK --set-mark 1
Loading the owner module with -m owner
enabled iptables to match packets based on which process
created them. -j MARK is the action that instructs the
command to “jump” to the MARK target.
DNS traffic must be excluded so as not to interfere with DNS lookup. DNS traffic routes through port 53 and should use normal routing.
iptables -t mangle -A OUTPUT -m owner --uid-owner tinyproxy -p udp --dport 53 -j RETURN
iptables -t mangle -A OUTPUT -m owner --uid-owner tinyproxy -p tcp --dport 53 -j RETURN
The final step is to apply network address translation (NAT) to these
packets. The Address field in the WireGuard configuration
file is 10.2.0.2/32, but these packets would show up as
originating from the machine’s IP address.
iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
Here, the NET table is selected (with -t). It will be
applied to the POSTROUTING chain (with -A.
This means NAT will be applied after routing decisions have been made,
but prior to packets leaving). It should only match outgoing packets on
the wg0 interface, and the NAT type should be
MASQUERADE. This NAT type translates the IP addresses to
what the VPN expects, and translates them back in return traffic.
Failing to do this would prevent return traffic from finding its way
back.
Try the cURL command again.
curl -x http://127.0.0.1:8888 http://ifconfig.me
You should see your VPN’s IP address returned!
Wouldn’t it be great if we didn’t have to run all those commands every time we want to enable or disable this VPN tunnel? Just write a shell script.
#!/bin/bash
GATEWAY=$(ip route show default | awk '/default/ {print $3}')
INTERFACE=$(ip route show default | awk '/default/ {print $5}')
VPN_IP_ADDR=$(
awk '/Endpoint/ {split($3, a, ":"); print a[1]}' \
/etc/wireguard/wg0.conf
)
ip route add default dev wg0 table vpn
ip route add $VPN_IP_ADDR via $GATEWAY dev $INTERFACE table vpn
ip rule add fwmark 1 table vpn
IPTABLES_FLAGS="-t mangle -A OUTPUT -m owner --uid-owner tinyproxy"
iptables $IPTABLES_FLAGS -p udp --dport 53 -j RETURN
iptables $IPTABLES_FLAGS -p tcp --dport 53 -j RETURN
iptables $IPTABLES_FLAGS -j MARK --set-mark 1
iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADESave this as setup-routing.sh in
/etc/wireguard and add a cleanup-routing.sh
script.
#!/bin/bash
VPN_IP_ADDR=$(
awk '/Endpoint/ {split($3, a, ":"); print a[1]}' \
/etc/wireguard/wg0.conf
)
ip route del $VPN_IP_ADDR table vpn 2>/dev/null
ip route del default dev wg0 table vpn 2>/dev/null
ip rule del fwmark 1 table vpn 2>/dev/null
IPTABLES_FLAGS="-t mangle -D OUTPUT -m owner --uid-owner tinyproxy"
iptables $IPTABLES_FLAGS -p tcp --dport 53 -j RETURN 2>/dev/null
iptables $IPTABLES_FLAGS -p udp --dport 53 -j RETURN 2>/dev/null
iptables $IPTABLES_FLAGS -j MARK --set-mark 1 2>/dev/null
iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE 2>/dev/nullMark both files as executable. You might this these still need manual
execution, but it gets better. WireGuard configuration files support
PostUp and PreDown fields. Point those to this
script and everything works seamlessly. Your final configuration file
should look like this.
[Interface]
PrivateKey = <your_private_key>
Address = 10.2.0.2/32
Table = off
PostUp = /etc/wireguard/setup-routing.sh
PreDown = /etc/wireguard/cleanup-routing.sh
[Peer]
PublicKey = <your_public_key>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <your_vpn_ip_addr>Enabling and disabling the tunnel is now simple.
wg-quick up wg0
wg-quick down wg0
# And to ensure the tunnel always starts on a reboot
systemctl enable wg-quick@wg0
Voila!
~K