В этой статье я попробую рассказать, как в домашней сети создать ещё один шлюз по умолчанию и настроить на нём выборочную маршрутизацию на основе списка подсетей. Используя в качестве такого списка базу данных геолокации IP-адресов, можно перенаправлять трафик в зависимости от страны назначения.
В моём случае все манипуляции проводились на файловом сервере и свелись к следующим шагам: создаём виртуальный интерфейс и список подсетей, настраиваем маршрутизацию, используем этот интерфейс как шлюз по умолчанию для устройств в домашней сети.
Эту статью сложно назвать полноценной инструкцией, но я надеюсь, что не упустил ничего важного.
Шаг 1. Создаем macvlan интерфейс
Сервер доступен по адресу 192.168.0.5/24
через интерфейс enp8s0
— это физический интерфейс сетевой карты.
Идея заключается в том, чтобы настроить на этом физическом интерфейсе два IP-адреса. Сложность состоит в том, что оба адреса должны находиться в одном широковещательном домене (L2-сегменте), а значит, реализовать это придётся на канальном уровне. Один из способов достичь такого поведения — использовать механизм macvlan. Он позволяет создать на одном физическом интерфейсе несколько виртуальных, каждый со своим уникальным MAC-адресом.
$> 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
При помощи macvlan поверх физического интерфейса enp8s0
создадим виртуальный интерфейс mc0
, который будет находиться в том же широковещательном домене. Сетевой адрес 192.168.0.3/24
будет назначаться DHCP-сервером. Добавим флаг UseRoutes=false
, так как маршрут по умолчанию в таблице main
для этого интерфейса не нужен.
/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
В файле конфигурации интерфейса enp8s0
в секцию [Network]
добавляем ссылку на новый интерфейс.
/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
Несмотря на то, что у интерфейсов enp8s0
и mc0
разные MAC-адреса, другие устройства в сети всё равно могут их путать. Это становится серьёзной проблемой, поскольку маршрутизация пакетов будет настроена на основе интерфейсов - пакеты должны приходить на правильный интерфейс, в самом пакете нет информации о том, через какой шлюз он должен идти. Подробно о причинах такого поведения можно прочитать по ссылке.
Чтобы найти причину, давайте посмотрим на ARP-таблицу любого узла в сети. Можно заметить, что ответ на ARP-запрос для одного IP-адреса приходит с двух разных MAC-адресов. Это создаёт состояние гонки: узел не знает, какой именно MAC-адрес считать правильным, и это может меняться от запроса к запросу.
$> 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
Чтобы это исправить изменяем параметры ядра для всех интерфейсов наarp_ignore=1
и arp_announce=2
, описание параметров можно найти тут.
$> 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
Совсем другое дело, теперь можно создать VPN-туннель и перейти к настройке маршрутизации.
Шаг 2. Создаем VPN-туннель
В моём случае используется WireGuard. Про этот протокол написано много подробных руководств, поэтому я приведу лишь пример конфигурационных файлов для networkd
. Шлюзом по умолчанию для этого интерфейса будет 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
Обновление от 29.10.2024:
Выбор VPN-протокола зависит от условий использования. Следует учитывать, что некоторыми провайдерами WireGuard может блокироваться.
Шаг 3. Генерация списка подсетей
Для реализации выборочной маршрутизации необходимо создать хеш-таблицу ipset
с нужными подсетями (в моем случае с российскими). Для трафика, направленного в эти подсети, маршрутизация меняться не будет, а весь остальной трафик будет перенаправлен через VPN-туннель.
Воспользуемся скриптом из этого комментария. Удалив из него пару строк, мы получим готовую команду для генерации хеша с нужными подсетями. Для создания новой хеш-таблицы скрипт достаточно запустить один раз. После этого настройки можно сохранить в файл для последующего использования.
/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.
После перезагрузки системы восстановлением правил ipset
будет заниматься служба 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
Шаг 4. Маркировка и фильтрация трафика
В iptables
необходимо настроить три цепочки: mangle
— для маркировки, nat
— для трансляции адресов и filter
— для фильтрации трафика.
$> cat /etc/iptables/00-iptables.rules
*mangle
# Помечаем весь трафик, пришедший на виртуальный интерфейс mc0, меткой 0x32
-A PREROUTING -i mc0 -j MARK --set-xmark 0x32/0xffffffff
# Помечаем весь трафик, пришедший из VPN-туннеля (wg0), меткой 0x64
-A PREROUTING -i wg0 -j MARK --set-xmark 0x64/0xffffffff
# Помечаем весь трафик для списка подсетей меткой 0x64
-A PREROUTING ! -d 192.168.0.0/16 -i mc0 -m set ! --match-set ru dst -j MARK --set-xmark 0x64/0xffffffff
COMMIT
*filter
# Блокируем весь входящий трафик из VPN, кроме ICMP
-A INPUT -i wg0 ! -p icmp -j DROP
# Разрешаем форвардинг всего трафика с интерфейса mc0
-A FORWARD -i mc0 -j ACCEPT
# Разрешаем форвардинг трафика, связанного с установленными VPN-соединениями
-A FORWARD -i wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
*nat
# Маскируем исходящие пакеты из локальной сети в VPN-туннель
-A POSTROUTING -s 192.168.0.0/24 -o wg0 -j MASQUERADE
COMMIT
Правило в таблице nat
, как можно догадаться, включает MASQUERADE на интерфейсе wg0
для пакетов, отправляемых из локальной сети. Это позволяет скрыть внутреннюю сеть за адресом VPN-клиента и избавляет от необходимости настраивать маршрутизацию на стороне VPN-сервера.
Правила в таблице mangle
добавляют каждому пакету метку (fwmark
), которая используется для принятия решений о маршрутизации на последующих этапах.
Пакеты с меткой
0x32
будут направляться через основной шлюз.Пакеты с меткой
0x64
будут направляться через VPN-туннель.
Логика работы правил следующая (в скобках указан порядковый номер правила):
все, что приходит на интерфейс
wg0
, всегда помечается флагом0x64
(2);пакеты, пришедшие на интерфейс
mc0
, по умолчанию помечаются как0x32
(1), если же адрес назначения находится в хеш-таблице, то сработает следующее правило и метка маршрутизации будет изменена на0x64
(3).
Правила в таблице filter
выполняют две задачи:
Безопасность: запрещаем весь входящий трафик на интерфейсе
wg0
, кроме ICMP (для диагностики связности).Форвардинг: явно разрешаем пересылку пакетов между интерфейсами
mc0
иwg0
(и весь обратный трафик). Если политика цепочкиFORWARD
установлена вACCEPT
, эти правила можно опустить.
Теперь важно убедиться, что маршрутизация IP-пакетов на уровне ядра разрешена.
$> sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1
После перезагрузки восстановлением настроек iptables
будет заниматься служба 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
Шаг 5. Настройка маршрутизации
Перед добавлением новых маршрутов необходимо создать две таблицы маршрутизации: proxy
и no-proxy
. Хотя номера таблиц могут не совпадать со значениями fwmark
, их синхронизация упрощает управление и делает конфигурацию более понятной.
/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 proxy:100
Добавляем новые маршруты в таблицы proxy
и 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
Теперь пакеты с меткой 0x32
будут использовать таблицу маршрутизации no-proxy
, пакеты с меткой 0x64
— proxy
.
Проверяем содержимое таблиц маршрутизации:
$> 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
Выглядит неплохо. Пробуем отправить пакеты через новый шлюз:
$> 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
Теперь всё готово.
Очень надеюсь, что мой опыт окажется полезным для ваших проектов.
Удачи в экспериментах!
Обновление от 22.09.2025:
В статью внесены правки, исправлены замеченные ошибки и технические неточности.