Предисловие
В данной статье будет показан пример реализации раздельного туннелирования на домашнем маршрутизаторе. Основная идея заключается в том, чтоб часть трафика шла через удаленный VPS-сервер, на котором развернут Xray + xHTTP, а часть через провайдера. Примеры применения данной технологии оставляю на откуп фантазии читателя, от себя лишь скажу, что статья ни в коем случае не предназначена для обхода каких-либо блокировок и написана исключительно в лабораторных целях. Развертывание Xray на VPS также не является предметом обсуждения в этой статье. Просто предположим, что он у вас есть. В целом будет достаточно даже одного ключа.
В качестве подопытного будет использоваться 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 и ждем перезагрузку.

Для установки Entware переходим в раздел меню "OPKG", выбираем флешку в выпадающем списке, включаем доступ для пользователя и сохраняем;

Затем локально скачиваем wget https://bin.entware.net/mipselsf-k3.4/installer/mipsel-installer.tar.gz; В веб-морде переходим к Applications, выбираем флешку, создаем папку install, закидываем скаченный файл;

Дальше уже гарантированно потребуется иметь возможность реализовать SSH-подключение. Для этого заходим сперва через telnet, вводим креды, затем выполняем на роутере:
service ssh system configuration save
После этого можно ходить по ssh admin@192.168.1.1;
Самое время активировать opkg, поэтому выполняем команды:
service opkg system configuration save
После активации OPKG возвращаемся на веб-морду, снова выбираем флешку, жмем пользователя, жмем сохранить. После этого можно перейти в диагностику и проверить журнал логов на процесс установки.
Корректный лог должен выглядеть примерно так:

Возвращаемся к терминалу и переходим в BusyBox Shell с помощью команды exec /opt/bin/sh и скачиваем актуальные индексы пакетов командой /opt/bin/opkg update;

Затем потребуется установить базовый набор пакетов, который нужен для x-ray core:
/opt/bin/opkg install ca-certificates ca-bundle /opt/bin/opkg install curl wget /opt/bin/opkg install ip-full /opt/bin/opkg install iptables
Установка X-RAY CORE
Для установки можно использ��вать данный скрипт:
cd /opt/tmp /opt/bin/opkg install unzip /opt/bin/curl -L -o xray.zip \ https://github.com/XTLS/Xray-core/releases/download/v26.2.6/Xray-linux-mips32le.zip rm -rf /opt/tmp/xray && mkdir -p /opt/tmp/xray /opt/bin/unzip -o xray.zip -d /opt/tmp/xray cp -f /opt/tmp/xray/xray /opt/bin/xray chmod +x /opt/bin/xray /opt/bin/xray version
Затем положим датники в рабочее место:
mkdir -p /opt/etc/xray cp -f /opt/tmp/xray/geoip.dat /opt/etc/xray/geoip.dat cp -f /opt/tmp/xray/geosite.dat /opt/etc/xray/geosite.dat cp -f /opt/tmp/xray/geoip.dat /opt/bin/geoip.dat cp -f /opt/tmp/xray/geosite.dat /opt/bin/geosite.dat ls -lh /opt/bin/geoip.dat /opt/bin/geosite.dat
В данном случае они нужны нам только ради того, чтоб в будущем не словить обидную ошибку по датникам.

Сборка конфига
Теперь создадим конфиг. Ниже приложен шаблон моего конфига, при желании его можно скопировать, подставив свои значения из ключа.
mkdir -p /opt/etc/xray
cat > /opt/etc/xray/config.json <<'JSON' { "log": { "loglevel": "warning" }, "inbounds": [ { "tag": "tun-in", "protocol": "tun", "settings": { "name": "xray0", "mtu": 1500 }, "sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"], "routeOnly": true } } ], "outbounds": [ { "tag": "direct", "protocol": "freedom" }, { "tag": "proxy", "protocol": "vless", "settings": { "vnext": [ { "address": "ваш домен", "port": ваш порт xhttp, "users": [ { "id": "ваш UUID", "encryption": "none" } ] } ] }, "streamSettings": { "network": "xhttp", "security": "tls", "tlsSettings": { "serverName": "ваш домен", "alpn": ["h2", "http/1.1"], "fingerprint": "chrome" }, "xhttpSettings": { "host": "ваш домен", "path": "/ваш домен", "mode": "auto" } } } ], "routing": { "domainStrategy": "AsIs", "rules": [ { "type": "field", "ip": ["geoip:google"], "outboundTag": "proxy" }, { "type": "field", "domain": [ "domain:discord.com", "domain:discord.gg", "domain:discordapp.com", "domain:discordapp.net", "domain:discordcdn.com", "domain:discord.media", "domain:soundcloud.com", "domain:sndcdn.com", "domain:chatgpt.com", "domain:openai.com", "domain:oaistatic.com", "domain:oaiusercontent.com", "domain:auth.openai.com", "domain:auth0.openai.com", "domain:whatsapp.com", "domain:whatsapp.net", "domain:whatsapp-cdn.net", "domain:static.whatsapp.net", "domain:cdn.whatsapp.net", "domain:youtube.com", "domain:youtu.be", "domain:googlevideo.com", "domain:ytimg.com", "domain:ggpht.com", "domain:gvt1.com", "domain:gvt2.com", "domain:gvt3.com", "domain:gvt6.com", "domain:youtubei.googleapis.com", "domain:youtube.googleapis.com", "domain:gstatic.com", "domain:googleapis.com", "domain:googleusercontent.com", "domain:accounts.google.com", "domain:clients3.google.com", "domain:clients4.google.com", "domain:google.com", "domain:android.com", "domain:connectivitycheck.android.com", "domain:mtalk.google.com", "domain:x.com", "domain:twitter.com", "domain:t.co", "domain:twimg.com", "domain:facebook.com", "domain:fb.com", "domain:facebook.net", "domain:fbcdn.net", "domain:fbsbx.com", "domain:instagram.com", "domain:cdninstagram.com", "domain:intel.com", "domain:netflix.com", "domain:netflix.net", "domain:fast.com", "domain:nflxext.com", "domain:nflximg.com", "domain:nflximg.net", "domain:nflxsearch.net", "domain:nflxso.net", "domain:nflxvideo.net" ], "outboundTag": "proxy" } ] } }
Указание проксируемых доменов
Здесь стоит обратить внимания на директиву "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
В случае успеха увидим что-то такое:

Проверяем маршрут по умолчанию:
/opt/sbin/ip -4 route show default /opt/sbin/ip -4 route show proto kernel scope link

Проверяем, что 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

