Port based routing

February 16, 2021

Recently, 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.

« Editing in vim from anywhere*
AoC 2021 in bash »