Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save koolvn/f99ba845e66e6a2643adbd055d5d034b to your computer and use it in GitHub Desktop.

Select an option

Save koolvn/f99ba845e66e6a2643adbd055d5d034b to your computer and use it in GitHub Desktop.
Domain VPN Routing via DNSmasq tagging AsusWRT-Merlin

Как настроить роутинг трафика через VPN по доменному имени

Описание

Подход основан на маркировке пакетов. Далее по этим маркерам роутер будет определять какие пакеты слать через VPN клиента, а какие нет

ВАЖНО:

  1. Клиенты роутера должны использовать адрес роутера как DNS (Дефолтное поведение)
  2. У вас уже должен быть настроен и подключен VPN клиент
  3. У вас включен и настроен доступ к роутеру по ssh
  4. Проверьте, что переменная TABLE_ID=300 в скрипте /jffs/scripts/vpnroute не пересекается с номерами из /etc/iproute2/rt_tables. Измените при необходимости
    cat /etc/iproute2/rt_tables
    100 wan0
    1 wgc1
    2 wgc2
    3 wgc3
    4 wgc4
    5 wgc5
    6 ovpnc1
    7 ovpnc2
    8 ovpnc3
    9 ovpnc4
    10 ovpnc5
    200 wan1

Установка

  1. В файл /jffs/configs/dnsmasq.conf.add прописываем домены, которые хотим открывать через VPN. В примере ниже адреса для Youtube и Jetbrains.

    ipset=/youtube.com/vpnroute
    ipset=/youtu.be/vpnroute
    ipset=/ggpht.com/vpnroute
    ipset=/youtubei.googleapis.com/vpnroute
    ipset=/ytimg.com/vpnroute
    ipset=/googlevideo.com/vpnroute
    ipset=/googleusercontent.com/vpnroute
    ipset=/gvt1.com/vpnroute
    ipset=/gvt2.com/vpnroute
    ipset=/jetbrains.com/vpnroute
    ipset=/jetbrains.ai/vpnroute
    ipset=/intellij.net/vpnroute
    

    Здесь vpnroute должна совпадать с переменной IPSET_NAME из скрипта

  2. Записываем скрипт в файл /jffs/scripts/vpnroute (можно назвать как угодно, главное, чтоб лежал в /jffs/scripts)

  3. Делаем файл исполняемым

    chmod +x /jffs/scripts/vpnroute
  4. Запускаем скрипт

    sh /jffs/scripts/vpnroute
  5. Проверяем, что всё успешно применилось

    Выполняем команды:

    ip rule: Ищите строку вроде 99: from all fwmark 0x55 lookup 300. Она должна быть в списке (выше приоритета 100).

    ip route show table 300: Должно показать:

       ```
       default dev wgc1 scope link
       192.168.0.0/16 dev br0 scope link
       ```
    

    ipset list vpnroute: Пустой список или с заголовком (после теста на youtube.com здесь появятся IP).

    iptables -t mangle -L -v -n: Ищите цепочку VPNROUTE с правилом MARK set 0x55, и в PREROUTING ссылку на неё с -i br0 и match-set vpnroute

  6. Перезапускаем сервис dnsmasq

    service restart_dnsmasq
  7. С устройства в вашей LAN откройте youtube.com в браузере или приложении

  8. Ставим скрипт в автозапуск после применения всех настроек NAT. Дописываем в конец файла /jffs/scripts/wgclient-start следующее:

    /jffs/scripts/vpnroute
  9. Делаем скрипт исполняемым chmod +x /jffs/scripts/wgclient-start

  10. Каждый раз, когда хотим добавить новый домен, прописываем его в /jffs/configs/dnsmasq.conf.add и выполняем команду

    service restart_dnsmasq
