1010# Presence check MUST use /usr/sbin/ip, not the enter-hook ip() wrapper.
1111# That wrapper sends any `ip … route` that is not `del` through vtysh as an
1212# add, so `ip -4 route show` both misses the kernel table and writes a
13- # staticd duplicate. delroute() already reads via /usr/sbin/ip; match that,
14- # and append ${VRF_OPTION} so VRF leases see the same table the add uses.
13+ # staticd duplicate. Use `ip -j` so a multipath default (two DHCP WANs at
14+ # the same distance) is visible — `ip route show default via GW dev IF`
15+ # returns nothing for a nexthop group. Append ${VRF_OPTION} so VRF leases
16+ # see the same table the add uses.
1517#
1618# If option 3 changes across a same-IP RENEW, drop defaults via gateways
1719# that disappeared from $new_routers (including option 3 going away entirely).
1820# Otherwise FRR keeps the old nexthop and the kernel shows ECMP over old+new.
1921# Compare $old_routers vs $new_routers (same pattern as
20- # 98-vyos-static-routes-dhclient-hook). The no-default-route enter hook blanks
21- # new_routers; removing a leftover option-3 default is the right outcome there
22- # too, so cleanup runs before the empty-new_routers return.
22+ # 98-vyos-static-routes-dhclient-hook). Delete with the same per-router
23+ # metric 01-vyos-cleanup uses (${IF_METRIC}, then +1 per old_routers
24+ # entry): a wrapper delete without metric always issues distance 210, which
25+ # misses the 211+ FRR leftovers and can leave a ghost as the kernel default
26+ # after option 3 is cleared.
27+ #
28+ # 06-vyos-nodefaultroute blanks new_routers and sets VYOS_NO_DEFAULT_ROUTE.
29+ # Skip the stale-delete (and its vtysh session) when that flag is set and
30+ # the kernel has no default via that gateway — otherwise a server that
31+ # force-sends option 3 logs "removing stale default" on every renew.
32+ # If a leftover kernel default exists (no-default-route just set), still
33+ # delete it.
2334#
2435# Adds still use the enter-hook ip() wrapper so FRR gets tag/distance
2536# IF_METRIC. Skip when classless static routes are present (stock ignores
@@ -42,16 +53,26 @@ if [ -n "$new_rfc3442_classless_static_routes" ]; then
4253 return 0 2>/dev/null || exit 0
4354fi
4455
45- # Kernel/FRR-installed default present? Real ip(8) only — never the wrapper.
56+ # Kernel default via $1 on $interface? Real ip(8) JSON — never the wrapper.
57+ # Matches both a single-nexthop default and a multipath nexthop group.
4658_default_via_present() {
47- /usr/sbin/ip -4 route show default via "$1" dev "${interface}" ${VRF_OPTION} 2>/dev/null | grep -q .
59+ local gw="$1"
60+ local json
61+ json=$(/usr/sbin/ip -j -4 route show default ${VRF_OPTION} 2>/dev/null) || return 1
62+ [ -n "$json" ] || return 1
63+ echo "$json" | jq -e --arg gw "$gw" --arg dev "$interface" \
64+ 'any(.[];
65+ (.gateway == $gw and .dev == $dev)
66+ or any(.nexthops[]?; .gateway == $gw and .dev == $dev)
67+ )' >/dev/null 2>&1
4868}
4969
50- if_metric="${IF_METRIC:-210}"
51-
5270# Drop defaults via gateways the server no longer announces, including when
53- # option 3 disappeared (empty new_routers) or no-default-route blanked it.
71+ # option 3 disappeared (empty new_routers). Metric numbering matches the
72+ # original install / 01-vyos-cleanup: IF_METRIC, then +1 per old_routers
73+ # entry (keepers included, or the 211+ leftover is missed).
5474if [ -n "$old_routers" ] && [ "$old_routers" != "$new_routers" ]; then
75+ if_metric="${IF_METRIC:-210}"
5576 for router in $old_routers; do
5677 skip=
5778 for keep in $new_routers; do
@@ -61,16 +82,25 @@ if [ -n "$old_routers" ] && [ "$old_routers" != "$new_routers" ]; then
6182 fi
6283 done
6384 if [ -n "$skip" ]; then
85+ if_metric=$((if_metric + 1))
86+ continue
87+ fi
88+ # no-default-route blanks new_routers every renew; only act if a
89+ # leftover default is actually present.
90+ if [ -n "$VYOS_NO_DEFAULT_ROUTE" ] && ! _default_via_present "${router}"; then
91+ if_metric=$((if_metric + 1))
6492 continue
6593 fi
6694 logmsg info \
67- "RENEW/REBIND: removing stale default via ${router} dev ${interface}"
68- # Wrapper: FRR-aware del (staticd + kernel).
69- ip -4 route del default via "${router}" dev "${interface}" >/dev/null 2>&1
95+ "RENEW/REBIND: removing stale default via ${router} dev ${interface} metric ${if_metric}"
96+ # Wrapper: FRR-aware del (staticd + kernel) at the install distance.
97+ ip -4 route del default via "${router}" dev "${interface}" \
98+ metric "${if_metric}" >/dev/null 2>&1
7099 if _default_via_present "${router}"; then
71100 logmsg warn \
72101 "RENEW/REBIND: stale default still present via ${router} dev ${interface}"
73102 fi
103+ if_metric=$((if_metric + 1))
74104 done
75105fi
76106
@@ -79,6 +109,7 @@ if [ -z "$new_routers" ]; then
79109 return 0 2>/dev/null || exit 0
80110fi
81111
112+ if_metric="${IF_METRIC:-210}"
82113for router in $new_routers; do
83114 if _default_via_present "${router}"; then
84115 if_metric=$((if_metric + 1))
0 commit comments