Transparent traffic tunneling with routing based on IP address geolocation

In this article I will try to tell you how to create another default gateway on your home network and configure it for selective routing based on a list of subnets. Using an IP address geolocation database as such a list, you can redirect traffic depending on the destination country.

In my case, all manipulations were carried out on a file server and boiled down to the following steps: create a virtual interface and a list of subnets, configure routing, use this interface as the default gateway for devices on the home network.

This article can hardly be called a complete instruction, I hope I didn’t miss anything important.

Step 1. Create a macvlan interface

The server is available at 192.168.0.5/24 via interface enp8s0.

$> ip address
enp8s0: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 18:c0:4d:65:87:3a brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.5/24 metric 100 brd 192.168.0.255 scope global dynamic enp8s0

$> ip route
default via 192.168.0.1 dev enp8s0 proto dhcp src 192.168.0.5
192.168.0.0/24 dev enp8s0 proto kernel scope link src 192.168.0.5

With the help macvlan on top of the physical interfaceenp8s0 let's create a virtual interface mc0which will be available in the same broadcast domain, network address 192.168.0.3/24 will be assigned by the DHCP server. Let's add a flag UseRoutes=falsedefault route in table main not needed for this interface.

/etc/systemd/network/20-wired-mc0.netdev
[NetDev]
Name=mc0
Kind=macvlan

[MACVLAN]
Mode=bridge
/etc/systemd/network/20-wired-mc0.network
[Match]
Name=mc0

[Network]
DHCP=ipv4

[DHCP]
UseMTU=true
UseRoutes=false

In the interface settings file enp8s0 in section [Network] add a link to the new interface.

/etc/systemd/network/10-wired-enp8s0.network
[Match]
Name=enp8s0

[Network]
DHCP=ipv4
MACVLAN=mc0

[DHCP]
UseMTU=true
$> ip link
enp8s0: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether 18:c0:4d:65:87:3a brd ff:ff:ff:ff:ff:ff
mc0@enp8s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 4a:1a:9c:13:73:ec brd ff:ff:ff:ff:ff:ff

Now for other nodes on the local network the interfaces enp8s0 And mc0 may be indistinguishable from each other, given that packet routing will be configured in the future, including on the basis of interfaces, this can lead to big problems, you can read in detail about the reasons for this behavior here.

If you look at the ARP table on a neighboring node, you will notice that the response to the ARP request comes from two interfaces, in which case a race condition may occur.

$> arp 
Address                  HWtype  HWaddress           Flags Mask 
192.168.0.3              ether   18:c0:4d:65:87:3a   C                     wlan1
192.168.0.5              ether   18:c0:4d:65:87:3a   C                     wlan1

$> tcpdump -l -i wlan1 arp | grep '192.168.0.3'
08:27:10.498966 ARP, Request who-has 192.168.0.3 tell 192.168.0.15
08:27:10.500022 ARP, Reply 192.168.0.3 is-at 18:c0:4d:65:87:3a
08:27:10.500238 ARP, Reply 192.168.0.3 is-at 4a:1a:9c:13:73:ec

To fix this, change the kernel parameters for all interfaces toarp_ignore=1 And arp_announce=2a description of the parameters can be found here.

$> echo "net.ipv4.conf.all.arp_ignore=1" >> /etc/sysctl.conf
$> echo "net.ipv4.conf.all.arp_announce=2" >> /etc/sysctl.conf
$> ip -s -s neigh flush all
$> ping 192.168.0.3
OK
$> ping 192.168.0.5
OK
$> arp -n
Address                  HWtype  HWaddress           Flags Mask 
192.168.0.3              ether   4a:1a:9c:13:73:ec   C                     wlan1
192.168.0.5              ether   18:c0:4d:65:87:3a   C                     wlan1
…
$> tcpdump -l -i wlan1 arp | grep '192.168.0.3'
08:27:46.448933 ARP, Request who-has 192.168.0.3 tell 192.168.0.15
08:27:46.449974 ARP, Reply 192.168.0.3 is-at 4a:1a:9c:13:73:ec 

A completely different matter, now you can create a VPN tunnel and move on to routing.

Step 2. Create a VPN tunnel

In my case, this is WireGuard, quite a lot has been written about it. I will give just an example of configuration files for networkdthe default gateway on this interface 192.168.2.1/24.

$> ip address
wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none 
    inet 192.168.2.6/24 scope global wg0
/etc/systemd/network/30-proxy-wg0.netdev
[NetDev]
Name=wg0
Kind=wireguard
Description=WireGuard tunnel (wg0)

[WireGuard]
ListenPort=<listen port>
PrivateKey=<private key>

[WireGuardPeer]
Endpoint=<host>:<port>
PublicKey=<public key>
PresharedKey=<preshared key>
AllowedIPs=0.0.0.0/0
/etc/systemd/network/30-proxy-wg0.network
[Match]
Name=wg0

[Network]
Address=192.168.2.6/24
DNS=1.1.1.1

Step 3. Generate a list of subnets

To route traffic you need to create ipset hash, in my case with Russian subnets, routing for them will not change, and all other traffic will be redirected to the VPN tunnel.

Let's use the script from this comment, remove a couple of lines and get a ready-made hash with the required subnets. To create a new hash script, you need to run it once, then you can save the settings to a file.

/etc/ipset/create-ipset.sh
#!/usr/bin/env bash

# Description:  Create IPSET to filter full countries for all ports and protocols
# Syntax:       create-ipset.sh countrycode [countrycode] ......
#               Use the standard locale country codes to get the proper IP list. eg.
#               create-ipset.sh cn ru ro
# Note:         To get a sorted list of the inserted IPSet IPs for example China list(cn) run the command:
#               ipset list cn | sort -n -t . -k 1,1 -k 2,2 -k 3,3 -k 4,4
# #############################################################################

# Defining some defaults
tempdir="/tmp"
sourceURL="http://www.ipdeny.com/ipblocks/data/countries/"
#
# Verifying that the program 'ipset' is installed
if ! (dpkg -l | grep '^ii  ipset' &>/dev/null); then
    echo "ERROR: 'ipset' package is not installed and required."
    echo "Please install it with the command 'apt-get install ipset' and start this script again"
    exit 1
fi
[ -e /sbin/ipset ] && ipset="/sbin/ipset" || ipset="/usr/sbin/ipset"
#
# Verifying the number of arguments
if [ $# -lt 1 ]; then
    echo "ERROR: wrong number of arguments. Must be at least one."
    echo "countries_block.bash countrycode [countrycode] ......"
    echo "Use the standard locale country codes to get the proper IP list. eg."
    echo "countries_block.bash cn ru ro"
    exit 2
fi
#
# Now load the rules for blocking each given countries and insert them into IPSet tables
for country; do
    # Read each line of the list and create the IPSet rules
    # Making sure only the valid country codes and lists are loaded
    if wget -q -P $tempdir ${sourceURL}${country}.zone; then
        # Destroy the IPSet list if it exists
        $ipset flush $country &>/dev/null
        # Create the IPSet list name
        echo "Creating and filling the IPSet country list: $country"
        $ipset create $country hash:net &>/dev/null
        (for IP in $(cat $tempdir/${country}.zone); do
            # Create the IPSet rule from each IP in the list
            echo -n "$ipset add $country $IP --exist - "
            $ipset add $country $IP -exist && echo "OK" || echo "FAILED"
        done) >$tempdir/IPSet-rules.${country}.txt
        # Delete the temporary downloaded counties IP lists
        rm $tempdir/${country}.zone
    else
        echo "Argument $country is invalid or not available as country IP list. Skipping"
    fi
done
# Dispaly the number of IP ranges entered in the IPset lists
echo "--------------------------------------"
for country; do
    echo "Number of ip ranges entered in IPset list '$country' : $($ipset list $country | wc -l)"
done
echo "======================================"
#
#eof
$> create-ipset.sh ru
$> ipset test ru ya.ru
213.180.193.56 is in set ru.
$> ipset test ru google.com
64.233.165.102 is NOT in set ru.

After the reboot, the service will restore the settings ipset-persistent.

/etc/systemd/system/ipset-persistent.service
[Unit] 
Description=runs ipset restore on boot
ConditionFileIsExecutable=/etc/ipset/restore-ipset.sh
After=network.target

[Service]
Type=forking
ExecStart=/etc/ipset/restore-ipset.sh
TimeoutSec=0
RemainAfterExit=yes
GuessMainPID=no

[Install]
WantedBy=multi-user.target
/etc/ipset/restore-ipset.sh
#!/usr/bin/env bash

RULES="/etc/ipset/*.rules"
for fname in $RULES; do 
  /usr/bin/flock /run/.ipset-restore /sbin/ipset restore -! < "$fname"
done

Step 4. Traffic marking and filtering

IN iptables three chains will have to be corrected: mangle, nat And filter.

$> cat /etc/iptables/00-iptables.rules
*mangle
-A PREROUTING -i mc0 -j MARK --set-xmark 0x32/0xffffffff
-A PREROUTING -i wg0 -j MARK --set-xmark 0x64/0xffffffff
-A PREROUTING ! -d 192.168.0.0/16 -i mc0 -m set ! --match-set ru dst -j MARK --set-xmark 0x64/0xffffffff
COMMIT
*filter
-A INPUT -i wg0 ! -p icmp -j DROP
-A FORWARD -i mc0 -j ACCEPT
-A FORWARD -i wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
*nat
-A POSTROUTING -s 192.168.0.0/24 -o wg0 -j MASQUERADE
COMMIT

Rule in chain nat as you might guess, enables NAT on the interface wg0 for packets sent from the local network, This will avoid setting up routing on the VPN server.

Rules in the chain mangle each package is added fwmark the label that we will use for routing. Packages with a tag 0x32 we will route through the default gateway, and with the label 0x64 through a VPN tunnel, the rule number in parentheses:

  • everything that comes to the interface wg0always flagged 0x64(2);

  • packets arriving at the interface mc0, by default are marked as 0x32(1)if the destination address is located abroad, then the following rule will work and the routing label will be changed to 0x64(3)in this case the order of the rules matters.

Rules in the chain filter allow packets to be routed between interfaces mc0 And wg0, if politics FORWARD default ACCEPTthen these rules can be skipped. Let's block incoming traffic directly on the interface wg0let's leave only the ICMP protocol.

It is important to ensure that IP routing is enabled at the kernel level.

$> sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

After the reboot, the service will restore the settings iptables-persistent.

/etc/systemd/system/iptables-persistent.service
[Unit] 
Description=runs iptables restore on boot
ConditionFileIsExecutable=/etc/iptables/restore-iptables.sh
After=network.target ipset-persistent.service

[Service]
Type=forking
ExecStart=/etc/iptables/restore-iptables.sh
TimeoutSec=0
RemainAfterExit=yes
GuessMainPID=no

[Install]
WantedBy=multi-user.target
/etc/iptables/restore-iptables.sh
#!/usr/bin/env bash

RULES="/etc/iptables/*.rules"
for fname in $RULES; do 
  /usr/bin/flock /run/.iptables-restore /sbin/iptables-restore -n < $RULES
done
/usr/bin/flock /run/.iptables-restore /etc/iptables/remove-duplicates.sh
/etc/iptables/remove-duplicates.sh
#!/usr/bin/env bash

RULES=$(mktemp)
if [ -f "$RULES" ]; then
  /sbin/iptables-save | awk '/^COMMIT$/ { delete x; }; !x[$0]++' > "$RULES"
  /sbin/iptables-restore "$RULES"
  rm -f "$RULES"
fi

Step 5. Setting up routing

Before adding new routes, let's create two tables: proxy And no-proxy. IP table numbers may not match fwmarkbut it’s more convenient.

/etc/iproute2/rt_tables
#
# reserved values
#
255	local
254	main
253	default
0	unspec
#
# local
#
#1	inr.ruhep
50	no-proxy
100	proxy
/etc/systemd/networkd.conf
[Network]
RouteTable=no-proxy:50
RouteTable=proxy:100

Adding new routes to tables proxy And no-proxy:

/etc/systemd/network/20-wired-mc0.network
[Match]
Name=mc0

[Network]
DHCP=ipv4

[DHCP]
UseMTU=true
UseRoutes=false

[Route]
Destination=192.168.0.0/24
Scope=link
Table=proxy

[Route]
Gateway=192.168.0.1
Table=no-proxy

[Route]
Destination=192.168.0.0/24
Scope=link
Table=no-proxy

[RoutingPolicyRule]
FirewallMark=50
Table=no-proxy
/etc/systemd/network/30-proxy-wg0.network
[Match]
Name=wg0

[Network]
Address=192.168.2.6/24
DNS=1.1.1.1

[Route]
Gateway=192.168.2.1
GatewayOnLink=yes
Table=proxy

[Route]
Destination=192.168.2.0/24
Scope=link
Table=proxy

[RoutingPolicyRule]
FirewallMark=100
Table=proxy

Now the packages are labeled 0x32 must use a table no-proxypackages with a tag 0x64proxy. Checking the contents of routing tables:

$> ip rule
0:		from all lookup local
32764:	from all fwmark 0x64 lookup proxy proto static
32765:	from all fwmark 0x32 lookup no-proxy proto static
32766:	from all lookup main
32767:	from all lookup default
$> ip route show table no-proxy 
default via 192.168.0.1 dev mc0 proto static onlink 
192.168.0.0/24 dev mc0 proto static scope link  
$> ip route show table proxy
default via 192.168.2.1 dev wg0 proto static onlink 
192.168.0.0/24 dev mc0 proto static scope link 
192.168.2.0/24 dev wg0 proto static scope link

Looks good, we try to send packets through the new gateway.

#> ip route
default via 192.168.0.3 dev wlan1
192.168.0.0/24 dev wlan1 proto kernel scope link src 192.168.0.8

$> nping -c 1 --tcp ya.ru 
SENT (0.0546s) TCP 192.168.0.8:55175 > 213.180.193.56:80 S ttl=64 id=65375 iplen=40  seq=1493994850 win=1480 
RCVD (0.0698s) TCP 213.180.193.56:80 > 192.168.0.8:55175 SA ttl=55 id=0 iplen=44  seq=377826229 win=42300 <mss 1410>
Max rtt: 15.046ms | Min rtt: 15.046ms | Avg rtt: 15.046ms

$> nping -c 1 --tcp google.com
SENT (0.0307s) TCP 192.168.0.8:13236 > 64.233.163.100:80 S ttl=64 id=60742 iplen=40  seq=3567496319 win=1480 
RCVD (0.1110s) TCP 64.233.163.100:80 > 192.168.0.8:13236 SA ttl=123 id=0 iplen=44  seq=1559691755 win=65535 <mss 1412>
Max rtt: 80.170ms | Min rtt: 80.170ms | Avg rtt: 80.170ms

Now everything is ready. Thank you!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *