This guide was created by a community member and reviewed by us. Firmware and router interfaces may change over time, so some steps may differ depending on your device or version.
OpenWrt with sing-box
sing-box is a capable proxy client that supports stealth protocols like VLESS,
Hysteria2, Shadowsocks, Trojan, and VMess, any of which can be used with a Xeovo subscription.
We use Hysteria2 throughout this guide because it is fast and simple to configure, but the protocol is the one part you can freely swap. Everything else — the VLANs, the kill switch, the DNS, the routing — is identical whichever you pick. Using a different protocol at the end shows the extraction commands and the matching sing-box outbound for each.
In this guide we set up one router with two client networks: a full-tunnel VLAN
where everything exits through Xeovo, and a family VLAN where only the traffic
you choose is tunneled and the rest exits normally. The full-tunnel VLAN has a kill switch: if sing-box goes down or crashes, it stops passing traffic instead of leaking to a censor.
OpenWrt itself keeps reaching the internet directly. Only the client networks go through sing-box. That keeps the setup simple and means a mistake in the proxy config never locks you out of the router. There are no third-party dependencies: stock OpenWrt, the sing-box binary, and a one-line shell command to read your subscription.
Prerequisites
- An OpenWrt 25.12.4 VM or physical router with at least 128 MB of flash. On
x86, give it a 500 MB or larger disk. - A Xeovo subscription.
- A machine on the same network with
ssh,scp,curl, and a browser. - Being comfortable on a command line.
Note on space. sing-box needs about 50 MB. Check with
df -h /overlay.
Single-image targets like x86 ext4 ship a small root filesystem, so grow it
before you rebuild withowut(install it withapk add owutif it is
missing):# run on the router uci set attendedsysupgrade.owut=owut uci set attendedsysupgrade.owut.rootfs_size='1024' uci commit attendedsysupgradeThe next
owut upgradethen builds a 1024 MB root. On x86 this resize
wipes your config,
so do it before configuring anything below. Back up withsysupgradeand copy it off the router regardless.
--create-backup /tmp/backup.tar.gz
Step 1: Get your endpoints
In the Xeovo dashboard, open Stealth proxy, pick the Linux / Throne
profile, and copy the subscription link from the link icon. That link returns a
plain list of proxy URIs, one per line.
You only need the Hysteria2 entries, and from each one the server IP:port, the
SNI hostname, and the password. The password is the same on every endpoint.
# run on any machine
curl -fsS 'https://YOUR-SUBSCRIPTION-URL/plain/config/' \
| tr -d '\r' \
| sed -nE 's#^hysteria2://([^@]+)@([^:/?#]+):([0-9]+)/\?sni=([^&#]+).*#server=\2 port=\3 sni=\4 password=\1#p'
The output looks like this:
server=x.x.x.x port=443 sni=de-hysteria2.xeovo.net password=****
server=x.x.x.x port=443 sni=fr-hysteria1.xeovo.net password=****
server=x.x.x.x port=443 sni=ch-hysteria1.xeovo.net password=****
Pick one endpoint for the full-tunnel VLAN and one for the family VLAN, and note
the server, SNI, and password. This guide uses Germany for the full-tunnel
VLAN and France for the family VLAN.
Step 2: Install sing-box
First find your architecture:
# run on the router
. /etc/os-release; echo "$OPENWRT_ARCH"
Open the sing-box releases page
and find the newest version marked Latest (the green tag). Download the
.apk whose name ends in openwrt_<your-arch>.apk. The example below is for
aarch64_cortex-a53; swap in your arch and the current version:
# run on the router
mkdir -p /etc/sing-box
wget -O /tmp/sing-box.apk \
https://github.com/SagerNet/sing-box/releases/download/v1.13.13/sing-box_1.13.13_openwrt_aarch64_cortex-a53.apk
apk add kmod-tun kmod-nft-queue kmod-inet-diag
apk add --allow-untrusted /tmp/sing-box.apk
--allow-untrusted is required because the upstream .apk is not signed with a
key in OpenWrt's keyring.
If your ISP blocks GitHub, download the same file on a machine that can reach
it, then copy it across and install from the local copy:
# on a machine that can reach GitHub:
scp sing-box.apk root@192.168.1.1:/tmp/sing-box.apk
# on the router:
apk add kmod-tun kmod-nft-queue kmod-inet-diag
apk add --allow-untrusted /tmp/sing-box.apk
Step 3: The sing-box config
This config is easier to edit and maintain on your own machine, so write it
there and copy it over with scp. Fill in the endpoint details from step 1.
What to change: FAMILY_IP / FAMILY_SNI for your family endpoint,
FULL_IP / FULL_SNI for your full-tunnel endpoint, YOUR_PASSWORD in both
outbounds, and default_interface if your WAN is not eth0.
{
"log": { "level": "info", "timestamp": true },
"experimental": { "cache_file": { "enabled": true } },
"dns": {
"servers": [
{
"tag": "proxy-dns",
"type": "https",
"server": "1.1.1.1",
"server_port": 443,
"tls": { "enabled": true, "server_name": "cloudflare-dns.com" },
"detour": "family-hy2"
},
{ "tag": "bootstrap-dns", "type": "udp", "server": "1.1.1.1" }
],
"final": "proxy-dns",
"strategy": "ipv4_only"
},
"inbounds": [
{ "type": "direct", "tag": "dns-in", "listen": "127.0.0.1", "listen_port": 5353, "network": "udp" },
{
"type": "tun", "tag": "tun-in",
"address": ["172.19.0.1/30"], "mtu": 1500,
"auto_route": true, "auto_redirect": true, "strict_route": false,
"include_interface": ["br-lan.50", "br-lan.70"]
}
],
"outbounds": [
{
"type": "hysteria2", "tag": "family-hy2",
"server": "FAMILY_IP", "server_port": 443,
"password": "YOUR_PASSWORD",
"tls": { "enabled": true, "server_name": "FAMILY_SNI" }
},
{
"type": "hysteria2", "tag": "full-hy2",
"server": "FULL_IP", "server_port": 443,
"password": "YOUR_PASSWORD",
"tls": { "enabled": true, "server_name": "FULL_SNI" }
},
{ "type": "direct", "tag": "direct" },
{ "type": "block", "tag": "block" }
],
"route": {
"rules": [
{ "action": "sniff" },
{ "protocol": "dns", "action": "hijack-dns" },
{ "ip_is_private": true, "action": "route", "outbound": "direct" },
{ "source_ip_cidr": ["192.168.70.0/24"], "action": "route", "outbound": "full-hy2" },
{
"type": "logical", "mode": "and",
"rules": [
{ "source_ip_cidr": ["192.168.50.0/24"] },
{ "domain": ["am.i.mullvad.net"] }
],
"action": "route", "outbound": "family-hy2"
},
{ "source_ip_cidr": ["192.168.50.0/24"], "port": [853], "action": "route", "outbound": "family-hy2" }
],
"default_interface": "eth0",
"default_domain_resolver": "bootstrap-dns",
"final": "direct"
}
}
How it works:
- The
tuninbound captures traffic frombr-lan.50andbr-lan.70.
auto_routeandauto_redirectprogram the kernel and fw4 so that captured
traffic is handed to sing-box without any manual firewall rules. - The full-tunnel rule routes everything from
192.168.70.0/24tofull-hy2. - The family probe rule routes
am.i.mullvad.netfrom192.168.50.0/24to
family-hy2, while everything else routes tofinal: directunless a
later family rule matches. - The
dnsblock routes client lookups to Cloudflare (1.1.1.1over HTTPS,
cloudflare-dns.com) throughfamily-hy2, so DNS resolves inside the tunnel
instead of leaking to your ISP's resolver.bootstrap-dnsis plain1.1.1.1
and is used only at startup, to resolve the Hysteria2 server addresses before
the tunnel exists. default_interfacemust be your WAN device so the Hysteria2 outbounds dial out
over it and do not loop back into the tun.
Don't copy it over yet. If you want to add policy-based routing to chosen
domains/addresses, see the optional sections below. Then copy it in step 4.
Optional: filter DNS content
proxy-dns above points at Cloudflare. By changing the server and the SNI, you
can point it at a filtered resolver that blocks ads instead. We'll use Mullvad,
which offers several free options (docs):
adblock.dns.mullvad.netblocks ads and trackers.family.dns.mullvad.netadds adult content and gambling.all.dns.mullvad.netblocks everything on their lists.
"server": "194.242.2.2",
"server_name": "adblock.dns.mullvad.net"
This resolver applies to every client on the router, so be careful with the
stricter levels. A broad block list will silently break sites for everyone, and
the breakage is hard to trace back to DNS. adblock is the safe default.
Optional: route by domain lists (policy-based routing)
Instead of listing domains by hand, you can pull curated lists. The
itdoginfo/allow-domains project
publishes ready-made .srs rule sets. Each list is one rule_set entry in
route pointing at its .srs, plus a rule that references it:
"rule_set": [
{
"type": "remote", "tag": "youtube", "format": "binary",
"url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/youtube.srs",
"download_detour": "family-hy2", "update_interval": "1h"
}
],
"rules": [
{ "rule_set": ["youtube"], "source_ip_cidr": ["192.168.50.0/24"], "action": "route", "outbound": "family-hy2" }
]
The rule matches family-VLAN connections whose destination domain is in the
youtube rule set and routes them via family-hy2; anything that doesn't match
falls through to final: direct. download_detour makes sing-box fetch the
.srs through the tunnel, so updates still work when GitHub is blocked.
Step 4: Start the service and enable autostart
# run on your PC
scp config.json root@192.168.1.1:/etc/sing-box/config.json
# run on the router
sing-box check -c /etc/sing-box/config.json
service sing-box enable
service sing-box start
logread -e sing-box | tail -40
Reboot to load the tun kmod, then reconnect on the stock LAN at 192.168.1.1
and confirm it auto-started:
# run on the router
reboot
# run on the router
logread -e sing-box | tail -40
From here on you control the service with:
# run on the router
service sing-box restart
service sing-box stop
service sing-box start
Whenever you change config.json, sing-box check -c /etc/sing-box/config.json
then service sing-box restart to apply it.
Step 5: Network
Now set up a WAN interface on eth0 and a VLAN-aware bridge on eth1 carrying
VLAN 50 (family) tagged and VLAN 70 (full tunnel) untagged. This way an
unconfigured client, like your PC, lands in the full-tunnel network with no
extra setup; a downstream AP or switch can still carry the family VLAN with
tags.
Interface names vary by device, so list yours first:
# run on the router
ip a
A fresh OpenWrt comes with a lan interface on a br-lan bridge plus a wan
interface. The commands below clear those defaults so our bridge is the only
one, then rebuild it with VLAN filtering. Adjust eth0 and eth1 to match your
hardware.
# run on the router
# clear the stock LAN bridge so our definition is the only one
uci -q delete network.lan
uci -q delete network.lan6
while uci -q delete network.@device[0]; do :; done
uci set network.wan=interface
uci set network.wan.device='eth0'
uci set network.wan.proto='dhcp'
uci set network.wan.ipv6='0'
uci set network.br_lan=device
uci set network.br_lan.name='br-lan'
uci set network.br_lan.type='bridge'
uci add_list network.br_lan.ports='eth1'
uci set network.br_lan.vlan_filtering='1'
uci add network bridge-vlan
uci set network.@bridge-vlan[-1].device='br-lan'
uci set network.@bridge-vlan[-1].vlan='50'
uci add_list network.@bridge-vlan[-1].ports='eth1:t'
uci add network bridge-vlan
uci set network.@bridge-vlan[-1].device='br-lan'
uci set network.@bridge-vlan[-1].vlan='70'
uci add_list network.@bridge-vlan[-1].ports='eth1:u*'
uci set network.family=interface
uci set network.family.device='br-lan.50'
uci set network.family.proto='static'
uci set network.family.ipaddr='192.168.50.1'
uci set network.family.netmask='255.255.255.0'
uci set network.family.ipv6='0'
uci set network.vpn=interface
uci set network.vpn.device='br-lan.70'
uci set network.vpn.proto='static'
uci set network.vpn.ipaddr='192.168.70.1'
uci set network.vpn.netmask='255.255.255.0'
uci set network.vpn.ipv6='0'
uci commit network
eth1:u* on VLAN 70 makes VLAN 70 the untagged PVID for the wired port. If you
want eth1 to be a pure trunk instead, change that line back to eth1:t and
make sure the downstream device tags VLAN 70; a normal untagged device will not
get DHCP from a tagged-only port.
We turn IPv6 off everywhere — on the WAN (option ipv6 '0' above) and on both
client interfaces — to keep this guide simple for people who don't want to deal
with IPv6. sing-box can tunnel it, but a misconfigured IPv6 setup leaks: clients
keep reaching the internet over v6 outside the tunnel while v4 looks fine. With
IPv6 off there's nothing to leak.
Step 6: DNS
We'll point dnsmasq at sing-box. sing-box listens on
127.0.0.1:5353 and forwards client queries through the tunnel:
# run on the router
uci set dhcp.@dnsmasq[0].noresolv='1'
uci set dhcp.@dnsmasq[0].localuse='0'
uci delete dhcp.@dnsmasq[0].server
uci add_list dhcp.@dnsmasq[0].server='127.0.0.1#5353'
uci set dhcp.family=dhcp
uci set dhcp.family.interface='family'
uci set dhcp.family.start='100'
uci set dhcp.family.limit='100'
uci set dhcp.family.leasetime='12h'
uci set dhcp.family.dhcpv4='server'
uci set dhcp.family.dhcpv6='disabled'
uci set dhcp.family.ra='disabled'
uci set dhcp.vpn=dhcp
uci set dhcp.vpn.interface='vpn'
uci set dhcp.vpn.start='100'
uci set dhcp.vpn.limit='100'
uci set dhcp.vpn.leasetime='12h'
uci set dhcp.vpn.dhcpv4='server'
uci set dhcp.vpn.dhcpv6='disabled'
uci set dhcp.vpn.ra='disabled'
uci set dhcp.wan=dhcp
uci set dhcp.wan.interface='wan'
uci set dhcp.wan.ignore='1'
uci commit dhcp
localuse '0' keeps the router itself resolving over its WAN, which it needs
before sing-box is up to install packages. Only the client VLANs are forced
through the in-tunnel resolver.
Step 7: Firewall and kill switch
Two client zones. The family zone forwards to WAN, so its direct traffic works.
The vpn zone has no forwarding to WAN at all, plus an explicit DROP rule. If
sing-box dies or crashes, full-tunnel clients lose internet rather than leak.
# run on the router
# Family VLAN: allowed to reach WAN directly
uci set firewall.family=zone
uci set firewall.family.name='family'
uci set firewall.family.input='ACCEPT'
uci set firewall.family.output='ACCEPT'
uci set firewall.family.forward='REJECT'
uci add_list firewall.family.network='family'
uci set firewall.family_wan=forwarding
uci set firewall.family_wan.src='family'
uci set firewall.family_wan.dest='wan'
# Full-tunnel VLAN: never forwarded to WAN
uci set firewall.vpn=zone
uci set firewall.vpn.name='vpn'
uci set firewall.vpn.input='ACCEPT'
uci set firewall.vpn.output='ACCEPT'
uci set firewall.vpn.forward='REJECT'
uci add_list firewall.vpn.network='vpn'
# Kill switch: drop anything from the full-tunnel VLAN to WAN
uci set firewall.ks_vpn=rule
uci set firewall.ks_vpn.name='Killswitch-vpn'
uci set firewall.ks_vpn.src='vpn'
uci set firewall.ks_vpn.dest='wan'
uci set firewall.ks_vpn.proto='all'
uci set firewall.ks_vpn.target='DROP'
uci commit firewall
Both zones keep input ACCEPT, the same as a normal LAN. Clients can reach the
router's own services (DHCP, DNS, LuCI), and sing-box's redirected traffic is
accepted without any extra firewall rule.
Optional: serve a VLAN over Wi-Fi
So far everything runs over the wired port: untagged traffic lands in vpn,
tagged VLAN 50 in family. A router with Wi-Fi can serve both networks as two
SSIDs, one per network. Each SSID joins one OpenWrt network and inherits its
firewall zone. Both SSIDs share one channel since they're on the same radio. On
auto some routers won't bring up the second one, so pick a fixed channel if
that happens.
The snippet below is minimal: fill in your own SSIDs, keys, and radio name. On
each iface, the network line is what does the VLAN and zone assignment. You
can also set this up from the Web UI.
# run on the router
# full-tunnel SSID -> vpn VLAN, behind the kill switch
uci set wireless.full=wifi-iface
uci set wireless.full.device='radio0' # your radio; check `uci show wireless`
uci set wireless.full.mode='ap'
uci set wireless.full.ssid='full-tunnel'
uci set wireless.full.encryption='sae'
uci set wireless.full.key='CHANGE_ME'
uci set wireless.full.network='vpn' # joins vpn, inherits the vpn zone
# family SSID -> family VLAN, split-tunnel
uci set wireless.fam=wifi-iface
uci set wireless.fam.device='radio0' # same radio, so same channel
uci set wireless.fam.mode='ap'
uci set wireless.fam.ssid='family'
uci set wireless.fam.encryption='sae'
uci set wireless.fam.key='CHANGE_ME'
uci set wireless.fam.network='family' # joins family, inherits the family zone
# a fresh OpenWrt leaves the radio disabled
uci set wireless.radio0.disabled='0'
uci commit wireless
wifi reload
Step 8: Apply (reboot)
Untagged traffic on the wired port lands in VLAN 70 now. A normal client
gets a192.168.70.xaddress, so when the router comes back ssh to
192.168.70.1instead of192.168.1.1.
# run on the router
reboot
ssh root@192.168.70.1
pgrep -f sing-box
logread -e sing-box | tail -40
# confirm sing-box came back after reconnecting on the full-tunnel VLAN
A wired client may keep its old DHCP state until the link bounces. If it does
not immediately get a 192.168.70.x address, unplug and reconnect the cable, or
renew DHCP on the client.
Step 9: Verify
Put a client on each VLAN. On the full-tunnel VLAN (192.168.70.x) everything is
tunneled, so your exit IP is always your chosen Xeovo endpoint, not your ISP:
# run on the full-tunnel client (VLAN 70)
curl https://ipinfo.io # should show Germany, not your ISP country
On the family VLAN (192.168.50.x) only am.i.mullvad.net is tunneled by default:
# run on the family client (VLAN 50)
curl https://ipinfo.io # should show your ISP/upstream country (direct path)
curl https://am.i.mullvad.net/json # should show France, via family-hy2
Now the kill switch. Stop sing-box and confirm the full-tunnel VLAN is cut off
while the family VLAN keeps working:
# stop/start on the router; run the curls on the clients
service sing-box stop
# full-tunnel will time out
curl -m5 https://am.i.mullvad.net/json
# family will still reach
curl -m5 https://ipinfo.io
service sing-box start
Finish with a browser leak test from each client at
dnsleaktest.com and
browserleaks.com/ip, with sing-box both up and
stopped. On the full-tunnel VLAN neither the exit IP nor the DNS resolver should
ever come back as your ISP's.
Using a different protocol
To use VLESS, Trojan, Shadowsocks, or VMess instead, you only edit the two
outbound blocks from step 3. Keep the family-hy2 and full-hy2 tags as they
are.
The snippets below use the same subscription URL from step 1.
Shadowsocks
curl -fsS 'https://YOUR-SUBSCRIPTION-URL/plain/config/' | tr -d '\r' \
| grep '^ss://' | grep -v 'plugin=' \
| sed -E 's#^ss://([^@]+)@([^#?]+).*#\2 \1#' \
| while read -r hostport creds; do
decoded=$(printf '%s' "$creds" | base64 -d)
method=${decoded%%:*}
password=${decoded#*:}
server=${hostport%:*}
port=${hostport##*:}
printf 'server=%s port=%s method=%s password=%s\n' "$server" "$port" "$method" "$password"
done
{
"type": "shadowsocks", "tag": "family-hy2",
"server": "FAMILY_HOST", "server_port": 9000,
"method": "METHOD",
"password": "YOUR_SS_PASSWORD"
}
Trojan
curl -fsS '...' | tr -d '\r' \
| grep '^trojan://' | grep -v '?' \
| sed -nE 's#^trojan://([^@]+)@([^:/?#]+):([0-9]+).*#server=\2 port=\3 password=\1#p'
{
"type": "trojan", "tag": "family-hy2",
"server": "FAMILY_HOST", "server_port": 444,
"password": "YOUR_TROJAN_PASSWORD",
"tls": { "enabled": true, "server_name": "FAMILY_HOST" }
}
VLESS (WebSocket + TLS)
curl -fsS '...' | tr -d '\r' | grep '^vless://' \
| sed -nE 's#^vless://([^@]+)@([^:/?#]+):([0-9]+).*sni=([^&#]+).*path=([^&#]+).*#server=\2 port=\3 uuid=\1 sni=\4 path=\5#p' \
| sed -E 's#%2[fF]#/#g'
{
"type": "vless", "tag": "family-hy2",
"server": "FAMILY_HOST", "server_port": 443,
"uuid": "YOUR_UUID",
"tls": {
"enabled": true, "server_name": "FAMILY_HOST",
"utls": { "enabled": true, "fingerprint": "chrome" }
},
"transport": { "type": "ws", "path": "/potosi", "headers": { "Host": "FAMILY_HOST" } }
}
The utls fingerprint makes the TLS handshake look like Chrome's so the
connection blends in. It's optional but worth keeping on the TLS-based protocols.
VMess (WebSocket + TLS)
curl -fsS '...' | tr -d '\r' | grep '^vmess://' | sed 's#^vmess://##' \
| while IFS= read -r b; do
printf '%s' "$b" | base64 -d \
| jq -r '"server=\(.add) port=\(.port) uuid=\(.id) path=\(.path)"'
done
{
"type": "vmess", "tag": "family-hy2",
"server": "FAMILY_HOST", "server_port": 443,
"uuid": "YOUR_UUID", "security": "auto",
"tls": {
"enabled": true, "server_name": "FAMILY_HOST",
"utls": { "enabled": true, "fingerprint": "chrome" }
},
"transport": { "type": "ws", "path": "/sacaca", "headers": { "Host": "FAMILY_HOST" } }
}
Swap both outbounds (family-hy2 and full-hy2) the same way and run sing-box.
check
When something is off
sing-box checkfails or the service won't start:sing-box check -cprints config parse errors (a leftover placeholder,
/etc/sing-box/config.json
a missing comma). Ifcheckpasses but the service still won't run,logreadshows the startup error.
-e sing-box | tail -20- The tunnel does not come up and you see an auth-like error (e.g.
404):
you've probably hit Xeovo's cap of 5 concurrent sessions, so disconnect other
devices and retry.
Recommended defaults
The demo config is bare. Two changes make it a sensible everyday default.
First, point proxy-dns at Mullvad's adblock.dns.mullvad.net (194.242.2.2)
instead of Cloudflare for ad and tracker blocking at the resolver (see
DNS filtering; adblock is the safe level, the
stricter lists break sites quietly).
Second, route a curated set of lists through the family tunnel, building on the
rule-set mechanism above.
Keep the am.i.mullvad.net and DoT rules first, then add the list rule. A
typical set, add or drop to suit:
"rule_set": [
{ "type": "remote", "tag": "youtube", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/youtube.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "cloudflare", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/cloudflare.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "telegram", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/telegram.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "meta", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/meta.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "news", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/news.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "geoblock", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/geoblock.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "russia_inside", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/russia_inside.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "hdrezka", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/hdrezka.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "hodca", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/hodca.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "ovh", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/ovh.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "hetzner", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/hetzner.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "digitalocean", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/digitalocean.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "cloudfront", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/cloudfront.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "tiktok", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/tiktok.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "discord", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/discord.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "block", "format": "binary", "url": "https://github.com/itdoginfo/allow-domains/releases/latest/download/block.srs", "download_detour": "family-hy2", "update_interval": "1h" },
{ "type": "remote", "tag": "antizapret", "format": "binary", "url": "https://github.com/savely-krasovsky/antizapret-sing-box/releases/latest/download/antizapret.srs", "download_detour": "family-hy2", "update_interval": "1h" }
],
"rules": [
{
"type": "logical", "mode": "and",
"rules": [
{ "source_ip_cidr": ["192.168.50.0/24"] },
{ "domain": ["am.i.mullvad.net"] }
],
"action": "route", "outbound": "family-hy2"
},
{ "source_ip_cidr": ["192.168.50.0/24"], "port": [853], "action": "route", "outbound": "family-hy2" },
{
"rule_set": [
"youtube", "cloudflare", "telegram", "meta", "news", "geoblock",
"russia_inside", "hdrezka", "hodca", "ovh", "hetzner", "digitalocean",
"cloudfront", "tiktok", "discord", "block", "antizapret"
],
"source_ip_cidr": ["192.168.50.0/24"],
"action": "route", "outbound": "family-hy2"
}
]
Upgrade OpenWrt with owut
We installed sing-box from the developer's .apk rather than an OpenWrt package,
because the package repos lag behind upstream. Since that binary isn't in the
mainline repos, an unattended owut upgrade can't rebuild the image with it and
fails. Upgrade with --remove instead, which drops the binary before rebuilding
the image:
# run on the router
owut upgrade --remove sing-box
After it reboots, reinstall sing-box exactly as in step 2 (download the .apk
again, or scp it across, then apk add --allow-untrusted). Your config
survives the upgrade without any action on your part.
Reinstalling the .apk does not enable or start the service — the rebuilt
image dropped it. Enable it again and confirm it's running:
# run on the router
service sing-box enable
service sing-box start
pgrep -f sing-box # prints a PID once it's up; nothing means it's not
logread -e sing-box | tail -20 # check for startup errors if pgrep is empty