#!/bin/bash # Tor exit node blocker for relay.mleku.dev (orly) # Fetches the Tor bulk exit list, builds/refreshes an nftables set, and drops # all packets from those IPs. Refreshed every 6h by a systemd timer. # # Sources (in order): # 1. https://check.torproject.org/torbulkexitlist (canonical exit IPs only) # 2. https://www.dan.me.uk/torlist (fallback, also relays) # If every fetch fails, the existing ruleset is kept untouched. set -euo pipefail CONF="/etc/nftables/tor-exit.conf" FETCH_TIMEOUT=20 TABLE="torblock" CHAIN="torblock_drop" log() { echo "[tor-exit-update] $(date -Is) $*" >&2; } fetch_list() { local url="$1" curl -fsSL --connect-timeout "$FETCH_TIMEOUT" --max-time 60 "$url" 2>/dev/null \ | grep -E '^[0-9]{1,3}(\.[0-9]{1,3}){3}$' || true } # Validate that every line is a bare IPv4 address (defensive against # a compromised/mangled upstream returning anything else). validate_ips() { awk -F. 'NF==4 && $1<=255 && $2<=255 && $3<=255 && $4<=255 && $1>0' 2>/dev/null || true } list="" if tmp="$(fetch_list "https://check.torproject.org/torbulkexitlist")"; then list="$(printf '%s\n' "$tmp" | validate_ips)" log "fetched torproject exitlist: $(printf '%s\n' "$list" | sed '/^$/d' | wc -l) IPs" fi if [ -z "$list" ] && tmp="$(fetch_list "https://www.dan.me/torlist")"; then list="$(printf '%s\n' "$tmp" | validate_ips)" log "fetched dan.me fallback: $(printf '%s\n' "$list" | sed '/^$/d' | wc -l) IPs" fi if [ -z "$list" ]; then log "no list fetched, keeping existing ruleset" exit 0 fi # Deduplicate while preserving numeric order. list="$(printf '%s\n' "$list" | sed '/^$/d' | sort -u -t. -k1,1n -k2,2n -k3,3n -k4,4n)" # Build the nftables ruleset: a set of exit IPs and a base chain that drops # them as the highest-priority filter chain so the drop wins before Docker's # accept rules on 80/443. { echo "table ip $TABLE {" echo " set exits {" echo " type ipv4_addr" echo " flags interval" echo " elements = {" # 8 IPs per line for readability printf '%s\n' "$list" | paste -sd' ' - | awk '{for(i=1;i<=NF;i++){printf "%s%s",$i,(i "$CONF.tmp" # Verify the generated ruleset parses before installing. if ! nft -c -f "$CONF.tmp" >/dev/null 2>&1; then log "generated ruleset failed to parse, keeping existing ruleset" rm -f "$CONF.tmp" exit 1 fi # Atomically replace: delete the old table (idempotent) and load the new one. nft delete table ip "$TABLE" 2>/dev/null || true nft -f "$CONF.tmp" mv "$CONF.tmp" "$CONF" log "applied $(printf '%s\n' "$list" | wc -l) exit IPs to nftables ($CONF)"