VPN-CA: локальный sing-box TUN и MikroTik policy routing
Настройка локального Linux gateway с sing-box: TUN, Naive outbound для TCP, Hysteria2 для UDP и выборочная маршрутизация MikroTik RouterOS 7.
Проверено на: RouterOS 7.23.5 · AlmaLinux 10.2 · sing-box 1.14.2
Роль локального gateway
Linux VM находится в той же локальной сети, что и MikroTik:
MikroTik 192.168.50.1
Linux gateway 192.168.50.61
MikroTik отправляет на gateway только выбранные назначения. Linux не решает, какие сайты нужно перенаправлять: он получает уже классифицированный трафик и выбирает транспорт по L4 protocol.
TCP -> NaiveProxy
UDP -> Hysteria2
Системная подготовка
Включаем IPv4 forwarding:
cat > /etc/sysctl.d/90-vpngw-forwarding.conf <<'EOF'
net.ipv4.ip_forward = 1
EOF
sysctl --system
Проверка:
sysctl net.ipv4.ip_forward
reverse path filtering
Для policy-routed TUN strict rp_filter на tun0 мешает обратному асимметричному трафику.
Рабочая политика проекта:
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.ens18.rp_filter = 1
net.ipv4.conf.tun0.rp_filter = 0
Файл:
cat > /etc/sysctl.d/99-vpngw-routing.conf <<'EOF'
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.ens18.rp_filter = 1
net.ipv4.conf.tun0.rp_filter = 0
EOF
sysctl --system
Почему нужен default=0: tun0 создаётся sing-box динамически и должен сразу получить правильное значение после restart.
TUN inbound
Упрощённый inbound sing-box:
{
"type": "tun",
"tag": "tun-in",
"interface_name": "tun0",
"address": ["172.31.255.1/30"],
"mtu": 1500,
"auto_route": true,
"auto_redirect": true,
"strict_route": false,
"dns_mode": "disabled",
"include_interface": ["ens18"],
"exclude_uid": [<SING_BOX_UID>],
"route_exclude_address": [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",
"224.0.0.0/4",
"203.0.113.10/32"
]
}
В конкретной топологии strict_route=true приводил к self-interception и timeout. Рабочим оказался strict_route=false вместе с exclude_uid процесса sing-box.
Edge IP обязательно исключается из TUN, иначе outbound может попытаться попасть обратно в самого себя.
TCP outbound: Naive
{
"type": "naive",
"tag": "naive-out",
"server": "203.0.113.10",
"server_port": 443,
"username": "<USER>",
"password": "<PASSWORD>",
"insecure_concurrency": 0,
"tls": {
"enabled": true,
"server_name": "edge.example.net"
}
}
В проекте используется literal IPv4 edge и отдельный TLS server_name. Это исключает нежелательный выбор недоступного IPv6 для самого edge.
UDP outbound: Hysteria2
{
"type": "hysteria2",
"tag": "hy2-out",
"server": "203.0.113.10",
"server_port": 443,
"obfs": {
"type": "salamander",
"password": "<OBFS_PASSWORD>"
},
"password": "<HY2_PASSWORD>",
"network": "udp",
"tls": {
"enabled": true,
"server_name": "edge.example.net"
}
}
Routing внутри sing-box:
{
"rules": [
{
"network": "udp",
"action": "route",
"outbound": "hy2-out"
}
],
"final": "naive-out",
"default_interface": "ens18"
}
Получается простая модель: весь UDP, который уже пришёл в TUN, идёт через Hysteria2; TCP — через Naive.
Проверка конфигурации
Перед restart:
sing-box check -c /etc/sing-box/config.json
После restart:
systemctl restart sing-box
systemctl is-active sing-box
ip -br addr show tun0
Критично повторно проверить rp_filter после пересоздания TUN:
sysctl net.ipv4.conf.default.rp_filter net.ipv4.conf.ens18.rp_filter net.ipv4.conf.tun0.rp_filter
firewalld
В проекте tun0 помещён в отдельную zone, а forwarding между физическим интерфейсом и TUN разрешён policy.
Смысл конфигурации:
ens18 -> public
tun0 -> vpngw-tun
public -> vpngw-tun ACCEPT
vpngw-tun -> public ACCEPT
NAT на Linux gateway для TUN не требуется: sing-box сам завершает proxy connections.
MikroTik routing table
На RouterOS создаётся отдельная FIB table:
/routing/table
add name=to-VPN-EDGE fib
Маршрут:
/ip/route
add dst-address=0.0.0.0/0 routing-table=to-VPN-EDGE gateway=192.168.50.61@main distance=1 comment="VPN-EDGE | default via Linux gateway"
Main table не меняется.
Selected destination routing
Предположим:
CLIENTS -> клиенты, для которых разрешён selective gateway
SELECTED-DST -> адреса сервисов, которым нужен обходной путь
Правила:
/ip/firewall/mangle
add chain=prerouting action=mark-routing new-routing-mark=to-VPN-EDGE passthrough=no protocol=udp src-address-list=CLIENTS dst-address-list=SELECTED-DST connection-mark=no-mark comment="VPN-EDGE | SELECTED DST | UDP"
add chain=prerouting action=mark-routing new-routing-mark=to-VPN-EDGE passthrough=no protocol=tcp src-address-list=CLIENTS dst-address-list=SELECTED-DST connection-mark=no-mark comment="VPN-EDGE | SELECTED DST | TCP"
Всё, что не входит в SELECTED-DST, остаётся в main routing.
Отдельный full-tunnel test host
Для диагностики удобно временно иметь один лабораторный host, весь внешний TCP/UDP которого уходит через gateway.
Для него можно использовать отдельный source list и два mangle rule с условием dst-address-list=!LOCAL_NETS.
Такой тест позволяет проверить Naive и Hysteria2 независимо от наполнения SELECTED-DST.
После валидации full-tunnel test rules лучше не использовать как production policy.
DNS redirect на MikroTik
Если MikroTik принудительно перенаправляет клиентский DNS на себя:
chain=dstnat action=redirect protocol=udp in-interface-list=LAN_VLANS dst-port=53
то full-tunnel тест через внешний DNS может не дойти до Linux gateway.
Для лабораторного source list нужен bypass перед redirect.
Для production selected-destination схемы redirect обычно можно оставить: MikroTik продолжает резолвить домены и наполнять destination address-list.
Старый VPN как standby
Старый VPN лучше не удалять. Достаточно отключить старое правило marking selected destination, чтобы оно не конкурировало с новой policy.
При аварии VPN-EDGE правила можно быстро переключить обратно.
Проверка
TCP egress:
curl -4 -sS --max-time 15 https://api.ipify.org
UDP egress через тестовый full-tunnel host:
dig +time=10 +tries=1 +short @208.67.222.222 myip.opendns.com A
Оба теста должны показать egress IP edge.
Для production selected routing внешний IP обычного сайта должен остаться IP основного WAN.
Дальше
Эксплуатация, поиск неисправностей и rollback: VPN-CA operations.