This script will list all of your connections, what device established each connection, what it’s connecting to, where it’s connecting to, and how many connections have been established.
It requires the sipcalc package.
Run it late at night, when no one is initiating any traffic, to catch potential bots, backdoors, etc.
You’ll see a line like this:
56 | [ROUTER] | 208.95.112.1 | United States | Total Uptime Technologies, LLC
… that’s the router doing geoIP lookups for this script, so it can display the Country column. The script caches past lookups, so it doesn’t have to re-do them on subsequent script reloads after the 10 second sleep. Thus you’ll see the left-most number above decrease over time. When you end the script, it cleans up that cache.
As regards that caching, it saves to /tmp/geoip_cache… which is in volatile memory. Each record saved is from 6 to 49 Bytes. It would take a lot to consume the router’s memory. The header prints out the total memory used by the cache.
The script prunes records which haven’t had a connection within $CACHE_HOLD_TIME, so memory isn’t exhausted for long run-times. CACHE_HOLD_TIME is user-configurable.
You’ll see it printing out slowly the first couple cycles as it does the lookups, but after that, it speeds up.
cat << 'EOF' > /etc/Connection_Tracker.sh #!/bin/sh CACHE_DIR="/tmp/geoip_cache" mkdir -p "$CACHE_DIR" # Set the cache hold time in seconds (60 minutes = 3600 seconds) CACHE_HOLD_TIME=3600 # Clean up cache folder from RAM when you press Ctrl+C to exit trap 'rm -rf "$CACHE_DIR"; printf "\033[H\033[JExiting...\n"; exit' INT TERM # Resolve ip-api.com to an IPv4 address at startup API_IP=$(nslookup ip-api.com 1.1.1.1 2>/dev/null | awk '/Address:/ {print $2}' | grep -E '^[0-9.]+$' | head -n1) [ -z "$API_IP" ] && API_IP="208.95.112.1" # Determine the router's current local WAN IP address to flag its traffic ROUTER_WAN_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '/src/ {print $7}') while true; do printf "\033[2J\033[H\033[3J" CACHE_SIZE=$(du -sh "$CACHE_DIR" 2>/dev/null | awk '{print $1}') [ -z "$CACHE_SIZE" ] && CACHE_SIZE="0K" echo "=========================================================================================" echo " OpenWrt One Live Threat Monitor | Cache Location: RAM ($CACHE_DIR)" echo " Memory Consumed: $CACHE_SIZE | Cache Expiry Window: $CACHE_HOLD_TIME Seconds" echo "=========================================================================================" printf "%-6s | %-15s | %-20s | %-15s | %s\n" "Active" "Source Device" "Destination IP" "Country" "ISP / Organization" echo "-----------------------------------------------------------------------------------------" # 1. Capture external destination IPs from conntrack RAW_CONNECTIONS=$(awk -v wan="$ROUTER_WAN_IP" '{ src="" dst="" for(i=1; i<=NF; i++) { if($i ~ /^src=/) { if(src == "") src=substr($i, 5) } if($i ~ /^dst=/) { if(dst == "") { dst=substr($i, 5) if(dst ~ /^(10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168\.|127\.|255\.|224\.|fe80|::1|0000:0000|fdf8)/) { dst="" } } } } if(src != "" && dst != "") { if(src == wan || src ~ /^(127\.|::1)/) { src="[ROUTER]" } print src " " dst } }' /proc/net/nf_conntrack 2>/dev/null) # 2. Extract unique paths, aggregate duplicate connections, compress IPv6 strings via sipcalc PROCESSED_LIST="" ACTIVE_IPS="" if [ -n "$RAW_CONNECTIONS" ]; then PROCESSED_LIST=$(echo "$RAW_CONNECTIONS" | sort | uniq -c | while read -r count src dst; do [ -z "$dst" ] && continue if echo "$dst" | grep -q ':'; then dst=$(sipcalc "$dst" 2>/dev/null | awk '/Compressed address/ {print $4}') fi if [ -n "$dst" ]; then echo "$count $src $dst" fi done) # Build list of current connections ACTIVE_IPS=$(echo "$PROCESSED_LIST" | awk '{print $3}' | sort -u) fi # 3. Sort logic: Group by IPv4 first then IPv6, arranged numerically by destination IP address SORTED_TRAFFIC=$(echo "$PROCESSED_LIST" | awk ' { if ($3 ~ /:/) print "v6", $0; else print "v4", $0 } ' | sort -k1,1 -k4,4V | cut -d' ' -f2-) # 4. Display results row-by-row and handle RAM cache checking echo "$SORTED_TRAFFIC" | while read -r count src dst; do [ -z "$dst" ] && continue CACHE_COUNTRY="${CACHE_DIR}/${dst}.country" CACHE_ORG="${CACHE_DIR}/${dst}.org" if [ -f "$CACHE_COUNTRY" ] && [ -f "$CACHE_ORG" ]; then country=$(cat "$CACHE_COUNTRY") org=$(cat "$CACHE_ORG") # Touch files to keep timestamp fresh if IP has > 0 connections touch "$CACHE_COUNTRY" "$CACHE_ORG" 2>/dev/null else geo_data=$(curl -s -H "Host: ip-api.com" --connect-timeout 2 --max-time 4 "http://${API_IP}/json/${dst}?fields=country,org") if [ -n "$geo_data" ] && echo "$geo_data" | grep -q '{'; then country=$(echo "$geo_data" | jsonfilter -e '@.country') org=$(echo "$geo_data" | jsonfilter -e '@.org') else country="Timeout" org="Lookup Failed" fi [ -z "$country" ] && country="Unknown" [ -z "$org" ] && org="Unknown" echo "$country" > "$CACHE_COUNTRY" echo "$org" > "$CACHE_ORG" sleep 1 fi printf "%-6s | %-15s | %-20s | %-15s | %s\n" "$count" "$src" "$dst" "$country" "$org" done # Check file timestamps to age out dead connections after CACHE_HOLD_TIME if [ -d "$CACHE_DIR" ]; then CURRENT_TIME=$(date +%s) for cache_file in "$CACHE_DIR"/*; do [ ! -e "$cache_file" ] && break cached_ip=$(basename "$cache_file" | sed -E 's/\.(country|org)$//') # If the IP is NOT connected, evaluate its time of death if ! echo "$ACTIVE_IPS" | grep -Fqx "$cached_ip"; then # Get the last modification time of the file using stat FILE_TIME=$(stat -c %Y "$cache_file" 2>/dev/null) if [ -n "$FILE_TIME" ]; then AGE=$((CURRENT_TIME - FILE_TIME)) # Shred the cache file only if it exceeds the CACHE_HOLD_TIME threshold if [ "$AGE" -ge "$CACHE_HOLD_TIME" ]; then rm -f "$cache_file" fi fi fi done fi sleep 10 done EOF
Make the file executable:
chmod +x /etc/Connection_Tracker.sh
Run the file (in a Terminal window connected to the router via ssh):
sh /etc/Connection_Tracker.sh