Port based routing
February 16, 2021Recently, my ISP started blocking outbound ssh connections[*] and that hindered my workflow a lot. The only other internet connection I had was my mobile phone's wireless hotspot (limited data). Somehow, I need to send ONLY the ssh packets through my wireless interface. [skip to script]
(*)
they blocked port 22 and a few others (no deep packet inspection)
The solution
I'm connected to the ISP through my ethernet cable, so my wireless interface was free.
Step 1: Mark packets
We can use iptables to mark the tcp packets that are going through my eth interface (eno1
) that have destination port 22 (ssh runs on 22 by default)
sudo iptables -t mangle -I OUTPUT -o eno1 -p tcp --dport 22 -j MARK --set-mark 1
Step 2: Route packets
Now that we have marked the packets, we need to make sure they go through my wireless interface (wlo1
). For this, we can create a new routing table such that the default gateway is the gateway for my wireless network. (Basically, it means that all packets that use this routing table will go through my wireless network).
sudo ip route add table 22 default via 192.168.0.1
Now, we just need to make sure that all marked packets use this routing table
sudo ip rule add fwmark 0x1 table 22
Step 3: Fix packets
Unfortunately, this setup wouldn't work. The reason is that although those packets will now go through wlo1
, the source IP is messed up (it still would be my IP on eno1
network). This will cause all packets to drop. To fix this, we can do Network Address Translation (NAT)
sudo iptables -t nat -I POSTROUTING -o wlo1 -p tcp --dport 22 -j SNAT --to 192.168.0.2
This basically changes the packets source to use my IP on the wireless network (here, 192.168.0.2
)
Final script
This script takes care of setting up the rules and deleting them when the task is done. It also figures out the gateway and device's IP on the wireless network.
#!/bin/bash
function help {
>&2 echo "Usage: $0 up|down"
exit 1
}
[[ $# == 1 ]] || { help; }
# `iptables -A ...` and `ip route/rule add ...` while running "up"
# `iptables -D ...` and `ip route/rule del ...`while running "down"
case $1 in
up)
ipt="-A"
ipr="add"
;;
down)
ipt="-D"
ipr="del"
;;
*)
help
;;
esac
# get wireless gateway ip
wlo1gw=$( ip r | grep -Po "default via \K(\d+\.?){4} .* wlo1" | cut -d' ' -f1 )
# get my ip on this wireless nw
wlo1ip=$( ip -f inet a show wlo1 | awk '/inet/{ print $2 }' | cut -d/ -f1 )
# any of them empty? ditch
[[ -z $wlo1gw || -z $wlo1ip ]] && { >&2 echo "wlo1 down?"; exit 1; }
# create table which sends via wireless iface
sudo ip route $ipr table 22 default via $wlo1gw
# add rule for marked packets to get routed by the table above
sudo ip rule $ipr fwmark 0x1 table 22
# mark ssh packets which are going out via eth iface
sudo iptables -t mangle $ipt OUTPUT -o eno1 -p tcp --dport 22 -j MARK --set-mark 1
# since im going to change the iface, set source ip to that iface's ip
sudo iptables -t nat $ipt POSTROUTING -o wlo1 -p tcp --dport 22 -j SNAT --to $wlo1ip
This script, along with various other scripts config files can be found in my dotfiles repo.