Настройка маршрутизации
Дальше начинается самое интересное - как заставить клиентский трафик из 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_IP="ваш ip" WAN_IF=$(/opt/sbin/ip -4 route show default | awk '{print $5; exit}') WAN_GW=$(/opt/sbin/ip -4 route show default | awk '{print $3; exit}') /opt/sbin/ip rule del priority 10000 2>/dev/null || true /opt/sbin/ip route flush table 100 2>/dev/null || true /opt/sbin/ip route replace default dev xray0 table 100 /opt/sbin/ip route replace ${VPS_IP}/32 via ${WAN_GW} dev ${WAN_IF} table 100 /opt/sbin/ip rule add fwmark 0x1 table 100 priority 10000
Почему нужен маршрут до VPS в table 100
Это анти-петля, спасающая от того, что трафик до самого VPS-сервера может попытаться уйти в туннель, и получится самопожирающийся цикл.
Проверка:
/opt/sbin/ip route show table 100 /opt/sbin/ip rule | tail -n 20
NAT-исключение: POSTROUTING RETURN -o xray0
Это тот нюанс, из-за которого многие решения работают “то да, то нет”, либо вообще не работают. На кинетике по умолчанию есть SNAT/MASQUERADE для LAN. Если не исключить выход в xray0, трафик внутри туннеля может получить не тот src, ответы не вернутся клиенту, и будет ощущение, что всё умерло.
Фиксится так:
/opt/sbin/iptables -t nat -D POSTROUTING -o xray0 -j RETURN 2>/dev/null || true /opt/sbin/iptables -t nat -I POSTROUTING 1 -o xray0 -j RETURN
Данное правило должно быть первым в POSTROUTING. Проверка:
/opt/sbin/iptables -t nat -L POSTROUTING -n -v --line-numbers | head -n 10
Затем нужно разрешить форвардинг между br0 и xray0. KeeneticOS использует достаточно строгие ACL-цепочки NDM, трафик LAN - tun может быть просто отфильтрован. Поэтому добавляем явные допуски:
/opt/sbin/iptables -D FORWARD -i br0 -o xray0 -j ACCEPT 2>/dev/null || true /opt/sbin/iptables -D FORWARD -i xray0 -o br0 -j ACCEPT 2>/dev/null || true /opt/sbin/iptables -I FORWARD 1 -i br0 -o xray0 -j ACCEPT /opt/sbin/iptables -I FORWARD 1 -i xray0 -o br0 -j ACCEPT /opt/sbin/iptables -L FORWARD -n -v --line-numbers | head -n 20
Маркировка трафика: тест на одном устройстве
Самый безопасный способ не уронить всю сеть - включить маркировку только для одного клиента. В случае каких-либо ошибок отвалится только одно устройство, а не всё, что подключено к вайфаю. Узнаем текущий LAN айпишник, в моем случае это 192.168.1.59, и запускаем скрипт:
LAN_NET="192.168.1.0/24" CLIENT_IP="192.168.1.59" /opt/sbin/iptables -t mangle -D PREROUTING -i br0 -s ${CLIENT_IP} ! -d ${LAN_NET} -j MARK --set-mark 0x1 2>/dev/null || true /opt/sbin/iptables -t mangle -I PREROUTING 1 -i br0 -s ${CLIENT_IP} ! -d ${LAN_NET} -j MARK --set-mark 0x1 /opt/sbin/iptables -t mangle -L PREROUTING -n -v --line-numbers | head -n 15
Если все в порядке, то при хождении по сайтам счетчик pkts/bytes у MARK будет расти. Если не все в порядке, рекомендую пройти чеклист для локализации проблемы:
Проверка, что xray здравствует и tun есть:
pidof xray /opt/sbin/ip link show xray0
Проверка правил:
/opt/sbin/ip rule | tail -n 30 /opt/sbin/ip route show table 100
Проверка MARK и NAT:
/opt/sbin/iptables -t mangle -L PREROUTING -n -v --line-numbers | head -n 10 /opt/sbin/iptables -t nat -L POSTROUTING -n -v --line-numbers | head -n 10
Если все проверки успешны (Xray жив, xray0 UP, table 100 содержит default dev xray0, MARK и RETURN присутствуют и их счетчики растут), значит транспортная часть схемы исправна и трафик 100% попадает в xray. В этом случае косяк нужно искать либо в самом xray конфиге, либо в доступности VPS-сервера, либо в особенностях конкретного приложения. Если хотя бы один пункт не выполняется - проблема на найденном слое и дальнейшие действия зависят от того, на каком этапе произошел факап.
Раскатка на всю сеть
Дальше - проще. Если все протестировано и один клиент работает, убираем -s CLIENT_IP и маркируем весь LAN:
LAN_NET="192.168.1.0/24" CLIENT_IP="192.168.1.59" /opt/sbin/iptables -t mangle -D PREROUTING -i br0 -s ${CLIENT_IP} ! -d ${LAN_NET} -j MARK --set-mark 0x1 2>/dev/null || true /opt/sbin/iptables -t mangle -D PREROUTING -i br0 ! -d ${LAN_NET} -j MARK --set-mark 0x1 2>/dev/null || true /opt/sbin/iptables -t mangle -I PREROUTING 1 -i br0 ! -d ${LAN_NET} -j MARK --set-mark 0x1
Поздравляю, на этом этапе раздельное туннелирование применяется ко всем устройствам в сети!
Почему после перезапуска 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, либо на какие-то неочевидные домены и порты, и текущим доменным сплитом это надежно не закрыть. В остальном получилась вполне себе надежная система.
