Added first version of the files. This is not the final versions and not for production
This commit is contained in:
@@ -0,0 +1,463 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -x
|
||||
set -o pipefail
|
||||
|
||||
# Configuration
|
||||
BANLIST_URL="https://email.example.com/f2b-banlist?id=xxxxxxxxxxxxxxxxxxxxx"
|
||||
DISCORD_WEBHOOK_URL="https://api.fluxer.app/webhooks/xxxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
DB_PATH="${DB_PATH:-"$SCRIPT_DIR/banlist.db"}"
|
||||
LOG_FILE="${LOG_FILE:-"$SCRIPT_DIR/banlist_monitor.log"}"
|
||||
TEMP_DIR="${TMPDIR:-/tmp}/banlist_monitor.$$"
|
||||
TIMEOUT="${TIMEOUT:-10}"
|
||||
LOCK_FILE="${LOCK_FILE:-/tmp/banlist_monitor.lock}"
|
||||
|
||||
mkdir -p "$TEMP_DIR"
|
||||
trap 'rm -rf -- "$TEMP_DIR"' EXIT INT TERM
|
||||
|
||||
# Logging function
|
||||
log() {
|
||||
local level=$1
|
||||
shift
|
||||
|
||||
local message="$*"
|
||||
local timestamp
|
||||
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
printf '[%s] [%s] %s\n' "$timestamp" "$level" "$message" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Escape a value for use inside a SQLite string literal
|
||||
sql_escape() {
|
||||
local value=$1
|
||||
value=${value//\'/\'\'}
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
# Initialize database
|
||||
init_database() {
|
||||
if [[ ! -f "$DB_PATH" ]]; then
|
||||
sqlite3 "$DB_PATH" <<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS banned_ips (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip_address TEXT UNIQUE NOT NULL,
|
||||
banned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ban_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip_address TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
details TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ip_status
|
||||
ON banned_ips(ip_address, status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_history_ip
|
||||
ON ban_history(ip_address);
|
||||
SQL
|
||||
|
||||
log "INFO" "Database initialized"
|
||||
else
|
||||
# Ensure older databases receive any missing objects.
|
||||
sqlite3 "$DB_PATH" <<'SQL'
|
||||
CREATE TABLE IF NOT EXISTS banned_ips (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip_address TEXT UNIQUE NOT NULL,
|
||||
banned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT DEFAULT 'active'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ban_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
ip_address TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
details TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ip_status
|
||||
ON banned_ips(ip_address, status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_history_ip
|
||||
ON ban_history(ip_address);
|
||||
SQL
|
||||
fi
|
||||
}
|
||||
|
||||
# Fetch ban list from URL
|
||||
fetch_banlist() {
|
||||
local output_file="$TEMP_DIR/banlist_current.txt"
|
||||
local cleaned_file="$TEMP_DIR/banlist_current.cleaned.txt"
|
||||
local downloaded_file="$TEMP_DIR/banlist_download.txt"
|
||||
|
||||
if ! curl \
|
||||
--fail \
|
||||
--silent \
|
||||
--show-error \
|
||||
--location \
|
||||
--connect-timeout "$TIMEOUT" \
|
||||
--max-time "$TIMEOUT" \
|
||||
"$BANLIST_URL" \
|
||||
-o "$downloaded_file" \
|
||||
2>>"$LOG_FILE"; then
|
||||
|
||||
log "ERROR" "Failed to fetch ban list"
|
||||
return 1
|
||||
fi
|
||||
|
||||
awk '
|
||||
{
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", $0)
|
||||
|
||||
# Accept IPv4/IPv6 addresses with optional CIDR notation.
|
||||
if ($0 != "" && $0 !~ /[^0-9A-Fa-f:.\/]/) {
|
||||
print $0
|
||||
}
|
||||
}
|
||||
' "$downloaded_file" | sort -u > "$cleaned_file"
|
||||
|
||||
if [[ ! -s "$cleaned_file" && -s "$downloaded_file" ]]; then
|
||||
log "ERROR" "Downloaded ban list contained no valid IP addresses"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv -- "$cleaned_file" "$output_file"
|
||||
|
||||
local count
|
||||
count=$(wc -l < "$output_file")
|
||||
|
||||
log "INFO" "Fetched $count banned IPs"
|
||||
printf '%s\n' "$output_file"
|
||||
}
|
||||
|
||||
# Get currently tracked active IPs from database
|
||||
get_tracked_ips() {
|
||||
local output_file="$TEMP_DIR/ips_tracked.txt"
|
||||
|
||||
sqlite3 "$DB_PATH" \
|
||||
"SELECT ip_address
|
||||
FROM banned_ips
|
||||
WHERE status = 'active'
|
||||
ORDER BY ip_address;" > "$output_file"
|
||||
|
||||
printf '%s\n' "$output_file"
|
||||
}
|
||||
|
||||
# Add IP to database
|
||||
add_ip() {
|
||||
local original_ip=$1
|
||||
local original_action=${2:-banned}
|
||||
local original_details=${3:-New IP banned}
|
||||
|
||||
local ip
|
||||
local action
|
||||
local details
|
||||
|
||||
ip=$(sql_escape "$original_ip")
|
||||
action=$(sql_escape "$original_action")
|
||||
details=$(sql_escape "$original_details")
|
||||
|
||||
sqlite3 "$DB_PATH" <<SQL
|
||||
INSERT OR IGNORE INTO banned_ips (ip_address, status)
|
||||
VALUES ('$ip', 'active');
|
||||
|
||||
UPDATE banned_ips
|
||||
SET status = 'active'
|
||||
WHERE ip_address = '$ip';
|
||||
|
||||
INSERT INTO ban_history (ip_address, action, details)
|
||||
VALUES ('$ip', '$action', '$details');
|
||||
SQL
|
||||
|
||||
log "INFO" "Added IP to database: $original_ip"
|
||||
}
|
||||
|
||||
# Mark IP as unbanned
|
||||
remove_ip() {
|
||||
local original_ip=$1
|
||||
local ip
|
||||
|
||||
ip=$(sql_escape "$original_ip")
|
||||
|
||||
sqlite3 "$DB_PATH" <<SQL
|
||||
UPDATE banned_ips
|
||||
SET status = 'inactive'
|
||||
WHERE ip_address = '$ip';
|
||||
|
||||
INSERT INTO ban_history (ip_address, action, details)
|
||||
VALUES ('$ip', 'unbanned', 'IP removed from ban list');
|
||||
SQL
|
||||
|
||||
log "INFO" "Marked IP as unbanned: $original_ip"
|
||||
}
|
||||
|
||||
# Format IPs for the notification
|
||||
format_ips_for_discord() {
|
||||
local ips_string=$1
|
||||
|
||||
printf '%s\n' "$ips_string" |
|
||||
awk 'NF { printf "`%s`\n", $0 }'
|
||||
}
|
||||
|
||||
# Send a notification with all changed IPs
|
||||
send_discord_notification() {
|
||||
local action=$1
|
||||
local ips_string=$2
|
||||
|
||||
if [[ -z "$DISCORD_WEBHOOK_URL" ]]; then
|
||||
log "WARN" "Discord webhook URL not configured"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$ips_string" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
local color
|
||||
local title
|
||||
local count
|
||||
local timestamp
|
||||
local description
|
||||
local payload
|
||||
|
||||
count=$(printf '%s\n' "$ips_string" | awk 'NF { count++ } END { print count + 0 }')
|
||||
timestamp=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||
description=$(format_ips_for_discord "$ips_string")
|
||||
|
||||
if [[ "$action" == "banned" ]]; then
|
||||
color=16711680
|
||||
title="🔴 IPs Banned ($count)"
|
||||
else
|
||||
color=65280
|
||||
title="🟢 IPs Unbanned ($count)"
|
||||
fi
|
||||
|
||||
payload=$(
|
||||
jq -n \
|
||||
--arg title "$title" \
|
||||
--arg description "$description" \
|
||||
--arg timestamp "$timestamp" \
|
||||
--argjson color "$color" \
|
||||
--argjson count "$count" \
|
||||
'{
|
||||
embeds: [
|
||||
{
|
||||
title: $title,
|
||||
description: $description,
|
||||
color: $color,
|
||||
fields: [
|
||||
{
|
||||
name: "Total IPs",
|
||||
value: ($count | tostring),
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: "Timestamp",
|
||||
value: $timestamp,
|
||||
inline: true
|
||||
}
|
||||
],
|
||||
footer: {
|
||||
text: "F2B Ban List Monitor"
|
||||
}
|
||||
}
|
||||
]
|
||||
}'
|
||||
)
|
||||
|
||||
if curl \
|
||||
--fail \
|
||||
--silent \
|
||||
--show-error \
|
||||
--location \
|
||||
--max-time "$TIMEOUT" \
|
||||
-X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data "$payload" \
|
||||
"$DISCORD_WEBHOOK_URL" \
|
||||
>/dev/null \
|
||||
2>>"$LOG_FILE"; then
|
||||
|
||||
log "INFO" "Discord notification sent for $action with $count IPs"
|
||||
else
|
||||
log "ERROR" "Failed to send Discord notification"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Find IPs present in the current list but not the tracked list
|
||||
find_new_ips() {
|
||||
local current_file=$1
|
||||
local tracked_file=$2
|
||||
|
||||
comm -23 "$current_file" "$tracked_file"
|
||||
}
|
||||
|
||||
# Find tracked IPs no longer present in the current list
|
||||
find_removed_ips() {
|
||||
local current_file=$1
|
||||
local tracked_file=$2
|
||||
|
||||
comm -13 "$current_file" "$tracked_file"
|
||||
}
|
||||
|
||||
# Main monitoring function
|
||||
check_for_changes() {
|
||||
log "INFO" "Starting ban list check"
|
||||
|
||||
local current_file
|
||||
local tracked_file
|
||||
local new_bans
|
||||
local unbanned
|
||||
|
||||
if ! current_file=$(fetch_banlist); then
|
||||
log "ERROR" "Failed to fetch ban list, skipping check"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! tracked_file=$(get_tracked_ips); then
|
||||
log "ERROR" "Failed to read tracked IPs"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Find newly banned IPs
|
||||
new_bans=$(find_new_ips "$current_file" "$tracked_file")
|
||||
|
||||
if [[ -n "$new_bans" ]]; then
|
||||
log "INFO" "Found $(printf '%s\n' "$new_bans" | awk 'NF { count++ } END { print count + 0 }') new bans"
|
||||
|
||||
while IFS= read -r ip; do
|
||||
[[ -n "$ip" ]] || continue
|
||||
add_ip "$ip" "banned" "New IP banned"
|
||||
done <<< "$new_bans"
|
||||
|
||||
send_discord_notification "banned" "$new_bans"
|
||||
fi
|
||||
|
||||
# Find unbanned IPs
|
||||
unbanned=$(find_removed_ips "$current_file" "$tracked_file")
|
||||
|
||||
if [[ -n "$unbanned" ]]; then
|
||||
log "INFO" "Found $(printf '%s\n' "$unbanned" | awk 'NF { count++ } END { print count + 0 }') unbans"
|
||||
|
||||
while IFS= read -r ip; do
|
||||
[[ -n "$ip" ]] || continue
|
||||
remove_ip "$ip"
|
||||
done <<< "$unbanned"
|
||||
|
||||
send_discord_notification "unbanned" "$unbanned"
|
||||
fi
|
||||
|
||||
if [[ -z "$new_bans" && -z "$unbanned" ]]; then
|
||||
log "INFO" "No changes detected"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get statistics
|
||||
get_statistics() {
|
||||
local active
|
||||
local inactive
|
||||
local total
|
||||
|
||||
active=$(sqlite3 "$DB_PATH" \
|
||||
"SELECT COUNT(*) FROM banned_ips WHERE status = 'active';")
|
||||
|
||||
inactive=$(sqlite3 "$DB_PATH" \
|
||||
"SELECT COUNT(*) FROM banned_ips WHERE status = 'inactive';")
|
||||
|
||||
total=$((active + inactive))
|
||||
|
||||
log "INFO" \
|
||||
"Ban list statistics - Active: $active, Inactive: $inactive, Total: $total"
|
||||
|
||||
printf 'Active: %s\nInactive: %s\nTotal: %s\n' \
|
||||
"$active" "$inactive" "$total"
|
||||
}
|
||||
|
||||
# View recent history
|
||||
view_history() {
|
||||
echo "=== Recent Ban History ==="
|
||||
|
||||
sqlite3 -header -column "$DB_PATH" \
|
||||
"SELECT ip_address, action, timestamp
|
||||
FROM ban_history
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 20;"
|
||||
}
|
||||
|
||||
# View active banned IPs
|
||||
view_active_bans() {
|
||||
echo "=== Currently Active Banned IPs ==="
|
||||
|
||||
sqlite3 "$DB_PATH" \
|
||||
"SELECT ip_address, banned_at
|
||||
FROM banned_ips
|
||||
WHERE status = 'active'
|
||||
ORDER BY banned_at DESC;"
|
||||
}
|
||||
|
||||
# Check required commands
|
||||
check_dependencies() {
|
||||
local cmd
|
||||
|
||||
for cmd in curl sqlite3 jq flock awk sort comm date wc; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "ERROR: $cmd is required but not installed" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Run the monitor
|
||||
main() {
|
||||
check_dependencies
|
||||
|
||||
# Prevent overlapping cron/systemd runs.
|
||||
exec 9>"$LOCK_FILE"
|
||||
|
||||
if ! flock -n 9; then
|
||||
echo "Another instance is already running"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
init_database
|
||||
check_for_changes
|
||||
get_statistics
|
||||
}
|
||||
|
||||
# Handle command-line arguments
|
||||
case "${1:-}" in
|
||||
history)
|
||||
check_dependencies
|
||||
init_database
|
||||
view_history
|
||||
;;
|
||||
|
||||
stats)
|
||||
check_dependencies
|
||||
init_database
|
||||
get_statistics
|
||||
;;
|
||||
|
||||
active)
|
||||
check_dependencies
|
||||
init_database
|
||||
view_active_bans
|
||||
;;
|
||||
|
||||
"")
|
||||
main
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage: $0 [history|stats|active]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -1,3 +1,44 @@
|
||||
# Mailcow-Ban-Check
|
||||
|
||||
This Bash script monitors a remote IP ban list, compares it with previously tracked IPs in SQLite, records changes, and sends notifications to a Discord-compatible webhook
|
||||
This Bash script monitors a remote IP ban list, compares it with previously tracked IPs in SQLite, records changes, and sends notifications to a Discord-compatible webhook
|
||||
|
||||
# Still in development
|
||||
# Not for Production
|
||||
|
||||
## !!! Important: the URLs placed into this script will contain credentials/access tokens or API Keys. Treat this script as sensitive. These credentials will be stored in a more secure way in a future update !!!
|
||||
|
||||
A Bash-based monitoring tool that tracks changes to a remote IP ban list.
|
||||
|
||||
The script downloads a ban list, compares it with previously tracked addresses in a local SQLite database, records new bans and removals, and sends notifications to a Discord-compatible webhook.
|
||||
|
||||
### Features
|
||||
|
||||
- Downloads a remote ban list over HTTPS
|
||||
- Supports IPv4, IPv6, and CIDR notation
|
||||
- Removes duplicates and surrounding whitespace
|
||||
- Tracks active and inactive IP addresses
|
||||
- Maintains a ban history
|
||||
- Sends notifications for:
|
||||
- Newly banned IPs
|
||||
- IPs removed from the ban list
|
||||
- Prevents overlapping executions with `flock`
|
||||
- Supports cron and systemd timer execution
|
||||
- Provides commands for viewing history, statistics, and active bans
|
||||
|
||||
### Requirements
|
||||
|
||||
The following commands must be installed:
|
||||
|
||||
- Bash
|
||||
- `curl`
|
||||
- `sqlite3`
|
||||
- `jq`
|
||||
- `flock`
|
||||
- `awk`
|
||||
- `sort`
|
||||
- `comm`
|
||||
- `date`
|
||||
- `wc`
|
||||
|
||||
|
||||
## !!! Important: the URLs placed into this script will contain credentials/access tokens or API Keys. Treat this script as sensitive. These credentials will be stored in a more secure way in a future update !!!
|
||||
|
||||
Reference in New Issue
Block a user