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 ; }
# Check if option is set in group's options file
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.
# Kept for diagnostic use (e.g. checking old pref values).
rule_pref() { echo $(( $1 * 10 )) ; }
......@@ -43,6 +52,7 @@ resolve_gw()
local ipcmd="${2:-ip}"
case "$val" in
default) resolve_default_gw "$ipcmd" ;;
blackhole|unreachable|prohibit|throw) return 0 ;;
*:*) echo "$val" ;;
*[a-zA-Z]*)
if [ "$ipcmd" = "ip -6" ] ; then
......
......@@ -357,21 +357,29 @@ lookup_table()
}
# 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()
{
local dir="$1"
local ipcmd="${2:-ip}"
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 ; }
# Parse first gateway for gw variable (used for display)
local first_line=$(read_value "$dir/gateway")
parse_gw_line "$first_line" "$ipcmd"
gw="$gw_ip"
[ -z "$gw" ] && { echo "[$name] Cannot resolve gateway '$first_line'" >&2 ; return 1 ; }
local raw_first=$(echo "$first_line" | awk '{print $1}')
if is_route_type_keyword "$raw_first" ; then
# 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
grep -v '^#' "$dir/gateway" | grep -q 'metric' && has_metric=1
......@@ -397,19 +405,27 @@ build_route_via()
fi
[ -z "$table" ] && { echo "[$name] Cannot determine table number" >&2 ; return 1 ; }
# Build route suffix: single gateway or multipath (non-metric only)
local gw_count=$(read_values "$dir/gateway" | wc -l)
if [ "$gw_count" -le 1 ] ; then
route_via="via $gw table $table"
# Build route suffix: route-type keyword, single gateway, or multipath (non-metric only)
local raw_first=$(read_value "$dir/gateway" | awk '{print $1}')
if is_route_type_keyword "$raw_first" ; then
# Keyword must precede prefix (caller emits 'route replace <kw> <dst> table N').
route_type_kw="$raw_first"
route_via=""
else
route_via="table $table"
while IFS= read -r gw_line ; do
[ -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"
route_type_kw=""
local gw_count=$(read_values "$dir/gateway" | wc -l)
if [ "$gw_count" -le 1 ] ; then
route_via="via $gw table $table"
else
route_via="table $table"
while IFS= read -r gw_line ; do
[ -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
}
......@@ -438,11 +454,19 @@ if [ "$ACTION" = "add" ] || [ "$ACTION" = "del" ] ; then
if [ "$ACTION" = "add" ] ; 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
get_ipv4_list "$ADD_DEL_TARGET" | sort -u | while read -r ip ; do
[ -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
fi
else
......@@ -730,7 +754,12 @@ for line in sys.stdin:
count=$(wc -l < "$_resolved_new")
# 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
[ -z "$gw_line" ] && continue
......@@ -855,7 +884,7 @@ process_group()
local name=$(basename "$_gwdir")
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"
# 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