Skip to content

Instantly share code, notes, and snippets.

@ahbanavi
Last active March 13, 2026 15:53
Show Gist options
  • Select an option

  • Save ahbanavi/70909d2b5e9ff47d812189757b4f96b6 to your computer and use it in GitHub Desktop.

Select an option

Save ahbanavi/70909d2b5e9ff47d812189757b4f96b6 to your computer and use it in GitHub Desktop.
OpenWrt Kill Switch for PassWall2 / Xray

OpenWrt Kill Switch for PassWall2 / Xray

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.


How It Works

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.


Files Overview

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

Step 1 — Create /etc/killswitch.sh

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.sh

Replace wan with your actual WAN interface name if different.
Check with: uci get network.wan.device


Step 2 — Register with fw4 via UCI

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 firewall

Remove 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 firewall

Step 3 — Create /usr/sbin/ks-watchdog.sh

cat > /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.sh

To support additional proxy binaries, add more pidof <name> lines to proxy_running().
Check your binary name with: ps | grep -E "xray|sing-box"


Step 4 — Create /etc/init.d/ks-watchdog

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-watchdog

Enable and start:

/etc/init.d/ks-watchdog enable
/etc/init.d/ks-watchdog start

Step 5 — Apply Firewall & Verify

/etc/init.d/firewall restart

Expected 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 killswitch

Expected 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
    }
}

Testing

# 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

Behavior Reference

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

Cleanup — Remove PassWall2 fw4 Warnings (Optional)

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 restart

PassWall2 may re-add these entries after an update. Re-run if warnings return.

@ohmydevops
Copy link

great

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