Skip to content

Instantly share code, notes, and snippets.

@fakhamatia
Last active July 27, 2025 21:54
Show Gist options
  • Save fakhamatia/d84bdddc39f555bef30574185a19bc53 to your computer and use it in GitHub Desktop.
Save fakhamatia/d84bdddc39f555bef30574185a19bc53 to your computer and use it in GitHub Desktop.
#!/bin/sh
TEST_URL="https://www.gstatic.com/generate_204"
MAX_LATENCY=1000
ALLOWED_COUNTRIES="AT|BE|CA|CH|CZ|DE|DK|ES|FI|FL|FR|GB|HU|IE|IS|IT|LI|LU|NL|NO|PT|RO|SE|SK|SW|UK|US"
SLEEP=15
TIMEOUT=$(awk "BEGIN {print (${MAX_LATENCY} / 1000) + 1}")
CURRENT=$(uci -q get passwall2.myshunt.default_node)
REMARK=$(uci -q get passwall2."$CURRENT".remarks)
SUB_COUNT=1
COUNTRY_CHECK=1
SWITCHED=0
LOCK_FILE="/tmp/passwall-auto-switch.lock"
STATS_FILE="/root/passwall_stats.json"
log_msg() {
echo "$1"
logger -t internet-detector "$1"
}
if [ -f "$LOCK_FILE" ]; then
echo "Script already running. Exiting."
exit 1
fi
touch "$LOCK_FILE"
cleanup() {
rm -f "$LOCK_FILE"
}
trap cleanup EXIT
get_nodes() {
uci show passwall2 | grep "=nodes" | cut -d. -f2 | cut -d= -f1 | while read -r node; do
proto=$(uci -q get passwall2."$node".protocol)
if [ "$proto" = "vmess" ] || [ "$proto" = "vless" ] || [ "$proto" = "trojan" ]; then
echo "$node"
fi
done
}
test_latency() {
ms=$(curl -o /dev/null -s -w "%{time_total}" --max-time "$TIMEOUT" "$TEST_URL")
echo $(awk "BEGIN {print int($ms * 1000)}")
}
change_node() {
node="${1:-_direct}"
uci set passwall2.myshunt.default_node="$node"
uci commit passwall2
/etc/init.d/passwall2 restart 2>/dev/null
sleep "$SLEEP"
}
get_country_code() {
response=$(curl -s -w "HTTPSTATUS:%{http_code}" --max-time "$TIMEOUT" https://ipinfo.io/country)
body=$(echo "$response" | sed -e 's/HTTPSTATUS\:.*//g' | tr -d '\n')
status=$(echo "$response" | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$status" != "200" ]; then
echo "UNKNOWN: HTTP $status"
elif [ -z "$body" ]; then
echo "UNKNOWN: Empty response"
else
echo "$body"
fi
}
NODES=$(get_nodes)
log_msg "Current node $CURRENT ($REMARK)"
CURRENT_FOUND=0
AFTER_CURRENT=""
if [ "$CURRENT" = "_direct" ]; then
CURRENT_FOUND=1
fi
for node in $NODES; do
if [ "$CURRENT_FOUND" = "1" ]; then
AFTER_CURRENT="$AFTER_CURRENT $node"
elif [ "$node" = "$CURRENT" ]; then
CURRENT_FOUND=1
fi
done
ORDERED_NODES="$AFTER_CURRENT"
update_stats() {
country="${1:-UNKNOWN}"
status="${2:-skip}"
[ ! -f "$STATS_FILE" ] && echo "{}" >"$STATS_FILE"
OLD=$(cat "$STATS_FILE")
TOTAL=$(echo "$OLD" | jsonfilter -e "@.$country.total" 2>/dev/null || echo 0)
SUCCESS=$(echo "$OLD" | jsonfilter -e "@.$country.success" 2>/dev/null || echo 0)
FAIL=$(echo "$OLD" | jsonfilter -e "@.$country.fail" 2>/dev/null || echo 0)
SKIP=$(echo "$OLD" | jsonfilter -e "@.$country.skip" 2>/dev/null || echo 0)
case "$status" in
success)
TOTAL=$((TOTAL + 1))
SUCCESS=$((SUCCESS + 1))
;;
fail)
TOTAL=$((TOTAL + 1))
FAIL=$((FAIL + 1))
;;
skip)
TOTAL=$((TOTAL + 1))
SKIP=$((SKIP + 1))
;;
esac
NEW=$(echo "$OLD" |
jq ". + {\"$country\": {\"total\": $TOTAL, \"success\": $SUCCESS, \"fail\": $FAIL, \"skip\": $SKIP}}")
echo "$NEW" >"$STATS_FILE"
}
for node in $ORDERED_NODES; do
REMARK=$(uci -q get passwall2."$node".remarks)
ADDRESS=$(uci -q get passwall2."$node".address)
log_msg "Trying node $node ($REMARK)"
COUNTRY_HINT=$(echo "$REMARK" | sed -n 's/.*\([^[:space:]]\{2,3\}\)\[.*/\1/p')
COUNTRY=""
COUNTRY_FROM_IP=0
NODE_CHANGED=0
if [ "$COUNTRY_CHECK" -eq 1 ]; then
if [ -n "$COUNTRY_HINT" ]; then
if echo "$COUNTRY_HINT" | grep -qE "^($ALLOWED_COUNTRIES)$"; then
COUNTRY="$COUNTRY_HINT"
log_msg "Country detected from remark and allowed: $COUNTRY"
else
update_stats "$COUNTRY_HINT" skip
log_msg "Country $COUNTRY_HINT from remark is not allowed, skipping"
continue
fi
else
if [ "$NODE_CHANGED" -eq 0 ]; then
change_node $node
NODE_CHANGED=1
fi
if [ "$COUNTRY_FROM_IP" -eq 0 ]; then
COUNTRY=$(get_country_code)
COUNTRY_FROM_IP=1
fi
if echo "$COUNTRY" | grep -qE "^($ALLOWED_COUNTRIES)$"; then
log_msg "Country detected from IP and allowed: $COUNTRY"
elif echo "$COUNTRY" | grep -q "^UNKNOWN"; then
update_stats "UNKNOWN" fail
log_msg "Unable to detect country from IP, marking as fail"
continue
else
update_stats "$COUNTRY" skip
log_msg "Country $COUNTRY from IP is not allowed, skipping"
continue
fi
fi
fi
if [ "$NODE_CHANGED" -eq 0 ]; then
change_node $node
NODE_CHANGED=1
fi
LATENCY=$(test_latency)
log_msg "Node $node latency: ${LATENCY}ms"
if [ "$LATENCY" -gt "$MAX_LATENCY" ] || [ "$LATENCY" -eq 0 ]; then
if [ "$COUNTRY_CHECK" -eq 1 ]; then
update_stats "$COUNTRY" fail
fi
log_msg "Latency too high or zero, marking as fail"
continue
fi
if [ "$COUNTRY_CHECK" -eq 1 ]; then
if [ "$COUNTRY_FROM_IP" -eq 1 ]; then
update_stats "$COUNTRY" success
log_msg "Node $node accepted"
SWITCHED=1
break
else
COUNTRY_FINAL=$(get_country_code)
if echo "$COUNTRY_FINAL" | grep -qE "^($ALLOWED_COUNTRIES)$"; then
update_stats "$COUNTRY_FINAL" success
log_msg "Node $node accepted after final IP check"
SWITCHED=1
break
elif echo "$COUNTRY_FINAL" | grep -q "^UNKNOWN"; then
update_stats "UNKNOWN" fail
log_msg "Final IP check failed, country unknown, marking as fail"
continue
else
update_stats "$COUNTRY_FINAL" skip
log_msg "Final IP check: country $COUNTRY_FINAL not allowed, skipping"
continue
fi
fi
else
log_msg "Node $node accepted"
SWITCHED=1
break
fi
done
if [ "$SWITCHED" -eq 0 ]; then
log_msg "All nodes failed. Switching to direct."
change_node "_direct"
if [ "$SUB_COUNT" -eq 0 ]; then
log_msg "No subscriptions found."
else
i=0
while [ "$i" -lt "$SUB_COUNT" ]; do
log_msg "Updating subscription $((i + 1))"
lua /usr/share/passwall2/subscribe.lua start "@subscribe_list[$i]" 2>/dev/null
sleep "$SLEEP"
i=$((i + 1))
done
fi
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment