Commit 5982bb88 authored by Vitaly Lipatov's avatar Vitaly Lipatov

route-update: enable IPv6 volatile domain detection + subnet aggregation

Volatile domain detection (short TTL, single record) was IPv4-only. Now works for both A and AAAA records. For IPv6 volatile domains (e.g. Akamai CDN rotation), computes covering /48+ subnets from accumulated IPs across runs, so new rotated IPs hit the bypass route instead of default gateway. Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 85e86860
......@@ -212,3 +212,50 @@ get_ipv6_list_bulk()
get_ipv6_list "$domain"
done
}
# Compute covering IPv6 subnets from a list of addresses (stdin).
# Groups by /48, computes longest common prefix within each group.
# Requires ≥2 IPs per group. Output: one subnet per line.
compute_ipv6_subnets()
{
python3 -c '
import sys, ipaddress
addrs = []
for line in sys.stdin:
line = line.strip()
if not line or line.startswith("#"):
continue
# strip /128 suffix if present
line = line.split("/")[0]
try:
addrs.append(ipaddress.IPv6Address(line))
except ValueError:
pass
# Group by /48
groups = {}
for a in addrs:
key = int(a) >> 80 # top 48 bits
groups.setdefault(key, []).append(int(a))
for key, ints in groups.items():
if len(ints) < 2:
continue
# XOR all pairs to find differing bits
xor_all = 0
for v in ints[1:]:
xor_all |= ints[0] ^ v
# Longest common prefix length
if xor_all == 0:
prefix_len = 128
else:
prefix_len = 128 - xor_all.bit_length()
# Clamp to /48 minimum (do not go wider)
if prefix_len < 48:
prefix_len = 48
# Build network from first IP with computed prefix
net = ipaddress.IPv6Network((ints[0], prefix_len), strict=False)
print(net)
'
}
......@@ -142,13 +142,13 @@ vlog() { [ -n "$VERBOSE" ] && log " $*" || true ; }
md5_lists() { cat "$@" 2>/dev/null | md5sum | awk '{print $1}' ; }
ensure_state_dir() { mkdir -p "$STATE_DIR/$1" ; }
# Detect domains with DNS balancing (single A record + short TTL)
# Detect domains with DNS balancing (single record + short TTL)
# Writes volatile domain list to STATE_DIR/$state/volatile_domains
# Args: $1=state_dir $2=name $3=label $4...=domain list files
# Args: $1=state_dir $2=name $3=label $4=rtype(A|AAAA) $5...=domain list files
detect_volatile_domains()
{
local state="$1" name="$2" label="$3"
shift 3
local state="$1" name="$2" label="$3" rtype="$4"
shift 4
local volatile_file="$STATE_DIR/$state/volatile_domains"
local ttl_threshold=120
local domains=$(mktemp)
......@@ -163,7 +163,7 @@ detect_volatile_domains()
> "$volatile_file"
while read -r domain ; do
local output=$(dig +noall +answer "$domain" A 2>/dev/null | grep "IN[[:space:]]*A[[:space:]]")
local output=$(dig +noall +answer "$domain" "$rtype" 2>/dev/null | grep "IN[[:space:]]*${rtype}[[:space:]]")
local count=$(echo "$output" | grep -c .)
local ttl=$(echo "$output" | head -1 | awk '{print $2}')
if [ "$count" -le 1 ] && [ "${ttl:-9999}" -le "$ttl_threshold" ] ; then
......@@ -172,11 +172,59 @@ detect_volatile_domains()
done < "$domains"
if [ -s "$volatile_file" ] ; then
vlog "[$name]$label volatile domains (TTL≤${ttl_threshold}s, single A): $(wc -l < "$volatile_file")"
vlog "[$name]$label volatile domains (TTL≤${ttl_threshold}s, single $rtype): $(wc -l < "$volatile_file")"
[ -n "$VERBOSE" ] && sed "s|^| [$name]$label |" "$volatile_file" >&2 || true
fi
}
# For IPv6 volatile domains: collect IPs from multiple resolvers,
# accumulate across runs, compute covering subnets, append to _resolved_new.
# Args: $1=state_dir $2=tag $3=label
# Uses: _resolved_new (appends subnets), STATE_DIR, EXTRA_DNS
expand_volatile_subnets()
{
local state="$1" tag="$2" label="$3"
local volatile_file="$STATE_DIR/$state/volatile_domains"
local vip_dir="$STATE_DIR/$state/volatile_ips"
mkdir -p "$vip_dir"
local added=0
while IFS=' ' read -r domain _info ; do
[ -z "$domain" ] && continue
local ip_file="$vip_dir/$domain"
# Collect AAAA from local + extra resolvers
local fresh=$(mktemp)
trap "rm -f $fresh" RETURN
dig +short "$domain" AAAA 2>/dev/null | grep ':' >> "$fresh"
[ -n "$EXTRA_DNS" ] && dig @$EXTRA_DNS +short "$domain" AAAA 2>/dev/null | grep ':' >> "$fresh" || true
# Merge with saved IPs (accumulate across runs)
if [ -s "$ip_file" ] ; then
cat "$ip_file" >> "$fresh"
fi
sort -u "$fresh" > "$ip_file"
rm -f "$fresh"
local ip_count=$(wc -l < "$ip_file")
if [ "$ip_count" -lt 2 ] ; then
vlog "${tag}${label} volatile $domain: only $ip_count IPs, need ≥2 for subnet"
continue
fi
# Compute covering subnets
local subnets=$(compute_ipv6_subnets < "$ip_file")
if [ -n "$subnets" ] ; then
echo "$subnets" >> "$_resolved_new"
local scount=$(echo "$subnets" | wc -l)
added=$((added + scount))
vlog "${tag}${label} volatile $domain: $ip_count IPs → $scount subnet(s): $subnets"
fi
done < "$volatile_file"
[ "$added" -gt 0 ] && log "${tag}${label} Added $added covering subnet(s) for volatile IPv6 domains"
}
# Allocate a free table number in range 200-250 (step 10)
alloc_table()
{
......@@ -504,10 +552,16 @@ resolve_list_file()
mv "${_resolved_new}.merged" "$_resolved_new"
vlog "$_tag$_label resolve history: $(ls "$history_dir" | wc -l) snapshots, $(wc -l < "$_resolved_new") unique IPs"
# Detect volatile domains
if grep -v '^#' "$_f" | grep -q '[a-zA-Z]' && [ "$_ipcmd" = "ip" ] ; then
# Detect volatile domains (IPv4 and IPv6)
if grep -v '^#' "$_f" | grep -q '[a-zA-Z]' ; then
local _vname=$(echo "$_tag" | tr -d '[]')
detect_volatile_domains "$_state" "$_vname" "$_label" "$_f"
local _rtype="A"
[ "$_ipcmd" = "ip -6" ] && _rtype="AAAA"
detect_volatile_domains "$_state" "$_vname" "$_label" "$_rtype" "$_f"
# For IPv6 volatile domains: add covering subnets
if [ "$_ipcmd" = "ip -6" ] && [ -s "$STATE_DIR/$_state/volatile_domains" ] ; then
expand_volatile_subnets "$_state" "$_tag" "$_label"
fi
fi
}
......
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