Note
Please be advised that this article is written for RouterOS7.20.1and ShellCrash1.9.4beta6on2026-01-29, both operated as KVM instance in PVE. Contents might vary for different version of RouterOS and ShellCrash.
Introduction
Previously I used DHCP gateway nomination to let devices in my network use proxy. Although it successfully enables me to use proxy seamlessly, I can’t expand this experience to all devices, since sometimes ShellCrash may really “crashed” and would affect normal network traffic. I have heard OSPF feature for a long time, so recently I set up OSPF using RouterOS according to DeepRouter’s article. This article is to record my experience and share my way to enhance the robustness.
ShellCrash and Bird Configuration
Although it is fine to deploy ShellCrash and BIRD in separate instance, it is highly recommended to deploy in the same one.
ShellCrash installation process is ignored, just make sure you are using MIX in DNS configuration(press 2(feature setting) - 2(DNS Setting)).
Check IPv4 and IPv6 forward configuration
run sudo sysctl -p and check if the following are properly configured:
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
If not, edit /etc/sysctl.conf.
BIRD
We will use !chnroutes to distinguish chn ips.
Make sure you doesn’t enable/start bird service after installation. There are several things to do.
For Debian:
sudo apt update
sudo apt install bird2 make
bird.conf
Edit /etc/bird/bird.conf and change as below.
make sure:
router idis your ShellCrash/BIRD instance IP- The
interfacein both ospf v2 and v3 is set to the interface this instance used to connect to the gateway(the LAN port). You may check usingip aorip link, and find the one OTHER THANlo.
log syslog all;
router id 192.168.114.51; #This line change to instance IP
protocol device {
scan time 60;
}
protocol kernel {
ipv4 {
import none;
export none;
};
}
protocol kernel {
ipv6 {
import none;
export none;
};
}
protocol static {
ipv4;
include "/etc/bird/routes4.conf";
}
protocol static {
ipv6;
include "/etc/bird/routes6.conf";
}
protocol ospf v2 {
ipv4 {
export all;
};
area 0.0.0.0 {
interface "ens18" { # Change this to your networking interface
type pointopoint;
};
};
}
protocol ospf v3 {
ipv6 {
export all;
};
area 0.0.0.0 {
interface "ens18" { # Change this to your networking interface
type pointopoint;
};
};
}
Generating routes
git clone https://github.com/ous50/nchnroutes.git
cd nchnroutes
make
If you have trouble connecting to github.com, use a mirror instead:
git clone https://hk.gh-proxy.org/https://github.com/ous50/nchnroutes.git
cd nchnroutes
make
You are pleased to continue if the following output is shown:
python3 produce.py
sudo mv routes4.conf /etc/bird/routes4.conf
sudo mv routes6.conf /etc/bird/routes6.conf
sudo birdc configure
BIRD 2.0.12 ready.
Reading configuration from /etc/bird/bird.conf
Reconfigured
RouterOS Configuration
You need to set up bypassing routing table first to prevent looping instantly after OSPF neighbor is found.
Bypass ShellCrash looping and setting up exceptions
We are going to make another routing table to bypass the traffic from ShellCrash to prevent loop.
/routing table
add disabled=no fib name=bypass-shellcrash
/routing rule
add action=lookup-only-in-table comment="Bypass Shellcrash Gateway using Routing Mark" disabled=no routing-mark=bypass-shellcrash table=bypass-shellcrash
Migrate your routes from main to bypass-shellcrash:
/ip route
add disabled=no distance=1 dst-address=<Your LAN with CIDR> gateway=lan routing-table=bypass-shellcrash scope=30 suppress-hw-offload=no target-scope=10
add disabled=no distance=1 dst-address=0.0.0.0/0 gateway=<Your Internet Gateway> routing-table=bypass-shellcrash scope=30 suppress-hw-offload=no target-scope=10
Firewall tagging
Make sure:
- Change the dst-address to your LAN, preserving the
!. - Change the src-mac-address to the ShellCrash networking interface port MAC address(check using
ip linkin shellcrash instance or directly from PVE)
/ip firewall mangle
add action=mark-routing chain=prerouting comment=bypass-shellcrash-gateway dst-address=!192.168.0.0/23 new-routing-mark=bypass-shellcrash src-mac-address=11:45:14:19:19:81
OSPF config
By default you do not need to change anything in this script.
/routing ospf instance
add disabled=no name=shellcrash
add disabled=no name=shellcrash-v6 version=3
/routing ospf area
add disabled=no instance=shellcrash name=ospf-area-v4
add disabled=no instance=shellcrash-v6 name=ospf-area-v6
/routing ospf interface-template
add area=ospf-area-v4 cost=10 disabled=no priority=32 type=ptp
add area=ospf-area-v6 cost=10 disabled=no priority=32 type=ptp
Tricks
Robust DNS setting
Due to the fake-ip used by ShellCrash, you should set ShellCrash’s DNS as an upstream.
In this case we assume nominating RouterOS’s DNS to clients and use the following RouterOS script to update upstream DNS source to enhance the robustness, in case of ShellCrash failed.
:global ospfInstance "shellsrash"
:global ospfDNS "1.1.4.5" # Your ShellCrash instance IP
:global publicDNS "223.5.5.5" # Change this DNS for your direct internet access
# (true / false), turn debug off after testing or your logs have abundant annoying debug messages.
:local DEBUG false
# marking the final status default value is false (down)
:local isOspfActive false
# Trying to find the neighbor
:local neighborIds [/routing ospf neighbor find instance=$ospfInstance]
# check if neighbor is found
:if ([:len $neighborIds] > 0) do={
# Note: In this case we will only use one instance.
# If you have multiple instance, you might need to traversal and find the instance.
:local neighborState [/routing ospf neighbor get ($neighborIds->0) state]
:if ($DEBUG) do={:log info "[DEBUG] Neighbor found! State: $neighborState"}
# Only consider ospf active when state is "Full". Remember, not "full". This state might vary on different RouterOS version
:if ($neighborState = "Full") do={
:set isOspfActive true
}
} else={
# if length is 0, this neighbor is not found. check bird configuration/status.
:if ($DEBUG) do={:log info "[DEBUG] No OSPF neighbor found. Logic assumes DOWN."}
:set isOspfActive false
}
# change DNS according to the variable isOspfActive
:local currentDNS [/ip dns get servers]
:if ($isOspfActive) do={
# OSPF Up
:if ($DEBUG) do={:log info "[DEBUG] Decision: OSPF is UP."}
:if ($currentDNS != $ospfDNS) do={
:log warning "OSPF Shellcrash Up: Switching to OSPF DNS ($ospfDNS)."
/ip dns set servers=$ospfDNS
/ip dns cache flush
} else={
:if ($DEBUG) do={:log info "[DEBUG] DNS is already correct (OSPF)."}
}
} else={
# OSPF Down (or missing neighbor)
:if ($DEBUG) do={:log info "[DEBUG] Decision: OSPF is DOWN/MISSING."}
:if ($currentDNS != $publicDNS) do={
:log warning "OSPF Shellcrash Down: Switching to Public DNS ($publicDNS)."
/ip dns set servers=$publicDNS
/ip dns cache flush
} else={
:if ($DEBUG) do={:log info "[DEBUG] DNS is already correct (Public)."}
}
}
Switch off bird when shellcrash is crashed
We will use a bash script to actively check if shellcrash is alive. If the API is unreachable, it will stop the Bird service to withdraw OSPF routes.
Copy and paste the following block into your PVE Shell (logged in as the clash user, or a user with sudo privileges):
# 1. Create the watchdog script
# Note: We use 'EOF' (quoted) to prevent variable expansion during creation.
sudo tee /home/clash/check_clash.sh > /dev/null << 'EOF'
#!/bin/bash
# Configuration
API_URL="http://127.0.0.1:9999/"
LOG_FILE="/home/clash/clash_watchdog.log"
DEBUG=false
INTERVAL=3
# Init state (0=unknown, 1=normal, 2=crashed)
LAST_STATE=0
while true; do
# Check ShellCrash API
HTTP_CODE=$(curl -s -m 1 -o /dev/null -w "%{http_code}" "$API_URL")
if [ "$DEBUG" = "true" ]; then
echo "[DEBUG] Dashboard code: $HTTP_CODE"
fi
if [ "$HTTP_CODE" == "200" ]; then
# ShellCrash is ALIVE
if [ "$LAST_STATE" != "1" ]; then
DATE=$(date "+%Y-%m-%d %H:%M:%S")
# Check if Bird is running using systemctl's return code (more reliable)
sudo systemctl is-active --quiet bird
BIRD_EXIT_CODE=$?
if [ $BIRD_EXIT_CODE -ne 0 ]; then
if [ "$DEBUG" = "true" ]; then
echo "ShellCrash detected. Starting Bird..."
fi
echo "[$DATE] ShellCrash recovered/detected. Starting Bird..." >> $LOG_FILE
sudo systemctl start bird
fi
LAST_STATE=1
fi
else
# ShellCrash is DEAD
if [ "$LAST_STATE" != "2" ]; then
DATE=$(date "+%Y-%m-%d %H:%M:%S")
if [ "$DEBUG" = "true" ]; then
echo "[$DATE] Alert! ShellCrash died (Code: $HTTP_CODE). Stopping Bird!"
fi
echo "[$DATE] Alert! ShellCrash died (Code: $HTTP_CODE). Stopping Bird!" >> $LOG_FILE
sudo systemctl stop bird
LAST_STATE=2
fi
fi
sleep $INTERVAL
done
EOF
# 2. Make it executable
sudo chmod +x /home/clash/check_clash.sh
# 3. Create Systemd Service
sudo tee /etc/systemd/system/clash-watchdog.service > /dev/null << EOF
[Unit]
Description=ShellCrash Watchdog for Bird OSPF
After=network.target
[Service]
User=clash
Group=clash
ExecStart=/home/clash/check_clash.sh
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
# 4. Enable and Start
sudo systemctl daemon-reload
sudo systemctl enable --now clash-watchdog.service
sudo systemctl status clash-watchdog.service