Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
This commit is contained in:
@@ -0,0 +1,507 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#########################################################################################
|
||||
#### route53-dns-backup.sh — Export AWS Route 53 zones to BIND zone file format ####
|
||||
#### Supports diff, S3 upload, retention, and zone restore ####
|
||||
#### Requires: bash 4+, aws-cli v2, jq ####
|
||||
#### ####
|
||||
#### Author: Phil Connor ####
|
||||
#### Contact: contact@mylinux.work ####
|
||||
#### License: MIT ####
|
||||
#### Version 1.00 ####
|
||||
#### ####
|
||||
#### Usage: ####
|
||||
#### ./route53-dns-backup.sh --export ####
|
||||
#### ./route53-dns-backup.sh --export --s3-bucket my-dns-backups ####
|
||||
#### ####
|
||||
#### See --help for all options. ####
|
||||
#########################################################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Defaults ──────────────────────────────────────────────────────────
|
||||
AWS_REGION="${AWS_REGION:-us-east-1}"
|
||||
BACKUP_DIR="${R53_BACKUP_DIR:-$HOME/route53-backups}"
|
||||
S3_BUCKET="${R53_S3_BUCKET:-}"
|
||||
S3_PREFIX="${R53_S3_PREFIX:-route53-backups}"
|
||||
RETAIN_DAYS="${R53_RETAIN_DAYS:-90}"
|
||||
VERBOSE="${VERBOSE:-false}"
|
||||
COLOR="${COLOR:-auto}"
|
||||
|
||||
# ── State ─────────────────────────────────────────────────────────────
|
||||
SCRIPT_NAME="$(basename "$0")"
|
||||
readonly SCRIPT_NAME
|
||||
RUN_MODE=""
|
||||
ZONE_ID=""
|
||||
RESTORE_FILE=""
|
||||
DRY_RUN="false"
|
||||
DIFF_MODE="false"
|
||||
OUTPUT_DIR=""
|
||||
TOTAL_ZONES=0
|
||||
TOTAL_RECORDS=0
|
||||
START_TIME=""
|
||||
|
||||
# ── Colors ────────────────────────────────────────────────────────────
|
||||
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" DIM="" RESET=""
|
||||
setup_colors() {
|
||||
if [[ "$COLOR" == "never" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ "$COLOR" == "auto" && ! -t 1 ]]; then
|
||||
return
|
||||
fi
|
||||
RED="\033[0;31m"
|
||||
GREEN="\033[0;32m"
|
||||
YELLOW="\033[0;33m"
|
||||
BLUE="\033[0;34m"
|
||||
BOLD="\033[1m"
|
||||
DIM="\033[2m"
|
||||
RESET="\033[0m"
|
||||
}
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────
|
||||
die() { printf "%b\n" "${RED}[ERROR]${RESET} $*" >&2; exit 1; }
|
||||
log_info() { printf "%b\n" "${GREEN}[INFO]${RESET} $*"; }
|
||||
log_warn() { printf "%b\n" "${YELLOW}[WARN]${RESET} $*"; }
|
||||
log_verbose() { [[ "$VERBOSE" == "true" ]] && printf "%b\n" "${DIM}[DEBUG]${RESET} $*" || true; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $SCRIPT_NAME [OPTIONS]
|
||||
|
||||
Export AWS Route 53 hosted zones to BIND zone file format.
|
||||
Supports diff, S3 upload, local retention, and zone restore.
|
||||
|
||||
Modes:
|
||||
--export Export hosted zones to BIND format
|
||||
--restore Restore records from a BIND zone file
|
||||
|
||||
Options:
|
||||
--zone-id ID Target a specific hosted zone (default: all)
|
||||
--diff Compare with most recent previous backup
|
||||
--s3-bucket BUCKET Upload backups to S3
|
||||
--s3-prefix PREFIX S3 key prefix (default: route53-backups)
|
||||
--file PATH BIND zone file for restore
|
||||
--dry-run Preview restore without applying
|
||||
--output-dir DIR Local backup directory (default: ~/route53-backups)
|
||||
--retain-days N Delete local backups older than N days (default: 90)
|
||||
--verbose Debug output
|
||||
--no-color Disable colored output
|
||||
--help Show this help
|
||||
|
||||
Environment Variables:
|
||||
AWS_PROFILE AWS CLI profile
|
||||
AWS_REGION AWS region (default: us-east-1)
|
||||
R53_BACKUP_DIR Backup directory (default: ~/route53-backups)
|
||||
R53_S3_BUCKET S3 bucket for uploads
|
||||
R53_S3_PREFIX S3 key prefix (default: route53-backups)
|
||||
R53_RETAIN_DAYS Local retention days (default: 90)
|
||||
|
||||
Examples:
|
||||
$SCRIPT_NAME --export
|
||||
$SCRIPT_NAME --export --diff --s3-bucket my-dns-backups
|
||||
$SCRIPT_NAME --export --zone-id Z0123456789ABCDEFGHIJ
|
||||
$SCRIPT_NAME --restore --zone-id Z0123456789ABCDEFGHIJ --file backup.zone --dry-run
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ── Dependency Check ──────────────────────────────────────────────────
|
||||
check_deps() {
|
||||
local missing=()
|
||||
for cmd in aws jq; do
|
||||
if ! command -v "$cmd" &>/dev/null; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
done
|
||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||
die "Missing required commands: ${missing[*]}"
|
||||
fi
|
||||
log_verbose "Dependencies OK: aws, jq"
|
||||
}
|
||||
|
||||
# ── Argument Parsing ──────────────────────────────────────────────────
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--export) RUN_MODE="export"; shift ;;
|
||||
--restore) RUN_MODE="restore"; shift ;;
|
||||
--zone-id) ZONE_ID="${2:?--zone-id requires a value}"; shift 2 ;;
|
||||
--diff) DIFF_MODE="true"; shift ;;
|
||||
--s3-bucket) S3_BUCKET="${2:?--s3-bucket requires a value}"; shift 2 ;;
|
||||
--s3-prefix) S3_PREFIX="${2:?--s3-prefix requires a value}"; shift 2 ;;
|
||||
--file) RESTORE_FILE="${2:?--file requires a value}"; shift 2 ;;
|
||||
--dry-run) DRY_RUN="true"; shift ;;
|
||||
--output-dir) OUTPUT_DIR="${2:?--output-dir requires a value}"; shift 2 ;;
|
||||
--retain-days) RETAIN_DAYS="${2:?--retain-days requires a value}"; shift 2 ;;
|
||||
--verbose) VERBOSE="true"; shift ;;
|
||||
--no-color) COLOR="never"; shift ;;
|
||||
--help) usage ;;
|
||||
*) die "Unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -z "$RUN_MODE" ]] && die "Specify --export or --restore. See --help."
|
||||
|
||||
if [[ "$RUN_MODE" == "restore" ]]; then
|
||||
[[ -z "$ZONE_ID" ]] && die "--restore requires --zone-id"
|
||||
[[ -z "$RESTORE_FILE" ]] && die "--restore requires --file"
|
||||
[[ ! -f "$RESTORE_FILE" ]] && die "File not found: $RESTORE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── AWS Identity ──────────────────────────────────────────────────────
|
||||
get_account_id() {
|
||||
aws sts get-caller-identity --query 'Account' --output text 2>/dev/null || echo "unknown"
|
||||
}
|
||||
|
||||
# ── List Hosted Zones ─────────────────────────────────────────────────
|
||||
list_hosted_zones() {
|
||||
if [[ -n "$ZONE_ID" ]]; then
|
||||
aws route53 get-hosted-zone --id "$ZONE_ID" --output json 2>/dev/null | \
|
||||
jq '[{Id: .HostedZone.Id, Name: .HostedZone.Name, Private: .HostedZone.Config.PrivateZone, RecordCount: .HostedZone.ResourceRecordSetCount}]'
|
||||
else
|
||||
local zones="[]"
|
||||
local marker=""
|
||||
while true; do
|
||||
local cmd=(aws route53 list-hosted-zones --output json --max-items 100)
|
||||
[[ -n "$marker" ]] && cmd+=(--starting-token "$marker")
|
||||
|
||||
local result
|
||||
result=$("${cmd[@]}" 2>/dev/null)
|
||||
|
||||
zones=$(echo "$zones" "$result" | jq -s '.[0] + [.[1].HostedZones[] | {Id: .Id, Name: .Name, Private: .Config.PrivateZone, RecordCount: .ResourceRecordSetCount}]')
|
||||
|
||||
marker=$(echo "$result" | jq -r '.NextToken // empty')
|
||||
[[ -z "$marker" ]] && break
|
||||
done
|
||||
echo "$zones"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Export Zone to BIND Format ────────────────────────────────────────
|
||||
export_zone() {
|
||||
local zone_id="$1"
|
||||
local zone_name="$2"
|
||||
local is_private="$3"
|
||||
local output_file="$4"
|
||||
|
||||
# Strip trailing dot for display
|
||||
local display_name="${zone_name%.}"
|
||||
|
||||
log_verbose "Exporting zone: $display_name ($zone_id)"
|
||||
|
||||
# Fetch all records
|
||||
local records
|
||||
records=$(aws route53 list-resource-record-sets \
|
||||
--hosted-zone-id "$zone_id" \
|
||||
--output json 2>/dev/null)
|
||||
|
||||
local record_count
|
||||
record_count=$(echo "$records" | jq '.ResourceRecordSets | length')
|
||||
|
||||
# Write BIND zone file
|
||||
{
|
||||
echo "; Zone file for $display_name"
|
||||
echo "; Exported from AWS Route 53 on $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
echo "; Hosted Zone ID: $zone_id"
|
||||
[[ "$is_private" == "true" ]] && echo "; Private hosted zone"
|
||||
echo ";"
|
||||
echo "\$ORIGIN ${zone_name}"
|
||||
echo ""
|
||||
|
||||
# Process each record set
|
||||
echo "$records" | jq -r '.ResourceRecordSets[] | @json' | while IFS= read -r rr; do
|
||||
local rr_name rr_type rr_ttl
|
||||
rr_name=$(echo "$rr" | jq -r '.Name')
|
||||
rr_type=$(echo "$rr" | jq -r '.Type')
|
||||
rr_ttl=$(echo "$rr" | jq -r '.TTL // 300')
|
||||
|
||||
# Handle ALIAS records (not supported in BIND)
|
||||
local alias_target
|
||||
alias_target=$(echo "$rr" | jq -r '.AliasTarget.DNSName // empty')
|
||||
if [[ -n "$alias_target" ]]; then
|
||||
echo "; ALIAS $rr_name $rr_type → $alias_target (Route 53 alias — no BIND equivalent)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Standard resource records
|
||||
echo "$rr" | jq -r '.ResourceRecords[]?.Value // empty' | while IFS= read -r value; do
|
||||
[[ -z "$value" ]] && continue
|
||||
printf "%-40s %s IN %-8s %s\n" "$rr_name" "$rr_ttl" "$rr_type" "$value"
|
||||
done
|
||||
done
|
||||
} > "$output_file"
|
||||
|
||||
local private_label=""
|
||||
[[ "$is_private" == "true" ]] && private_label=" (private)"
|
||||
|
||||
printf " %b %s (%s) — %d records → %s%s\n" \
|
||||
"${GREEN}✓${RESET}" "$display_name" "${zone_id##*/}" "$record_count" \
|
||||
"$(basename "$output_file")" "$private_label"
|
||||
|
||||
TOTAL_ZONES=$((TOTAL_ZONES + 1))
|
||||
TOTAL_RECORDS=$((TOTAL_RECORDS + record_count))
|
||||
|
||||
# Rate limiting — Route 53 API allows 5 req/s
|
||||
sleep 0.3
|
||||
}
|
||||
|
||||
# ── Diff Against Previous Backup ──────────────────────────────────────
|
||||
diff_zone() {
|
||||
local zone_name="$1"
|
||||
local current_file="$2"
|
||||
local backup_base="$3"
|
||||
|
||||
local display_name="${zone_name%.}"
|
||||
local zone_filename
|
||||
zone_filename=$(sanitize_filename "$zone_name")
|
||||
|
||||
# Find the most recent previous backup
|
||||
local prev_dir=""
|
||||
local today_dir
|
||||
today_dir=$(basename "$(dirname "$current_file")")
|
||||
|
||||
if [[ -d "$backup_base" ]]; then
|
||||
prev_dir=$(find "$backup_base" -maxdepth 1 -mindepth 1 -type d \
|
||||
-not -name "$today_dir" | sort -r | head -1)
|
||||
fi
|
||||
|
||||
if [[ -z "$prev_dir" || ! -f "$prev_dir/$zone_filename" ]]; then
|
||||
log_verbose "No previous backup found for $display_name — skipping diff"
|
||||
return
|
||||
fi
|
||||
|
||||
local prev_file="$prev_dir/$zone_filename"
|
||||
|
||||
# Filter out comments and blank lines for comparison
|
||||
local diff_output
|
||||
diff_output=$(diff \
|
||||
<(grep -v '^;' "$prev_file" | grep -v '^\$' | grep -v '^$' | sort) \
|
||||
<(grep -v '^;' "$current_file" | grep -v '^\$' | grep -v '^$' | sort) \
|
||||
2>/dev/null || true)
|
||||
|
||||
if [[ -z "$diff_output" ]]; then
|
||||
echo "No changes in $display_name"
|
||||
else
|
||||
echo ""
|
||||
printf "%bChanges detected in %s:%b\n" "$YELLOW" "$display_name" "$RESET"
|
||||
echo "$diff_output" | while IFS= read -r line; do
|
||||
case "$line" in
|
||||
">"*) printf " %b+ %s%b\n" "$GREEN" "${line:2}" "$RESET" ;;
|
||||
"<"*) printf " %b- %s%b\n" "$RED" "${line:2}" "$RESET" ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# ── S3 Upload ─────────────────────────────────────────────────────────
|
||||
upload_to_s3() {
|
||||
local local_dir="$1"
|
||||
local date_stamp
|
||||
date_stamp=$(basename "$local_dir")
|
||||
|
||||
local s3_path="s3://${S3_BUCKET}/${S3_PREFIX}/${date_stamp}/"
|
||||
|
||||
log_info "Uploading to $s3_path"
|
||||
|
||||
aws s3 cp "$local_dir/" "$s3_path" \
|
||||
--recursive \
|
||||
--sse AES256 \
|
||||
--quiet \
|
||||
2>/dev/null
|
||||
|
||||
log_info "Upload complete: $s3_path"
|
||||
}
|
||||
|
||||
# ── Local Retention ───────────────────────────────────────────────────
|
||||
enforce_retention() {
|
||||
local backup_base="$1"
|
||||
local days="$2"
|
||||
|
||||
[[ ! -d "$backup_base" ]] && return
|
||||
|
||||
local deleted=0
|
||||
while IFS= read -r dir; do
|
||||
[[ -z "$dir" ]] && continue
|
||||
log_verbose "Deleting old backup: $dir"
|
||||
rm -rf "$dir"
|
||||
deleted=$((deleted + 1))
|
||||
done < <(find "$backup_base" -maxdepth 1 -mindepth 1 -type d -mtime +"$days" 2>/dev/null)
|
||||
|
||||
[[ $deleted -gt 0 ]] && log_info "Deleted $deleted backup(s) older than $days days"
|
||||
}
|
||||
|
||||
# ── Restore Zone ──────────────────────────────────────────────────────
|
||||
restore_zone() {
|
||||
local zone_id="$1"
|
||||
local file="$2"
|
||||
|
||||
log_info "Restoring records to zone $zone_id from $file"
|
||||
[[ "$DRY_RUN" == "true" ]] && log_warn "DRY-RUN mode — no changes will be applied"
|
||||
|
||||
local changes='{"Changes":[]}'
|
||||
local record_count=0
|
||||
|
||||
while IFS= read -r line; do
|
||||
# Skip comments, directives, and blank lines
|
||||
[[ "$line" =~ ^[[:space:]]*$ ]] && continue
|
||||
[[ "$line" =~ ^[[:space:]]*\; ]] && continue
|
||||
[[ "$line" =~ ^\$ ]] && continue
|
||||
|
||||
# Parse BIND record: name ttl IN type value
|
||||
local rr_name rr_ttl rr_type rr_value
|
||||
read -r rr_name rr_ttl _ rr_type rr_value <<< "$line"
|
||||
|
||||
[[ -z "$rr_type" || -z "$rr_value" ]] && continue
|
||||
|
||||
# Skip SOA and NS records (Route 53 manages these)
|
||||
[[ "$rr_type" == "SOA" || "$rr_type" == "NS" ]] && continue
|
||||
|
||||
local change
|
||||
change=$(jq -n \
|
||||
--arg action "UPSERT" \
|
||||
--arg name "$rr_name" \
|
||||
--arg type "$rr_type" \
|
||||
--argjson ttl "$rr_ttl" \
|
||||
--arg value "$rr_value" \
|
||||
'{Action: $action, ResourceRecordSet: {Name: $name, Type: $type, TTL: $ttl, ResourceRecords: [{Value: $value}]}}')
|
||||
|
||||
changes=$(echo "$changes" | jq --argjson c "$change" '.Changes += [$c]')
|
||||
record_count=$((record_count + 1))
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
printf " %b[DRY-RUN]%b UPSERT %s %s %s → %s\n" "$YELLOW" "$RESET" "$rr_name" "$rr_ttl" "$rr_type" "$rr_value"
|
||||
fi
|
||||
done < "$file"
|
||||
|
||||
if [[ $record_count -eq 0 ]]; then
|
||||
log_warn "No restorable records found in $file"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "DRY-RUN: $record_count record(s) would be upserted"
|
||||
return
|
||||
fi
|
||||
|
||||
# Batch changes (Route 53 allows up to 1000 per request)
|
||||
local batch_size=500
|
||||
local total_changes
|
||||
total_changes=$(echo "$changes" | jq '.Changes | length')
|
||||
local batches=$(( (total_changes + batch_size - 1) / batch_size ))
|
||||
|
||||
for ((i = 0; i < batches; i++)); do
|
||||
local offset=$((i * batch_size))
|
||||
local batch
|
||||
batch=$(echo "$changes" | jq "{Comment: \"Restore from backup\", Changes: .Changes[$offset:$((offset + batch_size))]}")
|
||||
|
||||
log_verbose "Submitting batch $((i + 1))/$batches ($batch_size records)"
|
||||
|
||||
aws route53 change-resource-record-sets \
|
||||
--hosted-zone-id "$zone_id" \
|
||||
--change-batch "$batch" \
|
||||
--output text \
|
||||
2>/dev/null
|
||||
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
log_info "Restored $record_count record(s) to zone $zone_id"
|
||||
}
|
||||
|
||||
# ── Filename Sanitization ─────────────────────────────────────────────
|
||||
sanitize_filename() {
|
||||
local name="$1"
|
||||
name="${name%.}"
|
||||
echo "${name}.zone"
|
||||
}
|
||||
|
||||
# ── Main: Export ──────────────────────────────────────────────────────
|
||||
run_export() {
|
||||
local account_id
|
||||
account_id=$(get_account_id)
|
||||
local date_stamp
|
||||
date_stamp=$(date -u '+%Y-%m-%d')
|
||||
|
||||
local target_dir="${OUTPUT_DIR:-$BACKUP_DIR}/$date_stamp"
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
echo ""
|
||||
printf "%bRoute 53 DNS Backup%b\n" "$BOLD" "$RESET"
|
||||
echo "Account: $account_id"
|
||||
echo "Time: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
if [[ -n "$ZONE_ID" ]]; then
|
||||
echo "Mode: export (zone $ZONE_ID)"
|
||||
else
|
||||
echo "Mode: export (all zones)"
|
||||
fi
|
||||
echo ""
|
||||
echo "Exporting hosted zones..."
|
||||
|
||||
local zones
|
||||
zones=$(list_hosted_zones)
|
||||
|
||||
local zone_count
|
||||
zone_count=$(echo "$zones" | jq 'length')
|
||||
|
||||
if [[ "$zone_count" -eq 0 ]]; then
|
||||
log_warn "No hosted zones found"
|
||||
return
|
||||
fi
|
||||
|
||||
for ((i = 0; i < zone_count; i++)); do
|
||||
local z_id z_name z_private
|
||||
z_id=$(echo "$zones" | jq -r ".[$i].Id" | sed 's|/hostedzone/||')
|
||||
z_name=$(echo "$zones" | jq -r ".[$i].Name")
|
||||
z_private=$(echo "$zones" | jq -r ".[$i].Private")
|
||||
|
||||
local zone_file
|
||||
zone_file="$target_dir/$(sanitize_filename "$z_name")"
|
||||
|
||||
export_zone "$z_id" "$z_name" "$z_private" "$zone_file"
|
||||
|
||||
# Diff if requested
|
||||
if [[ "$DIFF_MODE" == "true" ]]; then
|
||||
diff_zone "$z_name" "$zone_file" "${OUTPUT_DIR:-$BACKUP_DIR}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
printf "Backup saved to: %b%s%b\n" "$BLUE" "$target_dir" "$RESET"
|
||||
echo " $TOTAL_ZONES zones, $TOTAL_RECORDS records total"
|
||||
|
||||
# S3 upload
|
||||
if [[ -n "$S3_BUCKET" ]]; then
|
||||
echo ""
|
||||
upload_to_s3 "$target_dir"
|
||||
fi
|
||||
|
||||
# Local retention
|
||||
enforce_retention "${OUTPUT_DIR:-$BACKUP_DIR}" "$RETAIN_DAYS"
|
||||
}
|
||||
|
||||
# ── Main: Restore ─────────────────────────────────────────────────────
|
||||
run_restore() {
|
||||
restore_zone "$ZONE_ID" "$RESTORE_FILE"
|
||||
}
|
||||
|
||||
# ── Entry Point ───────────────────────────────────────────────────────
|
||||
main() {
|
||||
START_TIME=$(date +%s)
|
||||
setup_colors
|
||||
parse_args "$@"
|
||||
check_deps
|
||||
|
||||
case "$RUN_MODE" in
|
||||
export) run_export ;;
|
||||
restore) run_restore ;;
|
||||
*) die "Unknown mode: $RUN_MODE" ;;
|
||||
esac
|
||||
|
||||
local elapsed=$(( $(date +%s) - START_TIME ))
|
||||
echo ""
|
||||
log_info "Completed in ${elapsed}s"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user