#!/bin/sh
# === Configuration ===
readonly VPN_IF="wgc3"
readonly LOGGER_NAME="Domain VPN Routing"
readonly LOG_LEVEL="user.notice"
readonly LAN_SUBNET="192.168.0.0/16"
readonly IPSET_NAME="vpnroute"
readonly IPSET_MAXELEM="10000"
readonly TABLE_ID="300"
readonly FWMARK="0x55"
readonly FWMARK_PRIORITY="15000"
readonly LAN_IF="br0"
readonly IPTABLES_CHAIN="VPNROUTE"
# === Argument Checks ===
if [ -n "$1" ] && [ "${1}" != "${VPN_IF##*[!0-9]}" ]; then
logger -t "$LOGGER_NAME" -p "$LOG_LEVEL" "Skipping run for interface '${VPN_IF%%[0-9]*}$1', exiting."
exit 0
fi
if ! ip link show "$VPN_IF" | grep -q "UP"; then
logger -t "$LOGGER_NAME" -p "$LOG_LEVEL" "Interface $VPN_IF is not UP. Exiting."
exit 1
fi
# Enable debug
#set -x
log_msg() {
logger -sc -t "${LOGGER_NAME}" -p "${LOG_LEVEL}" "$1"
}
log_msg "Starting VPN routing setup for $VPN_IF..."
# 1. Reverse Path Filter Fix
echo 0 > "/proc/sys/net/ipv4/conf/$VPN_IF/rp_filter" 2>/dev/null
# 2. Create IPSet
ipset_exists=0
if ipset -L -n "${IPSET_NAME}" >/dev/null 2>&1; then
ipset_exists=1
fi
ipset create "${IPSET_NAME}" hash:ip maxelem "${IPSET_MAXELEM}" -exist
# 3. Clean up Routing Rules
log_msg "Cleaning up old routing rules..."
while ip rule show | grep -q "fwmark ${FWMARK}"; do
ip rule del fwmark "${FWMARK}" table "${TABLE_ID}"
done
ip route flush table "${TABLE_ID}" 2>/dev/null
# 4. Iptables Setup (MARKING)
log_msg "Configuring iptables marking..."
iptables -t mangle -N "${IPTABLES_CHAIN}" 2>/dev/null
iptables -t mangle -F "${IPTABLES_CHAIN}"
# Mark the packet
iptables -t mangle -A "${IPTABLES_CHAIN}" -j MARK --set-mark "${FWMARK}"
# Link Chain to PREROUTING
if ! iptables -t mangle -C PREROUTING -i "${LAN_IF}" -m set --match-set "${IPSET_NAME}" dst -j "${IPTABLES_CHAIN}" 2>/dev/null; then
iptables -t mangle -A PREROUTING -i "${LAN_IF}" -m set --match-set "${IPSET_NAME}" dst -j "${IPTABLES_CHAIN}"
fi
# 5. Iptables Setup (MSS CLAMPING)
log_msg "Verifying MSS Clamping rule..."
MSS_ARGS="-o ${VPN_IF} -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu"
if ! iptables -t mangle -C FORWARD $MSS_ARGS 2>/dev/null; then
iptables -t mangle -A FORWARD $MSS_ARGS
log_msg "MSS Clamping rule added."
else
log_msg "MSS Clamping rule already exists."
fi
# 6. Policy Routing
log_msg "Adding routing rules and routes..."
ip rule add prio "${FWMARK_PRIORITY}" fwmark "${FWMARK}" table "${TABLE_ID}"
if [ -n "${LAN_SUBNET}" ]; then
ip route add "${LAN_SUBNET}" dev "${LAN_IF}" table "${TABLE_ID}"
fi
ip route add default dev "${VPN_IF}" table "${TABLE_ID}"
ip route add prohibit default table "${TABLE_ID}" metric 200 2>/dev/null
# 7. Finalize
log_msg "Flushing route cache..."
ip route flush cache 2>/dev/null
log_msg "VPN routing applied successfully."
if [ "$ipset_exists" -eq 0 ] && pidof dnsmasq > /dev/null; then
log_msg "New IPSet created. Restarting dnsmasq..."
service restart_dnsmasq
else
log_msg "IPSet already existed. Skipping dnsmasq restart."
fi
#!/bin/sh
# === Configuration ===
# This section contains all the variables you might want to change.
# The VPN tunnel interface (e.g., wgc1, wg0, tun0).
readonly VPN_IF="wgc1"
readonly LOGGER_NAME="Domain VPN Routing"
readonly LOG_LEVEL=user.notice
# The routing table ID to use for VPN traffic.
readonly TABLE_ID="300"
# You should leave the following AS IS unless you know what you are doing.
# Your local network subnet.
# This is to ensure local traffic does not go through the VPN.
readonly LAN_SUBNET="192.168.0.0/16"
# The name of the ipset that will contain the destination IPs for the VPN.
readonly IPSET_NAME="vpnroute"
# The maximum number of elements in the ipset.
readonly IPSET_MAXELEM="10000"
# The firewall mark to identify VPN-bound packets.
readonly FWMARK="0x55"
# The priority of the routing rule.
readonly FWMARK_PRIORITY="99"
# The LAN interface.
readonly LAN_IF="br0"
# The name for the custom iptables chain.
readonly IPTABLES_CHAIN="VPNROUTE"
# === Script Execution ===
# Exit immediately if a command exits with a non-zero status.
set -e
logger -sc -t "${LOGGER_NAME}" -p ${LOG_LEVEL} "Starting VPN routing setup..."
# 1. Create the ipset if it doesn't already exist.
# This set will hold the destination IPs that need to be routed through the VPN.
logger -sc -t "${LOGGER_NAME}" -p ${LOG_LEVEL} "Creating ipset '${IPSET_NAME}'..."
ipset create "${IPSET_NAME}" hash:ip maxelem "${IPSET_MAXELEM}" -exist
# 2. Clean up previous routing rules and table.
# This ensures that we start from a clean state every time the script is run.
# Errors are suppressed in case the rules or routes don't exist.
logger -sc -t "${LOGGER_NAME}" -p ${LOG_LEVEL} "Cleaning up old routing rules and routes..."
ip rule del fwmark "${FWMARK}" table "${TABLE_ID}" 2>/dev/null || true
ip route flush table "${TABLE_ID}" 2>/dev/null || true
# 3. Set up iptables for packet marking.
logger -sc -t "${LOGGER_NAME}" -p ${LOG_LEVEL} "Setting up iptables..."
# Create a new custom chain for our VPN logic if it doesn't exist.
iptables -t mangle -N "${IPTABLES_CHAIN}" 2>/dev/null || true
# Flush the custom chain to remove any old rules.
iptables -t mangle -F "${IPTABLES_CHAIN}"
# Delete any old rule in PREROUTING to avoid duplicates.
iptables -t mangle -D PREROUTING -i "${LAN_IF}" -m set --match-set "${IPSET_NAME}" dst -j "${IPTABLES_CHAIN}" 2>/dev/null || true
# Create a rule to jump to our custom chain for traffic from LAN that matches the destination ipset.
iptables -t mangle -A PREROUTING -i "${LAN_IF}" -m set --match-set "${IPSET_NAME}" dst -j "${IPTABLES_CHAIN}"
# In our custom chain, mark the packets so the routing policy can identify them.
iptables -t mangle -A "${IPTABLES_CHAIN}" -j MARK --set-mark "${FWMARK}"
# 4. Configure policy-based routing.
logger -sc -t "${LOGGER_NAME}" -p ${LOG_LEVEL} "Adding new routing rule and routes..."
# Add a rule to direct all packets with our mark to use our custom routing table.
ip rule add fwmark "${FWMARK}" table "${TABLE_ID}" priority "${FWMARK_PRIORITY}"
# 5. Populate the custom routing table.
# Add a default route to this table to send all traffic through the VPN interface.
ip route add default dev "${VPN_IF}" table "${TABLE_ID}"
# Add a route for the local network to go through the regular LAN interface.
# This prevents local traffic from being sent over the VPN.
ip route add "${LAN_SUBNET}" dev "${LAN_IF}" table "${TABLE_ID}"
# 6. Flush the route cache to apply changes immediately.
logger -sc -t "${LOGGER_NAME}" -p ${LOG_LEVEL} "Flushing route cache..."
ip route flush cache
logger -sc -t "${LOGGER_NAME}" -p ${LOG_LEVEL} "VPN routing rules applied successfully."
@AirAlarm
Copy link

AirAlarm commented Sep 2, 2025

Клиенты роутера должны использовать адрес роутера как DNS (Дефолтное поведение)

А если в локальной сети крутится Pihole + unbound для резолвинга, и клиенты роутера стучатся в него?

@koolvn
Copy link
Author

koolvn commented Sep 3, 2025

@AirAlarm Тут подсказать не смогу, но кажется, что подобную схему можно тоже организовать. Главное - это маркировать трафик и чтоб роутер трафик с этими маркерами пускал через впн. Надо разбираться короче говоря)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment