Blocks all WAN access whenever PassWall2 or a standalone Xray/Sing-box instance is down or has crashed. Uses a persistent nftables rule enforced by fw4 and a procd watchdog service that reacts within ~1 second on clean stop/start and within 5 seconds on a crash.
Boot
└── fw4 starts → killswitch.sh runs → DROP rule added to nftables
└── PassWall2/xray starts
└── ks-watchdog starts → detects xray running → DROP rule removed
PassWall2 stopped cleanly
└── nft monitor fires → sync_killswitch → xray gone → DROP rule added (~1s)
PassWall2 / xray crashed
└── 5s polling loop → xray gone → DROP rule added (≤5s)
Proxied traffic (xray → remote server) flows through the OUTPUT chain, not FORWARD, so the kill switch drop rule in FORWARD never interferes with active PassWall2 sessions.
| File | Purpose |
|---|---|
/etc/killswitch.sh |
Creates the nftables table; always adds the WAN drop rule |
/etc/init.d/ks-watchdog |
procd service definition — survives reboots |
/usr/sbin/ks-watchdog.sh |
Watchdog loop — adds/removes only the drop rule handle |
cat > /etc/killswitch.sh << 'EOF'
#!/bin/sh
nft delete table inet killswitch 2>/dev/null
nft add table inet killswitch
nft add chain inet killswitch forward \
'{ type filter hook forward priority filter + 100; policy accept; }'
nft add rule inet killswitch forward ct state '{ established, related }' accept
nft add rule inet killswitch forward oifname "wan" drop
EOF
chmod +x /etc/killswitch.shReplace
wanwith your actual WAN interface name if different.
Check with:uci get network.wan.device
This makes fw4 run killswitch.sh on every firewall restart/reload:
uci add firewall include
uci set 'firewall.@include[-1].type=script'
uci set 'firewall.@include[-1].path=/etc/killswitch.sh'
uci commit firewallRemove the unsupported name option if it was set previously:
SECTION=$(uci show firewall | grep "killswitch" | head -1 | cut -d'.' -f2)
uci delete firewall.${SECTION}.name 2>/dev/null
uci commit firewallcat > /usr/sbin/ks-watchdog.sh << 'EOF'
#!/bin/sh
proxy_running() {
pidof xray > /dev/null 2>&1 || \
pidof sing-box > /dev/null 2>&1 || \
pidof xray-linux-amd64 > /dev/null 2>&1
}
killswitch_active() {
nft list chain inet killswitch forward 2>/dev/null | grep -q "oifname.*drop"
}
activate_killswitch() {
logger -t killswitch "Proxy DOWN — activating WAN block"
nft add rule inet killswitch forward oifname "wan" drop
}
deactivate_killswitch() {
logger -t killswitch "Proxy UP — removing WAN block"
HANDLE=$(nft -a list chain inet killswitch forward 2>/dev/null \
| grep "oifname.*drop" | awk '{print $NF}')
[ -n "$HANDLE" ] && nft delete rule inet killswitch forward handle "$HANDLE"
}
sync_killswitch() {
if proxy_running; then
killswitch_active && deactivate_killswitch
else
killswitch_active || activate_killswitch
fi
}
trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INT
# Sync state immediately on service start
sync_killswitch
# Immediate trigger: react to PassWall2's nftables add/remove events
nft monitor 2>/dev/null | awk '/PSW2_MANGLE/ { fflush(); print }' | \
while read -r _; do
sleep 1
sync_killswitch
done &
# 5s polling fallback — catches crashes where nft rules stay but process dies
while true; do
sleep 5
sync_killswitch
done
EOF
chmod +x /usr/sbin/ks-watchdog.shTo support additional proxy binaries, add more
pidof <name>lines toproxy_running().
Check your binary name with:ps | grep -E "xray|sing-box"
cat > /etc/init.d/ks-watchdog << 'EOF'
#!/bin/sh /etc/rc.common
START=99
STOP=01
USE_PROCD=1
start_service() {
procd_open_instance
procd_set_param command /usr/sbin/ks-watchdog.sh
procd_set_param respawn 3600 5 0
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
EOF
chmod +x /etc/init.d/ks-watchdogEnable and start:
/etc/init.d/ks-watchdog enable
/etc/init.d/ks-watchdog start/etc/init.d/firewall restartExpected warnings (harmless, PassWall2 bug):
Section passwall2 option 'reload' is not supported by fw4
Section passwall2_server option 'reload' is not supported by fw4
Verify the kill switch table loaded:
nft list table inet killswitchExpected output when xray is down:
table inet killswitch {
chain forward {
type filter hook forward priority 100; policy accept;
ct state { established, related } accept
oifname "wan" drop
}
}
Expected output when xray is up (watchdog removed the drop rule):
table inet killswitch {
chain forward {
type filter hook forward priority 100; policy accept;
ct state { established, related } accept
}
}
# Watch logs in real time
logread -f | grep killswitch
# Stop PassWall2 — kill switch should activate within ~1 second
/etc/init.d/passwall2 stop
# → Proxy DOWN — activating WAN block
# Start PassWall2 — kill switch should deactivate within ~1 second
/etc/init.d/passwall2 start
# → Proxy UP — removing WAN block| Event | Detection | Latency |
|---|---|---|
passwall2 stop (clean) |
nft monitor |
~1 second |
passwall2 start (clean) |
nft monitor |
~1 second |
| xray/sing-box crash | 5s polling via pidof |
≤5 seconds |
| Router reboot | fw4 runs killswitch.sh on boot |
Immediate |
| fw4 restart/reload | fw4 re-runs killswitch.sh |
Immediate |
uci delete firewall.passwall2.reload 2>/dev/null
uci delete firewall.passwall2_server.reload 2>/dev/null
uci delete firewall.passwall2_server 2>/dev/null
uci commit firewall
/etc/init.d/firewall restartPassWall2 may re-add these entries after an update. Re-run if warnings return.
great