Connection Tracker

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