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 mc0
which 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=false
default 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=2
a 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 networkd
the 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
wg0
always flagged0x64
(2);packets arriving at the interface
mc0
, by default are marked as0x32
(1)if the destination address is located abroad, then the following rule will work and the routing label will be changed to0x64
(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 ACCEPT
then these rules can be skipped. Let's block incoming traffic directly on the interface wg0
let'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 fwmark
but 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-proxy
packages with a tag 0x64
— proxy
. 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!