Commit a7a51ff4 authored by Vitaly Lipatov's avatar Vitaly Lipatov

route-update: support route-type keywords (blackhole/unreachable/...) in gateway

Allow a group's gateway file to contain a kernel route-type keyword (blackhole/unreachable/prohibit/throw) instead of a next-hop. The script then installs routes of that type (ip route replace <kw> <dst> table N), so packets are rejected/dropped by the kernel instead of forwarded. unreachable/prohibit return ICMP to the client, giving instant failure (curl drops in ~2ms) vs hanging on a dead tunnel. Used on routes6.d/fr where the France egress (ikev2.fr) is IPv4-only and no IPv6 path exists: claude.ai AAAA now fast-rejects instead of timing out. Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
parent 3b1e5c99
...@@ -16,6 +16,15 @@ ipcmd_for() { case "$1" in *routes6*) echo "ip -6" ;; *) echo "ip" ;; esac ; } ...@@ -16,6 +16,15 @@ ipcmd_for() { case "$1" in *routes6*) echo "ip -6" ;; *) echo "ip" ;; esac ; }
# Check if option is set in group's options file # Check if option is set in group's options file
has_option() { [ -f "$1/options" ] && grep -q "^$2$" "$1/options" 2>/dev/null ; } has_option() { [ -f "$1/options" ] && grep -q "^$2$" "$1/options" 2>/dev/null ; }
# Special route-type keywords for gateway: instead of 'via <gw>', emit a kernel route type.
# blackhole = silent drop; unreachable/prohibit/throw = ICMP back to client → fast failure.
is_route_type_keyword() {
case "$1" in
blackhole|unreachable|prohibit|throw) return 0 ;;
*) return 1 ;;
esac
}
# DEPRECATED: pref is now assigned by group/list order in process_routes, not by table number. # DEPRECATED: pref is now assigned by group/list order in process_routes, not by table number.
# Kept for diagnostic use (e.g. checking old pref values). # Kept for diagnostic use (e.g. checking old pref values).
rule_pref() { echo $(( $1 * 10 )) ; } rule_pref() { echo $(( $1 * 10 )) ; }
...@@ -43,6 +52,7 @@ resolve_gw() ...@@ -43,6 +52,7 @@ resolve_gw()
local ipcmd="${2:-ip}" local ipcmd="${2:-ip}"
case "$val" in case "$val" in
default) resolve_default_gw "$ipcmd" ;; default) resolve_default_gw "$ipcmd" ;;
blackhole|unreachable|prohibit|throw) return 0 ;;
*:*) echo "$val" ;; *:*) echo "$val" ;;
*[a-zA-Z]*) *[a-zA-Z]*)
if [ "$ipcmd" = "ip -6" ] ; then if [ "$ipcmd" = "ip -6" ] ; then
......
...@@ -357,21 +357,29 @@ lookup_table() ...@@ -357,21 +357,29 @@ lookup_table()
} }
# Read gateway config from a group directory # Read gateway config from a group directory
# Sets: gw, opt_set_default, has_metric # Sets: gw, opt_set_default, has_metric, gw_route_type
read_group_config() read_group_config()
{ {
local dir="$1" local dir="$1"
local ipcmd="${2:-ip}" local ipcmd="${2:-ip}"
local name=$(basename "$dir") local name=$(basename "$dir")
gw="" ; opt_set_default="" ; has_metric="" gw="" ; opt_set_default="" ; has_metric="" ; gw_route_type=""
[ -f "$dir/gateway" ] || { echo "[$name] No gateway file, skipping" >&2 ; return 1 ; } [ -f "$dir/gateway" ] || { echo "[$name] No gateway file, skipping" >&2 ; return 1 ; }
# Parse first gateway for gw variable (used for display) # Parse first gateway for gw variable (used for display)
local first_line=$(read_value "$dir/gateway") local first_line=$(read_value "$dir/gateway")
parse_gw_line "$first_line" "$ipcmd" local raw_first=$(echo "$first_line" | awk '{print $1}')
gw="$gw_ip" if is_route_type_keyword "$raw_first" ; then
[ -z "$gw" ] && { echo "[$name] Cannot resolve gateway '$first_line'" >&2 ; return 1 ; } # Route-type keyword (blackhole/unreachable/prohibit/throw): no next-hop,
# routes emit '<keyword> table N' (packet rejected/dropped by kernel).
gw_route_type="$raw_first"
gw="$raw_first"
else
parse_gw_line "$first_line" "$ipcmd"
gw="$gw_ip"
[ -z "$gw" ] && { echo "[$name] Cannot resolve gateway '$first_line'" >&2 ; return 1 ; }
fi
# Detect metric mode # Detect metric mode
grep -v '^#' "$dir/gateway" | grep -q 'metric' && has_metric=1 grep -v '^#' "$dir/gateway" | grep -q 'metric' && has_metric=1
...@@ -397,19 +405,27 @@ build_route_via() ...@@ -397,19 +405,27 @@ build_route_via()
fi fi
[ -z "$table" ] && { echo "[$name] Cannot determine table number" >&2 ; return 1 ; } [ -z "$table" ] && { echo "[$name] Cannot determine table number" >&2 ; return 1 ; }
# Build route suffix: single gateway or multipath (non-metric only) # Build route suffix: route-type keyword, single gateway, or multipath (non-metric only)
local gw_count=$(read_values "$dir/gateway" | wc -l) local raw_first=$(read_value "$dir/gateway" | awk '{print $1}')
if [ "$gw_count" -le 1 ] ; then if is_route_type_keyword "$raw_first" ; then
route_via="via $gw table $table" # Keyword must precede prefix (caller emits 'route replace <kw> <dst> table N').
route_type_kw="$raw_first"
route_via=""
else else
route_via="table $table" route_type_kw=""
while IFS= read -r gw_line ; do local gw_count=$(read_values "$dir/gateway" | wc -l)
[ -z "$gw_line" ] && continue if [ "$gw_count" -le 1 ] ; then
echo "$gw_line" | grep -q '^#' && continue route_via="via $gw table $table"
parse_gw_line "$gw_line" "$ipcmd" else
[ -z "$gw_ip" ] && continue route_via="table $table"
route_via="$route_via nexthop via $gw_ip weight 1" while IFS= read -r gw_line ; do
done < "$dir/gateway" [ -z "$gw_line" ] && continue
echo "$gw_line" | grep -q '^#' && continue
parse_gw_line "$gw_line" "$ipcmd"
[ -z "$gw_ip" ] && continue
route_via="$route_via nexthop via $gw_ip weight 1"
done < "$dir/gateway"
fi
fi fi
} }
...@@ -438,11 +454,19 @@ if [ "$ACTION" = "add" ] || [ "$ACTION" = "del" ] ; then ...@@ -438,11 +454,19 @@ if [ "$ACTION" = "add" ] || [ "$ACTION" = "del" ] ; then
if [ "$ACTION" = "add" ] ; then if [ "$ACTION" = "add" ] ; then
if is_ipv4 "$ADD_DEL_TARGET" || is_ipv6 "$ADD_DEL_TARGET" ; then if is_ipv4 "$ADD_DEL_TARGET" || is_ipv6 "$ADD_DEL_TARGET" ; then
$ipcmd route replace "$ADD_DEL_TARGET" $route_via if [ -n "$route_type_kw" ] ; then
$ipcmd route replace $route_type_kw "$ADD_DEL_TARGET" table "$table"
else
$ipcmd route replace "$ADD_DEL_TARGET" $route_via
fi
else else
get_ipv4_list "$ADD_DEL_TARGET" | sort -u | while read -r ip ; do get_ipv4_list "$ADD_DEL_TARGET" | sort -u | while read -r ip ; do
[ -n "$ip" ] || continue [ -n "$ip" ] || continue
$ipcmd route replace "$ip" $route_via if [ -n "$route_type_kw" ] ; then
$ipcmd route replace $route_type_kw "$ip" table "$table"
else
$ipcmd route replace "$ip" $route_via
fi
done done
fi fi
else else
...@@ -730,7 +754,12 @@ for line in sys.stdin: ...@@ -730,7 +754,12 @@ for line in sys.stdin:
count=$(wc -l < "$_resolved_new") count=$(wc -l < "$_resolved_new")
# Zero-downtime reload: replace all, then remove stale # Zero-downtime reload: replace all, then remove stale
if [ -n "$_has_metric" ] ; then if [ -n "$_gw_route_type" ] ; then
# Route-type keyword (blackhole/unreachable/prohibit/throw): reject/drop at kernel, no next-hop.
# Keyword must precede the prefix: 'route replace unreachable <dst> table N'.
sed "s|^|route replace $_gw_route_type |; s|$| table $_table|" "$_resolved_new" | \
$_ipcmd -batch - 2>&1 | grep -v "^$" | head -5
elif [ -n "$_has_metric" ] ; then
{ {
while IFS= read -r gw_line ; do while IFS= read -r gw_line ; do
[ -z "$gw_line" ] && continue [ -z "$gw_line" ] && continue
...@@ -855,7 +884,7 @@ process_group() ...@@ -855,7 +884,7 @@ process_group()
local name=$(basename "$_gwdir") local name=$(basename "$_gwdir")
read_group_config "$_gwdir" "$_ipcmd" || return 0 read_group_config "$_gwdir" "$_ipcmd" || return 0
_gw="$gw" ; _has_metric="$has_metric" _gw="$gw" ; _has_metric="$has_metric" ; _gw_route_type="$gw_route_type"
local group_state="$(dirname "$_gwdir")/$name" local group_state="$(dirname "$_gwdir")/$name"
# use routes_dir-relative path for state # use routes_dir-relative path for state
......
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