Commit eb4bf0ec authored by System Administrator's avatar System Administrator

router: add IPv4 volatile subnet expansion for CDN edge IPs

- Add compute_ipv4_subnets() and expand_volatile_subnets_v4() to functions - Call expand_volatile_subnets_v4 for IPv4 volatile domains in route-update.sh - Fix shell condition precedence (||/&&) for both IPv4 and IPv6 volatile detection - Support multiple EXTRA_DNS servers (8.8.8.8, 1.1.1.1, 9.9.9.9) - Add is_pingonly_gw() for ikev2.fr/ikev2.gr in route-health.sh Fixes Codex 403: CDN edge IPs (8.47.69.x, 8.6.112.x) were missing from ai table because DNS returns different IPs than what route-update resolves via dig. expand_volatile_subnets_v4 accumulates IPs from multiple resolvers and computes covering /24 subnets automatically.
parent 08365b47
...@@ -71,29 +71,31 @@ resolve_gw() ...@@ -71,29 +71,31 @@ resolve_gw()
# Check if external DNS is reachable. Sets EXTRA_DNS=IP or EXTRA_DNS= # Check if external DNS is reachable. Sets EXTRA_DNS=IP or EXTRA_DNS=
# Include $EXTRA_DNS in hash so re-resolve triggers when availability changes # Include $EXTRA_DNS in hash so re-resolve triggers when availability changes
EXTRA_DNS_SERVER=8.8.8.8 EXTRA_DNS_SERVERS="8.8.8.8 1.1.1.1 9.9.9.9"
check_extra_dns() check_extra_dns()
{ {
if dig @$EXTRA_DNS_SERVER +short +time=1 +tries=1 google.com A >/dev/null 2>&1 ; then EXTRA_DNS=""
EXTRA_DNS=$EXTRA_DNS_SERVER for _srv in $EXTRA_DNS_SERVERS; do
else if dig @$_srv +short +time=1 +tries=1 google.com A >/dev/null 2>&1; then
EXTRA_DNS= EXTRA_DNS="$EXTRA_DNS $_srv"
fi fi
done
EXTRA_DNS="${EXTRA_DNS# }"
} }
get_ipv4_list() get_ipv4_list()
{ {
echo "$1" | grep -q "[a-z]" || return 0 echo "$1" | grep -q "[a-z]" || return 0
dig "$1" A | grep -v "^;" | grep "IN[[:space:]]*A[[:space:]]" | sed -e "s|.*[[:space:]]||" | sort dig "$1" A | grep -v "^;" | grep "IN[[:space:]]*A[[:space:]]" | sed -e "s|.*[[:space:]]||" | sort
[ -n "$EXTRA_DNS" ] && dig @$EXTRA_DNS "$1" A | grep -v "^;" | grep "IN[[:space:]]*A[[:space:]]" | sed -e "s|.*[[:space:]]||" | sort || true for _dns in $EXTRA_DNS; do dig @$_dns "$1" A | grep -v "^;" | grep "IN[[:space:]]*A[[:space:]]" | sed -e "s|.*[[:space:]]||" | sort; done || true
} }
get_ipv6_list() get_ipv6_list()
{ {
echo "$1" | grep -q "[a-z]" || return 0 echo "$1" | grep -q "[a-z]" || return 0
dig "$1" AAAA | grep -v "^;" | grep "IN[[:space:]]*AAAA[[:space:]]" | sed -e "s|.*[[:space:]]||" | sort dig "$1" AAAA | grep -v "^;" | grep "IN[[:space:]]*AAAA[[:space:]]" | sed -e "s|.*[[:space:]]||" | sort
[ -n "$EXTRA_DNS" ] && dig @$EXTRA_DNS "$1" AAAA | grep -v "^;" | grep "IN[[:space:]]*AAAA[[:space:]]" | sed -e "s|.*[[:space:]]||" | sort || true for _dns in $EXTRA_DNS; do dig @$_dns "$1" AAAA | grep -v "^;" | grep "IN[[:space:]]*AAAA[[:space:]]" | sed -e "s|.*[[:space:]]||" | sort; done || true
} }
is_ipv4() is_ipv4()
...@@ -165,10 +167,10 @@ get_ipv4_list_bulk() ...@@ -165,10 +167,10 @@ get_ipv4_list_bulk()
[ -s "$domains" ] || return 0 [ -s "$domains" ] || return 0
# Bulk async resolve: local resolver + Google DNS (if reachable) # Bulk async resolve: local resolver + extra DNS servers (if reachable)
{ {
adnshost -a -t a -Fi -f < "$domains" adnshost -a -t a -Fi -f < "$domains"
[ -n "$EXTRA_DNS" ] && adnshost -a -t a -Fi -f --config "nameserver $EXTRA_DNS" < "$domains" || true for _dns in $EXTRA_DNS; do adnshost -a -t a -Fi -f --config "nameserver $_dns" < "$domains" || true; done
} > "$adns_out" 2>/dev/null } > "$adns_out" 2>/dev/null
# Output resolved IPs grouped by domain # Output resolved IPs grouped by domain
...@@ -184,7 +186,8 @@ get_ipv4_list_bulk() ...@@ -184,7 +186,8 @@ get_ipv4_list_bulk()
sort -u > "$cname_domains" sort -u > "$cname_domains"
if [ -s "$cname_domains" ] ; then if [ -s "$cname_domains" ] ; then
local resolvers="$(awk '/^nameserver/ {print $2}' /etc/resolv.conf | paste -sd,)" local resolvers="$(awk '/^nameserver/ {print $2}' /etc/resolv.conf | paste -sd,)"
[ -n "$EXTRA_DNS" ] && resolvers="$resolvers,$EXTRA_DNS" local _dns_comma=$(echo "$EXTRA_DNS" | tr ' ' ',')
[ -n "$_dns_comma" ] && resolvers="$resolvers,$_dns_comma"
dnsx -a -resp-only -silent -r "$resolvers" -t 50 < "$cname_domains" 2>/dev/null dnsx -a -resp-only -silent -r "$resolvers" -t 50 < "$cname_domains" 2>/dev/null
fi fi
rm -f "$cname_domains" rm -f "$cname_domains"
...@@ -211,10 +214,10 @@ get_ipv6_list_bulk() ...@@ -211,10 +214,10 @@ get_ipv6_list_bulk()
[ -s "$domains" ] || return 0 [ -s "$domains" ] || return 0
# Bulk async resolve: local resolver + Google DNS (if reachable) # Bulk async resolve: local resolver + extra DNS servers (if reachable)
{ {
adnshost -a -t aaaa -Fi -f < "$domains" adnshost -a -t aaaa -Fi -f < "$domains"
[ -n "$EXTRA_DNS" ] && adnshost -a -t aaaa -Fi -f --config "nameserver $EXTRA_DNS" < "$domains" || true for _dns in $EXTRA_DNS; do adnshost -a -t aaaa -Fi -f --config "nameserver $_dns" < "$domains" || true; done
} > "$adns_out" 2>/dev/null } > "$adns_out" 2>/dev/null
# Output resolved IPs grouped by domain # Output resolved IPs grouped by domain
...@@ -230,7 +233,8 @@ get_ipv6_list_bulk() ...@@ -230,7 +233,8 @@ get_ipv6_list_bulk()
sort -u > "$cname_domains" sort -u > "$cname_domains"
if [ -s "$cname_domains" ] ; then if [ -s "$cname_domains" ] ; then
local resolvers="$(awk '/^nameserver/ {print $2}' /etc/resolv.conf | paste -sd,)" local resolvers="$(awk '/^nameserver/ {print $2}' /etc/resolv.conf | paste -sd,)"
[ -n "$EXTRA_DNS" ] && resolvers="$resolvers,$EXTRA_DNS" local _dns_comma=$(echo "$EXTRA_DNS" | tr ' ' ',')
[ -n "$_dns_comma" ] && resolvers="$resolvers,$_dns_comma"
dnsx -aaaa -resp-only -silent -r "$resolvers" -t 50 < "$cname_domains" 2>/dev/null dnsx -aaaa -resp-only -silent -r "$resolvers" -t 50 < "$cname_domains" 2>/dev/null
fi fi
rm -f "$cname_domains" rm -f "$cname_domains"
...@@ -282,3 +286,127 @@ for key, ints in groups.items(): ...@@ -282,3 +286,127 @@ for key, ints in groups.items():
print(net) print(net)
' '
} }
# Compute covering IPv4 subnets from a list of addresses (stdin).
# Groups by /16, computes longest common prefix within each group.
# Requires ≥2 IPs per group. Output: one subnet per line.
compute_ipv4_subnets()
{
python3 -c '
import sys, ipaddress
addrs = []
for line in sys.stdin:
line = line.strip()
if not line or line.startswith("#"):
continue
line = line.split("/")[0]
try:
addrs.append(ipaddress.IPv4Address(line))
except ValueError:
pass
groups = {}
for a in addrs:
key = int(a) >> 16
groups.setdefault(key, []).append(int(a))
for key, ints in groups.items():
if len(ints) < 2:
continue
xor_all = 0
for v in ints[1:]:
xor_all |= ints[0] ^ v
if xor_all == 0:
prefix_len = 32
else:
prefix_len = 32 - xor_all.bit_length()
if prefix_len < 16:
prefix_len = 16
net = ipaddress.IPv4Network((ints[0], prefix_len), strict=False)
print(net)
'
}
# For IPv4 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 $4=list_file
# Uses: _resolved_new (appends subnets), STATE_DIR, EXTRA_DNS
expand_volatile_subnets_v4()
{
vlog "DEBUG: expand_volatile_subnets_v4 called with $*"
local state="$1" tag="$2" label="$3" list_file="$4"
local volatile_file="$STATE_DIR/$state/volatile_domains"
local vip_dir="$STATE_DIR/$state/volatile_ips"
mkdir -p "$vip_dir"
# Prune volatile_ips for domains no longer present in the list.
if [ -n "$list_file" ] && ls "$vip_dir"/* >/dev/null 2>&1 ; then
local current_domains=$(mktemp)
cat_expanded "$list_file" 2>/dev/null | grep -v '^#' | grep -v '^$' \
| grep '[a-zA-Z]' | sort -u > "$current_domains"
for vf in "$vip_dir"/* ; do
[ -f "$vf" ] || continue
if ! grep -qxF "$(basename "$vf")" "$current_domains" ; then
rm -f "$vf"
vlog "${tag}${label} pruned stale volatile_ips: $(basename "$vf")"
fi
done
rm -f "$current_domains"
fi
# Build combined domain list: volatile_domains + domains with saved volatile_ips
local all_domains=$(mktemp)
[ -s "$volatile_file" ] && awk -F' ' '{print $1}' "$volatile_file" > "$all_domains"
# Also include domains with accumulated IPs from prior runs
for f in "$vip_dir"/* ; do
[ -s "$f" ] && basename "$f" >> "$all_domains"
done 2>/dev/null
sort -u "$all_domains" -o "$all_domains"
[ -s "$all_domains" ] || { rm -f "$all_domains" ; return 0 ; }
# Phase 1: parallel dig for all volatile domains
local fresh_dir=$(mktemp -d)
local digger=$(mktemp)
trap "rm -rf $fresh_dir $digger $all_domains" RETURN
cat > "$digger" << 'DIGGER'
#!/bin/sh
domain="$1"; outdir="$2"; extra_dns="$3"
out="$outdir/$domain"
dig +short "$domain" A 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' > "$out"
for _dns in $extra_dns; do dig @$_dns +short "$domain" A 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' >> "$out" || true; done
DIGGER
chmod +x "$digger"
xargs -P 10 -I{} "$digger" {} "$fresh_dir" "$EXTRA_DNS" < "$all_domains"
# Phase 2: sequential merge + subnet computation
local added=0
while read -r domain ; do
[ -z "$domain" ] && continue
local ip_file="$vip_dir/$domain"
local fresh="$fresh_dir/$domain"
# Merge fresh + saved IPs
{ [ -s "$fresh" ] && cat "$fresh" ; [ -s "$ip_file" ] && cat "$ip_file" ; } | sort -u > "$ip_file.tmp"
mv "$ip_file.tmp" "$ip_file"
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_ipv4_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 < "$all_domains"
rm -rf "$fresh_dir" "$digger"
[ "$added" -gt 0 ] && log "${tag}${label} Added $added covering subnet(s) for volatile IPv4 domains" || true
}
...@@ -201,7 +201,8 @@ if [ "${ttl:-9999}" -le "$threshold" ] ; then ...@@ -201,7 +201,8 @@ if [ "${ttl:-9999}" -le "$threshold" ] ; then
reason="ttl=${ttl}" reason="ttl=${ttl}"
elif [ -n "$extra_dns" ] ; then elif [ -n "$extra_dns" ] ; then
local_ips=$(echo "$output" | awk '{print $NF}' | sort) local_ips=$(echo "$output" | awk '{print $NF}' | sort)
extra_ips=$(dig @$extra_dns +noall +answer "$domain" "$rtype" 2>/dev/null | grep "IN[[:space:]]*${rtype}[[:space:]]" | awk '{print $NF}' | sort) extra_dns_first=$(echo "$extra_dns" | awk '{print $1}')
extra_ips=$(dig @$extra_dns_first +noall +answer "$domain" "$rtype" 2>/dev/null | grep "IN[[:space:]]*${rtype}[[:space:]]" | awk '{print $NF}' | sort)
if [ -n "$extra_ips" ] && [ "$local_ips" != "$extra_ips" ] ; then if [ -n "$extra_ips" ] && [ "$local_ips" != "$extra_ips" ] ; then
reason="diff-resolvers" reason="diff-resolvers"
fi fi
...@@ -287,7 +288,7 @@ expand_volatile_subnets() ...@@ -287,7 +288,7 @@ expand_volatile_subnets()
domain="$1"; outdir="$2"; extra_dns="$3" domain="$1"; outdir="$2"; extra_dns="$3"
out="$outdir/$domain" out="$outdir/$domain"
dig +short "$domain" AAAA 2>/dev/null | grep ':' > "$out" dig +short "$domain" AAAA 2>/dev/null | grep ':' > "$out"
[ -n "$extra_dns" ] && dig @$extra_dns +short "$domain" AAAA 2>/dev/null | grep ':' >> "$out" || true for _dns in $extra_dns; do dig @$_dns +short "$domain" AAAA 2>/dev/null | grep ':' >> "$out" || true; done
DIGGER DIGGER
chmod +x "$digger" chmod +x "$digger"
xargs -P 10 -I{} "$digger" {} "$fresh_dir" "$EXTRA_DNS" < "$all_domains" xargs -P 10 -I{} "$digger" {} "$fresh_dir" "$EXTRA_DNS" < "$all_domains"
...@@ -693,9 +694,13 @@ resolve_list_file() ...@@ -693,9 +694,13 @@ resolve_list_file()
# For IPv6 volatile domains: add covering subnets # For IPv6 volatile domains: add covering subnets
# Also run if volatile_ips accumulated from prior runs (even if current detect found nothing) # Also run if volatile_ips accumulated from prior runs (even if current detect found nothing)
local _vip_dir="$STATE_DIR/$_state/volatile_ips" local _vip_dir="$STATE_DIR/$_state/volatile_ips"
if [ "$_ipcmd" = "ip -6" ] && { [ -s "$STATE_DIR/$_state/volatile_domains" ] || [ -d "$_vip_dir" ] && ls "$_vip_dir"/* >/dev/null 2>&1 ; } ; then if [ "$_ipcmd" = "ip -6" ] && { [ -s "$STATE_DIR/$_state/volatile_domains" ] || { [ -d "$_vip_dir" ] && ls "$_vip_dir"/* >/dev/null 2>&1 ; } ; } ; then
expand_volatile_subnets "$_state" "$_tag" "$_label" "$_f" expand_volatile_subnets "$_state" "$_tag" "$_label" "$_f"
fi fi
# For IPv4 volatile domains: add covering subnets
if [ "$_ipcmd" = "ip" ] && { [ -s "$STATE_DIR/$_state/volatile_domains" ] || { [ -d "$_vip_dir" ] && ls "$_vip_dir"/* >/dev/null 2>&1 ; } ; } ; then
expand_volatile_subnets_v4 "$_state" "$_tag" "$_label" "$_f"
fi
fi fi
} }
...@@ -1230,7 +1235,7 @@ check_list_duplicates "$ROUTES6_DIR" " (v6)" || dup_errors=$((dup_errors + $?)) ...@@ -1230,7 +1235,7 @@ check_list_duplicates "$ROUTES6_DIR" " (v6)" || dup_errors=$((dup_errors + $?))
[ "$dup_errors" -gt 0 ] && fatal "Duplicate .list files detected — fix config before proceeding" [ "$dup_errors" -gt 0 ] && fatal "Duplicate .list files detected — fix config before proceeding"
check_extra_dns check_extra_dns
[ -n "$EXTRA_DNS" ] && vlog "Extra DNS: $EXTRA_DNS" || log "Extra DNS ($EXTRA_DNS_SERVER) unreachable, local resolver only" [ -n "$EXTRA_DNS" ] && vlog "Extra DNS: $EXTRA_DNS" || log "Extra DNS ($EXTRA_DNS_SERVERS) unreachable, local resolver only"
# Cleanup runs FIRST so that flushes for moved/orphaned lists happen # Cleanup runs FIRST so that flushes for moved/orphaned lists happen
# before the new group reloads its routes (avoids stale metric'd routes # before the new group reloads its routes (avoids stale metric'd routes
......
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