Commit 60e7ed97 authored by Vitaly Lipatov's avatar Vitaly Lipatov

router: add unified route-update.sh with directory-based config

Replace ipset+mangle approach with pure ip route tables. Configuration via routes.d/ (IPv4) and routes6.d/ (IPv6) directories where each subdirectory = gateway and .list symlinks = domain/IP lists. Features: - Hash-based change detection (skip if lists unchanged) - Double check: file hash + resolved IPs diff - Batch route loading via ip -batch - Automatic cleanup of orphaned state - --show/--force/--add/--del/--flush options Also adds is_ipv6() and get_ipv6_list_bulk() to functions. Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent be2b56c3
web/config
vz/azbyka/base.task
dns/whois-cache/*
router/.state/
......@@ -19,6 +19,11 @@ is_ipv4()
echo "$1" | grep -q -E "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
}
is_ipv6()
{
echo "$1" | grep -q ':'
}
## Несколько диапазонов в одной строке
# $ expand_range "r[1-2][a-b].test"
# r1a.test
......@@ -96,3 +101,46 @@ get_ipv4_list_bulk()
get_ipv4_list "$domain"
done
}
# Bulk resolve domains to IPv6 via adnshost (async) with dig fallback for CNAMEs
# Usage: get_ipv6_list_bulk < domains.txt
# or: get_ipv6_list_bulk domains.txt
get_ipv6_list_bulk()
{
local domains=$(mktemp)
local adns_out=$(mktemp)
trap "rm -f $domains $adns_out" RETURN
# Read domains, skip IPs (output them directly)
while read -r entry ; do
[ -z "$entry" ] && continue
if is_ipv6 "$entry" ; then
echo "$entry"
elif ! is_ipv4 "$entry" ; then
echo "$entry" >> "$domains"
fi
done < "${1:-/dev/stdin}"
[ -s "$domains" ] || return 0
# Bulk async resolve: local resolver + Google DNS
{
adnshost -a -t aaaa -Fi -f < "$domains"
adnshost -a -t aaaa -Fi -f --config "nameserver 8.8.8.8" < "$domains"
} > "$adns_out" 2>/dev/null
# Output resolved IPs grouped by domain
awk '/ AAAA / { print $1, $3 }' "$adns_out" | sort -u | awk '{
if (domain != $1) { print ""; print "# " $1; domain = $1 }
print $2
}'
# Fallback to dig for CNAME/tempfail domains
grep "^;" "$adns_out" | grep -v 'nxdomain\|nodata\|querydomaintoolong' | \
awk '{ for(i=4;i<=NF;i++) if($i ~ /^[a-z0-9].*\.[a-z]/) { print $i; break } }' | \
sort -u | while read domain ; do
echo
echo "# $domain (dig fallback)"
get_ipv6_list "$domain"
done
}
#!/bin/sh
# Unified route updater: reads directory-based config from routes.d/ and routes6.d/
# Each subdirectory = gateway, .list symlinks inside = domain/IP lists to route through it
#
# Usage: route-update.sh [--force] [--show] [--set-rules] [--flush GATEWAY]
# route-update.sh --add IP|DOMAIN GATEWAY
# route-update.sh --del IP|DOMAIN GATEWAY
cd "$(dirname "$(realpath "$0")")" || exit
. ./functions
IFACE="${IFACE:-eth0}"
IFACE6="${IFACE6:-vmbr0}"
ROUTES_DIR=routes.d
ROUTES6_DIR=routes6.d
STATE_DIR=.state
FORCE=
SHOW=
SET_RULES=
# Parse arguments
ACTION=
ADD_DEL_TARGET=
ADD_DEL_GW=
FLUSH_GW=
while [ -n "$1" ] ; do
case "$1" in
--force) FORCE=1 ;;
--show) SHOW=1 ;;
--set-rules) SET_RULES=1 ;;
--flush) FLUSH_GW="$2" ; shift ;;
--add) ACTION=add ; ADD_DEL_TARGET="$2" ; ADD_DEL_GW="$3" ; shift 2 ;;
--del) ACTION=del ; ADD_DEL_TARGET="$2" ; ADD_DEL_GW="$3" ; shift 2 ;;
*) echo "Unknown option: $1" >&2 ; exit 1 ;;
esac
shift
done
# Helpers
log() { echo "$(date '+%H:%M:%S') $*" ; }
md5_lists()
{
# md5 of concatenated content of all .list files (following symlinks)
cat "$@" 2>/dev/null | md5sum | awk '{print $1}'
}
ensure_state_dir()
{
local dir="$1"
mkdir -p "$STATE_DIR/$dir"
}
# Get table number from IPv4 gateway (last octet)
ipv4_table()
{
echo "${1##*.}"
}
# Get rule priority from table number (table * 10)
rule_pref()
{
echo $(( $1 * 10 ))
}
# --- Manual add/del ---
if [ "$ACTION" = "add" ] ; then
[ -n "$ADD_DEL_TARGET" ] && [ -n "$ADD_DEL_GW" ] || { echo "Usage: $0 --add IP|DOMAIN GATEWAY" >&2 ; exit 1 ; }
table=$(ipv4_table "$ADD_DEL_GW")
if is_ipv4 "$ADD_DEL_TARGET" ; then
ip route replace "$ADD_DEL_TARGET" via "$ADD_DEL_GW" table "$table"
else
get_ipv4_list "$ADD_DEL_TARGET" | sort -u | while read -r ip ; do
[ -n "$ip" ] || continue
ip route replace "$ip" via "$ADD_DEL_GW" table "$table"
done
fi
exit
fi
if [ "$ACTION" = "del" ] ; then
[ -n "$ADD_DEL_TARGET" ] && [ -n "$ADD_DEL_GW" ] || { echo "Usage: $0 --del IP|DOMAIN GATEWAY" >&2 ; exit 1 ; }
table=$(ipv4_table "$ADD_DEL_GW")
if is_ipv4 "$ADD_DEL_TARGET" ; then
ip route del "$ADD_DEL_TARGET" table "$table" 2>/dev/null
else
get_ipv4_list "$ADD_DEL_TARGET" | sort -u | while read -r ip ; do
[ -n "$ip" ] || continue
ip route del "$ip" table "$table" 2>/dev/null
done
fi
exit
fi
# --- Flush a specific gateway ---
if [ -n "$FLUSH_GW" ] ; then
table=$(ipv4_table "$FLUSH_GW")
log "Flushing table $table (gateway $FLUSH_GW)"
[ -z "$SHOW" ] && ip route flush table "$table"
exit
fi
# --- Process IPv4 routes.d/ ---
process_v4()
{
[ -d "$ROUTES_DIR" ] || return 0
for gwdir in "$ROUTES_DIR"/*/ ; do
[ -d "$gwdir" ] || continue
gw=$(basename "$gwdir")
table=$(ipv4_table "$gw")
pref=$(rule_pref "$table")
state="$ROUTES_DIR/$gw"
ensure_state_dir "$state"
# Collect .list files (follow symlinks with ls -L)
lists=$(find -L "$gwdir" -maxdepth 1 -name '*.list' -type f 2>/dev/null | sort)
if [ -z "$lists" ] ; then
log "[$gw] No .list files, flushing table $table"
if [ -z "$SHOW" ] ; then
ip route flush table "$table" 2>/dev/null
ip rule del lookup "$table" 2>/dev/null
rm -f "$STATE_DIR/$state/hash" "$STATE_DIR/$state/resolved"
fi
continue
fi
# Compute hash of all list contents
current_hash=$(md5_lists $lists)
if [ -z "$FORCE" ] && [ -f "$STATE_DIR/$state/hash" ] ; then
read -r saved_hash < "$STATE_DIR/$state/hash"
if [ "$current_hash" = "$saved_hash" ] ; then
[ -n "$SHOW" ] && log "[$gw] No changes (hash match), skipping"
continue
fi
fi
log "[$gw] Changes detected, resolving..."
# Fetch lists and run DNS resolution
resolved_new="$STATE_DIR/$state/resolved.new"
{
for f in $lists ; do
cat_expanded "$f"
done
} | grep -v '^#' | grep -v '^$' | get_ipv4_list_bulk | \
grep -v '^#' | grep -v '^$' | sort -u > "$resolved_new"
# Check if resolved IPs actually changed
if [ -z "$FORCE" ] && [ -f "$STATE_DIR/$state/resolved" ] ; then
if diff -q "$STATE_DIR/$state/resolved" "$resolved_new" >/dev/null 2>&1 ; then
log "[$gw] Resolved IPs unchanged, updating hash only"
echo "$current_hash" > "$STATE_DIR/$state/hash"
rm -f "$resolved_new"
continue
fi
fi
count=$(wc -l < "$resolved_new")
log "[$gw] Loading $count routes into table $table via $gw"
if [ -n "$SHOW" ] ; then
echo " Would flush table $table and load $count routes"
head -5 "$resolved_new" | sed 's/^/ /'
[ "$count" -gt 5 ] && echo " ... ($count total)"
rm -f "$resolved_new"
continue
fi
# Flush and load via batch
ip route flush table "$table"
sed "s|^|route replace |; s|$| via $gw table $table|" "$resolved_new" | \
ip -batch - 2>&1 | grep -v "^$" | head -5
# Ensure ip rule exists
if ! ip rule show | grep -q "lookup $table.*pref $pref" ; then
ip rule add iif "$IFACE" lookup "$table" pref "$pref" 2>/dev/null
fi
# Save state
echo "$current_hash" > "$STATE_DIR/$state/hash"
mv "$resolved_new" "$STATE_DIR/$state/resolved"
log "[$gw] Done"
done
}
# --- Process IPv6 routes6.d/ ---
process_v6()
{
[ -d "$ROUTES6_DIR" ] || return 0
for gwdir in "$ROUTES6_DIR"/*/ ; do
[ -d "$gwdir" ] || continue
dirname=$(basename "$gwdir")
# Read gateway and table from files, or derive from dirname
if [ -f "$gwdir/gateway" ] ; then
read -r gw < "$gwdir/gateway"
else
# Restore IPv6 from dirname: - → :, -- → ::
gw=$(echo "$dirname" | sed 's/--/::/g; s/-/:/g')
fi
if [ -f "$gwdir/table" ] ; then
read -r table < "$gwdir/table"
else
echo "[$dirname] No table file, skipping" >&2
continue
fi
pref=$(rule_pref "$table")
state="$ROUTES6_DIR/$dirname"
ensure_state_dir "$state"
# Collect .list files
lists=$(find -L "$gwdir" -maxdepth 1 -name '*.list' -type f 2>/dev/null | sort)
if [ -z "$lists" ] ; then
log "[$dirname] (v6) No .list files, flushing table $table"
if [ -z "$SHOW" ] ; then
ip -6 route flush table "$table" 2>/dev/null
ip -6 rule del lookup "$table" 2>/dev/null
rm -f "$STATE_DIR/$state/hash" "$STATE_DIR/$state/resolved"
fi
continue
fi
current_hash=$(md5_lists $lists)
if [ -z "$FORCE" ] && [ -f "$STATE_DIR/$state/hash" ] ; then
read -r saved_hash < "$STATE_DIR/$state/hash"
if [ "$current_hash" = "$saved_hash" ] ; then
[ -n "$SHOW" ] && log "[$dirname] (v6) No changes (hash match), skipping"
continue
fi
fi
log "[$dirname] (v6) Changes detected, resolving..."
resolved_new="$STATE_DIR/$state/resolved.new"
{
for f in $lists ; do
cat_expanded "$f"
done
} | grep -v '^#' | grep -v '^$' | get_ipv6_list_bulk | \
grep -v '^#' | grep -v '^$' | sort -u > "$resolved_new"
if [ -z "$FORCE" ] && [ -f "$STATE_DIR/$state/resolved" ] ; then
if diff -q "$STATE_DIR/$state/resolved" "$resolved_new" >/dev/null 2>&1 ; then
log "[$dirname] (v6) Resolved IPs unchanged, updating hash only"
echo "$current_hash" > "$STATE_DIR/$state/hash"
rm -f "$resolved_new"
continue
fi
fi
count=$(wc -l < "$resolved_new")
log "[$dirname] (v6) Loading $count routes into table $table via $gw"
if [ -n "$SHOW" ] ; then
echo " Would flush table $table and load $count routes"
head -5 "$resolved_new" | sed 's/^/ /'
[ "$count" -gt 5 ] && echo " ... ($count total)"
rm -f "$resolved_new"
continue
fi
ip -6 route flush table "$table"
sed "s|^|route replace |; s|$| via $gw table $table|" "$resolved_new" | \
ip -6 -batch - 2>&1 | grep -v "^$" | head -5
if ! ip -6 rule show | grep -q "lookup $table.*pref $pref" ; then
ip -6 rule add iif "$IFACE6" lookup "$table" pref "$pref" 2>/dev/null
fi
echo "$current_hash" > "$STATE_DIR/$state/hash"
echo "$table" > "$STATE_DIR/$state/table"
mv "$resolved_new" "$STATE_DIR/$state/resolved"
log "[$dirname] (v6) Done"
done
}
# --- Cleanup orphaned state dirs ---
cleanup_state()
{
# IPv4
if [ -d "$STATE_DIR/$ROUTES_DIR" ] ; then
for sdir in "$STATE_DIR/$ROUTES_DIR"/*/ ; do
[ -d "$sdir" ] || continue
name=$(basename "$sdir")
if [ ! -d "$ROUTES_DIR/$name" ] ; then
table=$(ipv4_table "$name")
log "Cleaning orphaned state for $name (table $table)"
if [ -z "$SHOW" ] ; then
ip route flush table "$table" 2>/dev/null
ip rule del lookup "$table" 2>/dev/null
rm -rf "$sdir"
fi
fi
done
fi
# IPv6
if [ -d "$STATE_DIR/$ROUTES6_DIR" ] ; then
for sdir in "$STATE_DIR/$ROUTES6_DIR"/*/ ; do
[ -d "$sdir" ] || continue
name=$(basename "$sdir")
if [ ! -d "$ROUTES6_DIR/$name" ] ; then
if [ -f "$sdir/table" ] ; then
read -r table < "$sdir/table"
else
table=""
fi
log "Cleaning orphaned v6 state for $name"
if [ -z "$SHOW" ] && [ -n "$table" ] ; then
ip -6 route flush table "$table" 2>/dev/null
ip -6 rule del lookup "$table" 2>/dev/null
fi
[ -z "$SHOW" ] && rm -rf "$sdir"
fi
done
fi
}
# --- Set-rules only mode ---
if [ -n "$SET_RULES" ] ; then
for gwdir in "$ROUTES_DIR"/*/ ; do
[ -d "$gwdir" ] || continue
gw=$(basename "$gwdir")
table=$(ipv4_table "$gw")
pref=$(rule_pref "$table")
if ! ip rule show | grep -q "lookup $table.*pref $pref" ; then
ip rule add iif "$IFACE" lookup "$table" pref "$pref"
fi
done
for gwdir in "$ROUTES6_DIR"/*/ ; do
[ -d "$gwdir" ] || continue
dirname=$(basename "$gwdir")
[ -f "$gwdir/table" ] || continue
read -r table < "$gwdir/table"
pref=$(rule_pref "$table")
if ! ip -6 rule show | grep -q "lookup $table.*pref $pref" ; then
ip -6 rule add iif "$IFACE6" lookup "$table" pref "$pref"
fi
done
exit
fi
# --- Main ---
[ -n "$SHOW" ] && log "Dry-run mode (no changes will be made)"
process_v4
process_v6
cleanup_state
[ -n "$SHOW" ] || log "All done"
/root/egw-route/egw.list
\ No newline at end of file
/root/antifilter/community.lst
\ No newline at end of file
/root/antifilter/ipresolve.lst
\ No newline at end of file
/root/antifilter/subnet.lst
\ No newline at end of file
/root/egw-route/ogw.list
\ No newline at end of file
/root/egw-route/telegram.list
\ No newline at end of file
/root/egw-route/whatsapp.list
\ No newline at end of file
/root/egw-route/youtube.list
\ No newline at end of file
/root/egw-route/egw.list
\ No newline at end of file
/root/egw-route/workaround.list
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment