When an autonomous system you care about announces a new prefix — or pulls one — that's the kind of thing you usually find out about by accident. A firewall rule starts misbehaving, a customer reports a routing change, an alert fires on something unrelated and you trace it backwards. There's no good reason for that. The data is public, the deltas are easy to compute, and a daily email is enough to catch most of what matters.
So I wrote a small bash script that does exactly that.
What it does
Once a day, the script:
- Fetches the current list of announced CIDRs for each AS number you care about, from
asn.ipinfo.app/api/text/list/AS<number>. - Compares it against the previous run's list, stored on disk under
as_monitor_data/. - If anything has changed, sends a single HTML email — green rows for prefixes that were added, red rows for prefixes that were removed.
- Stays silent on days when nothing has changed.
That last point matters. The whole point of a monitor like this is that you trust it to be quiet when there's nothing to see. If it pings you every day, you start ignoring it within a week, and the one day it has something useful to say it goes straight to the trash.
Configuration
A handful of variables at the top of the script:
SMTP_SERVER="mail.example.com"
SMTP_PORT="25" # or 587 for TLS
SMTP_USE_TLS="false" # "true" turns on --ssl-reqd
SMTP_USER="" # leave blank for an open relay
SMTP_PASS=""
EMAIL_FROM="[email protected]"
EMAIL_TO="[email protected]"
AS_NUMBERS="398493 15169" # space-separated
It supports both authenticated SMTP-with-TLS (the typical port 587 setup) and an open relay on port 25 — handy if you have a local mailserver on the LAN that doesn't care about auth from internal hosts. If SMTP_USER/SMTP_PASS are unset, the script skips the auth flag entirely.
The dependencies are minimal — curl, plus the standard coreutils (sort, comm, wc). The asn.ipinfo.app text/list endpoint is plain text, one CIDR per line, so no JSON parser is needed.
Run it
chmod +x as_monitor.sh
./as_monitor.sh
The first run is "everything new" — there's no previous file to compare against, so every prefix shows up as added. From the second run onwards you only see the deltas. Drop it in cron:
0 9 * * * /path/to/as_monitor.sh
Daily at 9 AM. Adjust to taste — once a day is honestly plenty for prefix-level monitoring; ASN announcements don't churn that fast.
The script
#!/bin/bash
# AS IP Monitor Script
# Monitors IP prefix changes for multiple AS numbers and sends email reports
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DATA_DIR="${SCRIPT_DIR}/as_monitor_data"
SMTP_SERVER="smtp.example.com"
SMTP_PORT="587"
SMTP_USE_TLS="true" # Set to "false" for non-secure port 25 relays
SMTP_USER="[email protected]"
SMTP_PASS="your-password"
EMAIL_FROM="[email protected]"
EMAIL_TO="[email protected]"
EMAIL_SUBJECT="AS IP Monitor - Daily Report"
# AS numbers to monitor (space-separated)
AS_NUMBERS="398493 15169 8075"
# API base URL
API_BASE="https://asn.ipinfo.app/api/text/list"
# Create data directory if it doesn't exist
mkdir -p "$DATA_DIR"
# Function to fetch current IPs for an AS
fetch_as_ips() {
local asn=$1
local output_file=$2
# Fetch IP prefixes from the API
curl -s "${API_BASE}/AS${asn}" | sort > "$output_file"
# Check if fetch was successful
if [ ! -s "$output_file" ]; then
echo "Warning: Failed to fetch data for AS${asn}" >&2
return 1
fi
return 0
}
# Function to compare IP lists and generate changes
compare_lists() {
local old_file=$1
local new_file=$2
local added_file=$3
local removed_file=$4
# Find added IPs (in new but not in old)
if [ -f "$old_file" ]; then
comm -13 "$old_file" "$new_file" > "$added_file"
comm -23 "$old_file" "$new_file" > "$removed_file"
else
# First run - all IPs are "added"
cp "$new_file" "$added_file"
touch "$removed_file"
fi
}
# Function to generate HTML report for a single AS
generate_as_report() {
local asn=$1
local added_file=$2
local removed_file=$3
local added_count=$(wc -l < "$added_file")
local removed_count=$(wc -l < "$removed_file")
# Skip if no changes
if [ "$added_count" -eq 0 ] && [ "$removed_count" -eq 0 ]; then
return 0
fi
echo "<h2>AS${asn} Changes</h2>"
echo "<p><strong>Added:</strong> ${added_count} | <strong>Removed:</strong> ${removed_count}</p>"
if [ "$added_count" -gt 0 ] || [ "$removed_count" -gt 0 ]; then
echo "<table border='1' cellpadding='8' cellspacing='0' style='border-collapse: collapse; width: 100%; margin-bottom: 20px;'>"
echo "<thead style='background-color: #f0f0f0;'>"
echo "<tr><th>Status</th><th>IP Prefix</th></tr>"
echo "</thead><tbody>"
# Process removed IPs
while read -r prefix; do
if [ -n "$prefix" ]; then
echo "<tr style='background-color: #ffcccc;'>"
echo "<td><strong style='color: #cc0000;'>REMOVED</strong></td>"
echo "<td>${prefix}</td>"
echo "</tr>"
fi
done < "$removed_file"
# Process added IPs
while read -r prefix; do
if [ -n "$prefix" ]; then
echo "<tr style='background-color: #ccffcc;'>"
echo "<td><strong style='color: #00cc00;'>ADDED</strong></td>"
echo "<td>${prefix}</td>"
echo "</tr>"
fi
done < "$added_file"
echo "</tbody></table>"
fi
}
# Function to send email via SMTP
send_email() {
local subject=$1
local html_body=$2
local total_changes=$3
# Only send email if there are changes
if [ "$total_changes" -eq 0 ]; then
echo "No changes detected. Email not sent."
return 0
fi
# Create email with headers
local email_file="${DATA_DIR}/email_$(date +%s).tmp"
cat > "$email_file" <<EOF
From: ${EMAIL_FROM}
To: ${EMAIL_TO}
Subject: ${subject}
MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
h2 { color: #666; margin-top: 30px; }
table { font-size: 14px; }
.summary { background-color: #f9f9f9; padding: 15px; border-left: 4px solid #007bff; margin-bottom: 20px; }
</style>
</head>
<body>
<h1>AS IP Monitor Report</h1>
<div class="summary">
<p><strong>Report Date:</strong> $(date '+%Y-%m-%d %H:%M:%S')</p>
<p><strong>Total Changes:</strong> ${total_changes}</p>
</div>
${html_body}
<hr>
<p style='color: #999; font-size: 12px;'>This is an automated report from AS IP Monitor</p>
</body>
</html>
EOF
# Send via curl with SMTP
local curl_opts=(
--url "smtp://${SMTP_SERVER}:${SMTP_PORT}"
--mail-from "${EMAIL_FROM}"
--mail-rcpt "${EMAIL_TO}"
--upload-file "$email_file"
)
# Add TLS/authentication only if configured
if [ "$SMTP_USE_TLS" = "true" ]; then
curl_opts+=(--ssl-reqd)
fi
# Add authentication if user/pass are provided
if [ -n "$SMTP_USER" ] && [ -n "$SMTP_PASS" ]; then
curl_opts+=(--user "${SMTP_USER}:${SMTP_PASS}")
fi
curl "${curl_opts[@]}" 2>&1
local result=$?
rm -f "$email_file"
if [ $result -eq 0 ]; then
echo "Email sent successfully!"
else
echo "Failed to send email. Check SMTP settings."
fi
return $result
}
# Main execution
main() {
echo "=== AS IP Monitor - $(date) ==="
echo "Monitoring AS numbers: ${AS_NUMBERS}"
echo ""
local html_report=""
local total_changes=0
for asn in $AS_NUMBERS; do
echo "Processing AS${asn}..."
local current_file="${DATA_DIR}/as${asn}_current.txt"
local previous_file="${DATA_DIR}/as${asn}_previous.txt"
local added_file="${DATA_DIR}/as${asn}_added.txt"
local removed_file="${DATA_DIR}/as${asn}_removed.txt"
# Fetch current data
if ! fetch_as_ips "$asn" "$current_file"; then
echo "Skipping AS${asn} due to fetch error"
continue
fi
# Compare with previous data
compare_lists "$previous_file" "$current_file" "$added_file" "$removed_file"
# Generate HTML report section
local as_html=$(generate_as_report "$asn" "$added_file" "$removed_file")
if [ -n "$as_html" ]; then
html_report="${html_report}${as_html}"
local added_count=$(wc -l < "$added_file")
local removed_count=$(wc -l < "$removed_file")
total_changes=$((total_changes + added_count + removed_count))
fi
# Update previous file for next run
cp "$current_file" "$previous_file"
echo "AS${asn}: $(wc -l < "$added_file") added, $(wc -l < "$removed_file") removed"
echo ""
done
# Send email report
if [ -n "$html_report" ]; then
echo "Sending email report with ${total_changes} total changes..."
send_email "$EMAIL_SUBJECT" "$html_report" "$total_changes"
else
echo "No changes detected across all monitored AS numbers."
fi
echo "=== Monitoring complete ==="
}
# Run main function
main
Why I bother
Most of the AS numbers I'm watching are ones I care about for either firewall reasons (hosting providers I block, networks I allowlist) or operational reasons (my own and friends' networks). When one of those rolls out a new /24 or pulls a /22 it'd be nice to know that day rather than the third time something downstream gets weird.
It's not glamorous. It's a bash script and a cron entry. But that's the right shape for a problem this small — anything more elaborate would be over-engineering, and it'd quietly stop working in six months when I forgot which container it was running in.
asn.ipinfo.app makes this easy: one URL per ASN, plain text, parseable with comm. If you've got similar monitoring needs and a Linux box anywhere on your network, this script is short enough to read top-to-bottom in a minute and customise to taste.
https://asn.ipinfo.app/api/text/list/AS<number>
That's the whole interface. Pick your ASNs, point the script at them, walk away.