В данной статье будет показан пример реализации раздельного туннелирования на домашнем маршрутизаторе. Основная идея заключается в том, чтоб часть трафика шла через удаленный VPS-сервер, на котором развернут Xray + xHTTP, а часть через провайдера. Примеры применения данной технологии оставляю на откуп фантазии читателя, от себя лишь скажу, что статья ни в коем случае не предназначена для обхода каких-либо блокировок и написана исключительно в лабораторных целях. Развертывание Xray на также не является предметом обсуждения в этой статье. Просто предположим, что он у вас есть. В целом будет достаточно даже одного ключа.
В качестве подопытного будет использоваться Keenetic Hopper KN-3810 — добротный гигабитный роутер на АРМ процессоре с прошивкой KeeneticOS 5 версии (да-да, даже не Openwrt). На подопытном будет использоваться Entware/opkg, через которые будет поднят x-ray core. В отличии от sing-box, он поддерживает транспорт xHTTP. Раздельное туннелирование буду делать по принципу DNS-based routing, т.к. у меня нет необходимости маршрутизировать конкретные IP.
Немного терминологии кинетика для тех, кто ранее ничем подобным не занимался: opkg — это пакетный менеджер Openwrt: Entware — своего рода репозиторий opkg, который можно накатить поверх прошивки для запуска доп.софта; Xray core — vpn "клиент", поддерживающие известные протоколы;
Подготовка флешки для Entware/OPKG
Первое, чем нужно заняться, это подготовка флешки, на которую будут установлены Entware/OPKG. Минимальный объем от 2ГБ для базовой установки, но желательно выделить от 8 до 32, чтоб разместить репозиторий пакетов и обновлений без переполнения. Не забываем, что на нее же будут сыпаться кэш, логи и прочий мусор, а также флешка должна выдерживать частые записи/удаления без повреждений. Еще один немаловажный факт, который стоит учесть, что для работы OPKG флешку в роутере придется похоронить. Форматировать следует в FAT32 или EXT4. Keenetic рекомендует использовать формат EXT4, поэтому выбираю его:
Вставляем флешку, определяем командой fdisk -l; В данном случае /dev/sda - моя 8-гиговая флешка;
Pasted-image-20260307194838.png
Размонтируем командой umount /dev/sda1;
Форматируем в EX4 командой mkfs.ext4 -L OPKG /dev/sda1;
С помощью команды fsck -f /dev/sda1 проверяем, что все ок, и извлекаем флешку;
Pasted-image-20260307195732.png
Установка OPKG
Далее необходимо донастроить флешку и установить OPKG на самом Кинетике. Вставляем ее в USB‑порт, и, если всё сделано правильно, роутер сам распознаёт флешку. Заходим на роутер через веб‑интерфейс и переходим в «Компоненты операционной системы». В случае с версией прошивки 5.0.7 необходимо найти и активировать чекбоксы следующих компонентов:
Поддержка открытых пакетов;
Файловая система Ext (для ext4);
Интерфейс USB;
Поддержка USB-накопителей;
Сервер SSH;
Block-mount;
Если не находится block-mount - это ок. В разных версиях он называется по-разному, но почти всегда этот компонент уже активирован по умолчанию. После обновляем KeeneticOS и ждем перезагрузку.
Pasted-image-20260307201358.png
Для установки Entware переходим в раздел меню "OPKG", выбираем флешку в выпадающем списке, включаем доступ для пользователя и сохраняем;
Pasted-image-20260307231045.png
Затем локально скачиваем wget https://bin.entware.net/mipselsf-k3.4/installer/mipsel-installer.tar.gz; В веб-морде переходим к Applications, выбираем флешку, создаем папку install, закидываем скаченный файл;
Pasted-image-20260307235118.png
Дальше уже гарантированно потребуется иметь возможность реализовать SSH-подключение. Для этого заходим сперва через telnet, вводим креды, затем выполняем на роутере:
service ssh
system configuration save
После этого можно ходить по ssh admin@192.168.1.1; Самое время активировать opkg, поэтому выполняем команды:
service opkg
system configuration save
После активации OPKG возвращаемся на веб-морду, снова выбираем флешку, жмем пользователя, жмем сохранить. После этого можно перейти в диагностику и проверить журнал логов на процесс установки. Корректный лог должен выглядеть примерно так:
Pasted-image-20260308000911.png
Возвращаемся к терминалу и переходим в BusyBox Shell с помощью команды exec /opt/bin/sh и скачиваем актуальные индексы пакетов командой /opt/bin/opkg update;
Pasted-image-20260308125600.png
Затем потребуется установить базовый набор пакетов, который нужен для x-ray core:
Указание проксируемых доменов Здесь стоит обратить внимания на директиву "domain", в нее записывается список доменных имен, которые будут проксироваться.
Зачем здесь tun inbound и что он делает Tun инбаунд заставляет xray создать виртуальный TUN-интерфейс xray0. Это обычный линуксовый L3 интерфейс, ядро считает его сетевой картой, куда можно маршрутизировать пакеты. Дальше политика маршрутизации на роутере направит трафик клиентов в этот интерфейс. Таким образом xray становится своего рода прокладкой между LAN и WAN.
Зачем нужен Sniffing для раздельного туннелирования Раздельное туннелирование по доменным именам невозможно на уровне L3/L4 (маршрутизация и iptables видят ip/port, но не видят домен). Поэтому мы заставляем xray снифать SNI в Client Hello, host- заголовок в HTTP и домены в QUIC (udp/443), когда это возможно.
Direct & proxy outbounds У нас 2 выхода: "direct" через провайдера и "proxy" до vps. Важная деталь - direct стоит первым не просто так. Это приоретизация, которая означает, что если правило роутинга по каким-либо причинам не сработает (не распознан домен, нет в списке и т.д.), трафик уйдет через провайдера, а не случайно польется в VPN целиком.
Geoip Google Наличие правила "ip": ["geoip:google"] означает, что любой трафик к Google IP-пулам уходит в туннель даже если домен не снифается. Это может быть полезно для Google-приложений, например, на ТВ, когда мы не знаем, к каким апстримам обращается приложение под капотом. Кроме того, он выполняет роль страховки от случаев, когда домен не получается определить и вместо срабатывания правила приоритета маршрутизации через провайдера, мы гарантированно отправляем Google IP-пулы в VPN. Да, это расширяет объем туннелированного трафика, но делает поведение google-сервисов менее плавающим.
Почему в списке лишние домены Как упомянул ранее, для браузера обычно хватает доменного имени, но приложения ходят на api, в картинки и еще много куда. Если не включать CDN, то приложение может стартовать, но не отдавать корректно контент.
Как трафик попадает в xray Конфиг регулирует только то, как xray делит трафик. Попадание трафика в xray регулируется рядом PBR, об этом более подробно будет описано ниже по тексту.
Первый запуск и проверка интерфейса:
mkdir -p /opt/var/log
killall xray 2>/dev/null
/opt/bin/xray run -c /opt/etc/xray/config.json >/opt/var/log/xray.log 2>&1 &
sleep 1
/opt/sbin/ip link show xray0
tail -n 80 /opt/var/log/xray.log
В случае успеха увидим что-то такое:
Pasted-image-20260309013549.png
Проверяем маршрут по умолчанию:
/opt/sbin/ip -4 route show default
/opt/sbin/ip -4 route show proto kernel scope link
Pasted-image-20260309014004.png
Проверяем, что xray0 в up:
/opt/sbin/ip link show xray0
/opt/sbin/ip -4 route show default
Если отдает какие-либо ошибки, например Device "xray0" does not exist, выполняем скрипт перезапуска:
killall xray 2>/dev/null
mkdir -p /opt/var/log
/opt/bin/xray run -c /opt/etc/xray/config.json >/opt/var/log/xray.log 2>&1 &
sleep 1
/opt/sbin/ip link show xray0 || true
tail -n 120 /opt/var/log/xray.log
Pasted-image-20260309181638.png
Настройка маршрутизации
Дальше начинается самое интересное - как заставить клиентский трафик из LAN проходить через xray0, но при этом не сломать управление роутером, не умереть из-за NAT и фаервола и не чинить руками после каждого рестарта xray.
Почему наивная маршрутизация ломает интернет Самая частая ошибка в гайдах - это попытка маршрутизировать по source-подсети:
ip rule add from 192.168.1.0/24 table 100
ip route add default dev xray0 table 100
Это выглядит логично, но на Keenetic ломает все очень быстро. Причина банальна - сам роутер имеет адрес 192.168.1.1 и тоже попадает в 192.168.1.0/24. В итоге DNS/NTP/системные запросы роутера тоже начинают уходить в туннель. Появляется плавающая сеть, веб-морда зависает, SSH отваливается. Правильным подходом будет маркировать (fwmark) только форвардящийся трафик из LAN, а не трафик самого роутера.
Архитектура решения
Мы строим цепочку:
iptables mangle PREROUTING - помечаем трафик, пришедший из LAN (br0) и идущий не в локальную сеть, меткой 0x1;
ip rule - весь трафик с меткой 0x1 отправляем в таблицу маршрутизации 100;
table 100 - default dev xray0, плюс анти-петля до VPS, чтоб не зациклиться;
Xray получает весь поток в TUN и внутри делит его по следующему принципу: домены из списка в xray, всё остальное в direct. Эта схема позволяет доменный сплит делать на L7 через sniff SNI/host, а L3/L4 оставить системе.
Policy routing: обязательные три правила
Далее пойдёт “эталонный” набор правил. Каждое из них решает конкретную проблему.
Таблица маршрутизации 100 + ip rule по fwmark Сначала создаем table 100 и правило, которое будет направлять маркированный трафик в эту таблицу:
Почему нужен маршрут до VPS в table 100 Это анти-петля, спасающая от того, что трафик до самого VPS-сервера может попытаться уйти в туннель, и получится самопожирающийся цикл.
Это тот нюанс, из-за которого многие решения работают “то да, то нет”, либо вообще не работают. На кинетике по умолчанию есть SNAT/MASQUERADE для LAN. Если не исключить выход в xray0, трафик внутри туннеля может получить не тот src, ответы не вернутся клиенту, и будет ощущение, что всё умерло.
Затем нужно разрешить форвардинг между br0 и xray0. KeeneticOS использует достаточно строгие ACL-цепочки NDM, трафик LAN - tun может быть просто отфильтрован. Поэтому добавляем явные допуски:
Самый безопасный способ не уронить всю сеть - включить маркировку только для одного клиента. В случае каких-либо ошибок отвалится только одно устройство, а не всё, что подключено к вайфаю. Узнаем текущий LAN айпишник, в моем случае это 192.168.1.59, и запускаем скрипт:
Если все в порядке, то при хождении по сайтам счетчик pkts/bytes у MARK будет расти. Если не все в порядке, рекомендую пройти чеклист для локализации проблемы:
Если все проверки успешны (Xray жив, xray0 UP, table 100 содержит default dev xray0, MARK и RETURN присутствуют и их счетчики растут), значит транспортная часть схемы исправна и трафик 100% попадает в xray. В этом случае косяк нужно искать либо в самом xray конфиге, либо в доступности VPS-сервера, либо в особенностях конкретного приложения. Если хотя бы один пункт не выполняется - проблема на найденном слое и дальнейшие действия зависят от того, на каком этапе произошел факап.
Раскатка на всю сеть
Дальше - проще. Если все протестировано и один клиент работает, убираем -s CLIENT_IP и маркируем весь LAN:
Поздравляю, на этом этапе раздельное туннелирование применяется ко всем устройствам в сети!
Почему после перезапуска Xray может сломаться (и как это убрать)
Вы могли заметить странную вещь: все работает... но до первого рестарта xray, после которого интересующие нас ресурсы перестают открываться. Причина не в xray и не в доменах. Причина в одной неочевидной детали работы линукса: xray при рестарте пересоздает интерфейс xray0, маршруты типа default dev xray0 в table 100 могут исчезать (интерфейс то удаляется, то создается заново), а в итоге ip rule и MARK остаются, но table 100 уже не знает куда отправлять трафик.
Именно поэтому мы делаем идемпотентный apply-скрипт + init.d сервис!
cat > /opt/bin/xray-pbr-apply.sh <<'SH'
#!/opt/bin/sh
set -eu
LAN_NET="192.168.1.0/24"
VPS_IP="ваш ip"
MARK="0x1"
TABLE="100"
PRIO="10000"
IP="/opt/sbin/ip"
IPT="/opt/sbin/iptables"
WAN_IF="$($IP -4 route show default | awk '{print $5; exit}')"
WAN_GW="$($IP -4 route show default | awk '{print $3; exit}')"
i=0
while ! $IP link show xray0 >/dev/null 2>&1; do
i=$((i+1))
[ "$i" -ge 20 ] && exit 1
sleep 1
done
if ! $IP rule | grep -q "fwmark $MARK.*lookup $TABLE"; then
$IP rule add fwmark $MARK table $TABLE priority $PRIO
fi
if ! $IP route show table $TABLE | grep -q "^default dev xray0"; then
$IP route replace default dev xray0 table $TABLE
fi
if ! $IP route show table $TABLE | grep -q "^$VPS_IP/32"; then
$IP route replace $VPS_IP/32 via "$WAN_GW" dev "$WAN_IF" table $TABLE
fi
if ! $IPT -t nat -C POSTROUTING -o xray0 -j RETURN 2>/dev/null; then
$IPT -t nat -I POSTROUTING 1 -o xray0 -j RETURN
fi
if ! $IPT -C FORWARD -i br0 -o xray0 -j ACCEPT 2>/dev/null; then
$IPT -I FORWARD 1 -i br0 -o xray0 -j ACCEPT
fi
if ! $IPT -C FORWARD -i xray0 -o br0 -j ACCEPT 2>/dev/null; then
$IPT -I FORWARD 1 -i xray0 -o br0 -j ACCEPT
fi
if ! $IPT -t mangle -C PREROUTING -i br0 ! -d "$LAN_NET" -j MARK --set-mark $MARK 2>/dev/null; then
$IPT -t mangle -I PREROUTING 1 -i br0 ! -d "$LAN_NET" -j MARK --set-mark $MARK
fi
exit 0
SH
chmod +x /opt/bin/xray-pbr-apply.sh
Скрипт проверяет наличие xray0, default dev xray0 в table 100, маршрут до VPS, ip rule fwmark и MARK/NAT/FORWARD правила. Если что-то не так - восстанавливает.
Затем создадим guard, который будет поднимать xray, если он умер, и запускает apply-скрипт, чтобы вернуть маршруты, если что-то сбросилось
cat > /opt/etc/init.d/S99xray <<'SH'
#!/opt/bin/sh
XRAY="/opt/bin/xray"
CFG="/opt/etc/xray/config.json"
LOG="/opt/var/log/xray.log"
PIDF="/opt/var/run/xray.pid"
GUARDPID="/opt/var/run/xray-guard.pid"
start_xray() {
mkdir -p /opt/var/log /opt/var/run
if [ -f "$PIDF" ] && kill -0 "$(cat "$PIDF")" 2>/dev/null; then
return 0
fi
: > "$LOG"
"$XRAY" run -c "$CFG" >>"$LOG" 2>&1 &
echo $! > "$PIDF"
}
stop_xray() {
if [ -f "$PIDF" ]; then
kill "$(cat "$PIDF")" 2>/dev/null || true
rm -f "$PIDF"
else
killall xray 2>/dev/null || true
fi
}
start_guard() {
if [ -f "$GUARDPID" ] && kill -0 "$(cat "$GUARDPID")" 2>/dev/null; then
return 0
fi
(
while true; do
if ! pidof xray >/dev/null 2>&1; then
start_xray
fi
/opt/bin/xray-pbr-apply.sh >/dev/null 2>&1 || true
sleep 15
done
) &
echo $! > "$GUARDPID"
}
stop_guard() {
if [ -f "$GUARDPID" ]; then
kill "$(cat "$GUARDPID")" 2>/dev/null || true
rm -f "$GUARDPID"
fi
}
case "${1:-}" in
start)
start_xray
/opt/bin/xray-pbr-apply.sh || true
start_guard
;;
stop)
stop_guard
stop_xray
;;
restart)
stop_guard
stop_xray
sleep 1
start_xray
/opt/bin/xray-pbr-apply.sh || true
start_guard
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
SH
chmod +x /opt/etc/init.d/S99xray
/opt/etc/init.d/S99xray start
Проверить работу гварда можно сымитировав проблему, например удалить default route из table 100 и посмотреть, восстановится ли он сам через 15-20 секунд.
/opt/sbin/ip route del default table 100
sleep 20
/opt/sbin/ip route show table 100
Итоги и отдельные нюансы, которые стоит упомянуть
Если вдруг у вас в ходу IPv6, часть устройств может уйти по нему мимо VPS, потому что мы строили PBR под IPv4. Самый простое решение это отключить IPv6 в принципе на всех устройствах, т.к. его поддержка - это отдельный гигантский пласт работ.
Единственное, чего не удалось победить - это whatsapp. Судя по всему, он использует еще ряд соединений, которые либо идут на голые ip без sni, либо на какие-то неочевидные домены и порты, и текущим доменным сплитом это надежно не закрыть. В остальном получилась вполне себе надежная система.