Skip to content

ip,ip6,frr: flush routes like zebra on address and iface teardown - #710

Open
maxime-leroy wants to merge 5 commits into
DPDK:mainfrom
maxime-leroy:flush_addr_like_zebra_on_addr_iface_teardown
Open

ip,ip6,frr: flush routes like zebra on address and iface teardown#710
maxime-leroy wants to merge 5 commits into
DPDK:mainfrom
maxime-leroy:flush_addr_like_zebra_on_addr_iface_teardown

Conversation

@maxime-leroy

Copy link
Copy Markdown
Collaborator

zebra drops routes from its RIB on its own, without sending anything to the
dataplane, in two situations:

It assumes the dataplane already dropped them, as Linux does
(fib_disable_ip, fib_del_ifaddr), and calls rib_delete() with
fromkernel=true, so nothing reaches the provider. grout keeps them, so the
two tables disagree for good: grout still forwards on routes zebra no
longer knows, and zebra never re-learns them until the plugin resyncs.

Measured with grout + zebra:

grcli address del <last IPv4 addr>   10.5: kept in both   10.7: grout only
grcli interface set port p0 down     10.5 and 10.7: grout only, v4 and v6

grout now flushes those routes too and notifies, so both tables converge on
every FRR version. The address case is unconditional: without a source
address the remaining nexthops can never be resolved, so keeping them was a
defect of its own. The interface case is a compatibility concession, a down
interface being a reversible state, so it is optional
(flush-on-iface-down, off by default) and the plugin enables it. Carrier
loss is not a trigger, the interface stays configured up. Routes installed
by a routing daemon are never touched, their owner reevaluates them.

The notifications for the routes deleted with an interface are dropped as
well (skip-events-on-iface-down, off by default, enabled by the plugin):
zebra has already purged them, and each one costs a full rib_delete() plus
a failed reinstall attempt for the routes zebra owns.

Two new smoke tests. Both assert that grout and zebra hold the same view
rather than a fixed outcome, so they are version agnostic, and both are red
without the series:

  frr-10.5.4   addr_del_kernel_route / iface_down_kernel_route   OK
  frr-10.7.0   addr_del_kernel_route / iface_down_kernel_route   OK

Without any IPv4 address left on an interface, ARP requests can no longer
be sent from it: the nexthops reachable through that interface can never
be resolved again, and the routes using them are dead weight. Linux
flushes them (fib_del_ifaddr) and control planes rely on it. Since FRR
10.7, zebra reacts to the deletion of the last IPv4 address of an
interface by dropping such routes from its RIB on its own, without
sending anything to the dataplane, which left grout and zebra with two
different views for good.

Flush them in grout instead, and notify as usual so that zebra follows on
every FRR version. Routes installed by a routing daemon are left alone,
their owner reevaluates them when the nexthop stops resolving.

This also means a lost DHCP lease now removes the IPv4 routes of the
interface, as it does on Linux.

IPv6 is left untouched: on-link information does not come from the
configured addresses there, and the link-local address remains available
to source neighbor solicitations.

Signed-off-by: Maxime Leroy <maxime@leroys.fr>
zebra removes the kernel routes of an interface from its RIB as soon as
the interface goes administratively down, and it does not tell the
dataplane about it: rib_update_handle_kernel_route_down_possibility()
considers a nexthop dead when its interface is not up, for both address
families. Linux behaves the same way (fib_disable_ip flushes, and IPv6
loses its addresses with the link), so the routes never come back. grout
keeps them and leaves their nexthops unresolved, which is arguably the
better model for a reversible state, but it means the two tables disagree
for good as soon as an interface is set down.

Add flush-on-iface-down to make grout follow the control plane on that
point. It is off by default: grout alone keeps its current behaviour, and
the routes of a down interface come back when it goes up again. Carrier
loss is deliberately not a trigger, the interface is still configured up
and neither Linux nor zebra flush anything in that case.

Routes installed by a routing daemon are left alone, their owner
reevaluates them on the interface event.

Signed-off-by: Maxime Leroy <maxime@leroys.fr>
zebra drops the kernel routes of an interface from its RIB as soon as the
interface goes administratively down, for both address families, and
sends nothing to the dataplane: it assumes the routes are already gone,
as they would be on Linux. grout keeps them, so the two tables diverged
for good on every supported FRR version.

Enable flush-on-iface-down when connecting to grout, and do it on every
connection since a restarted grout comes back with its defaults.

Signed-off-by: Maxime Leroy <maxime@leroys.fr>
The routes deleted along with an interface, whether it goes down or is
removed, are of no use to a control plane which evicts them on the
interface event: zebra has already dropped them from its RIB by the time
grout reports them, and each notification costs a full rib_delete() for
nothing, plus a failed reinstall attempt for the routes zebra owns. Linux
is silent in that case too, and its consumers are expected to react to
the interface event.

Add skip-events-on-iface-down, off by default, and let the deletion paths
say whether they want the events instead of guessing from ambient state.
The route deletion which follows the removal of the last IPv4 address of
an interface keeps notifying: nothing tells the control plane about it
otherwise.

The configuration request now carries a set_attrs mask, so that a client
setting one attribute does not silently reset the ones it does not know
about. The plugin enables both attributes at once.

Signed-off-by: Maxime Leroy <maxime@leroys.fr>
The route config set command dispatches to every registered address
family with the same parse node, so a family which was not given an
argument gets zero. Both handlers rejected it with EINVAL, which made
"route config set default rib4-routes N" report a failure although the
value had been applied, and contradicted the request documentation which
says that zero means unchanged.

Treat zero as nothing to change, like the neighbouring FIB fields already
do. A set_attrs mask would only be needed if zero were a valid FIB size.

Fixes: e1b0bde ("ip,ip6: allow configuring default FIB size")
Signed-off-by: Maxime Leroy <maxime@leroys.fr>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider

Great, no issues found!

Qodo reviewed your code and found no material issues that require review

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@maxime-leroy

Copy link
Copy Markdown
Collaborator Author

The API check failure is not a real incompatibility. Both
gr_iface_config and gr_iface_config_set_req are added by this series:
the second commit creates them, the fourth extends them, and nothing
outside the series ever saw the intermediate layout. Against main the
whole series is additive, only new requests and new types, so any client
built before it stays compatible.

The check trips because its baseline is the parent commit:

prev_revision=${PREV_REVISION:-HEAD^}

which means every breaking commit needs its own bump. GR_API_VERSION is
a handshake token though, and what it has to describe accurately is the
compatibility between two released binaries, not between two commits of
a development cycle. Comparing against the last release tag instead would
express that, and would leave the commit layout of a series free.

So I would rather not bump here: it would put every client and daemon in
lockstep for a struct layout that never shipped. If you prefer the check
green on every commit as it stands, I can declare the final layout in the
commit which introduces the requests, or squash the two commits together.

@rjarry

rjarry commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

See my comment on #711.

I understand that in this specific case: adding a new message and modifying it in the same patch series should not cause check-api.sh to fail. However, there is no way to differentiate between a new message being modified mid pull request from other real ABI breaking changes.

I really want to keep all API breaking changes self contained (i.e. with their own GR_API_VERSION bump attached).

If you can find a way to determine the "correct" reference commit mid pull request, we can drop the hard-coded HEAD^. But I don't think it will be trivial, and most likely impossible.

@rjarry

rjarry commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Commenting on the code here, why do we need a knob to configure these behaviors? We could just mimic what Linux does (even if it is silly). The point here is to be transparent.

If we really want to have this configurable at runtime, I think it would make more sense to use environment variables like for other opt-in things.

grout/main/grout.default

Lines 17 to 26 in 819435f

# Override the default route in the main routing table. Required when running
# in a private network namespace (PrivateNetwork=true) or in a container. Must
# be set to false if removing PrivateNetwork=true from the systemd service and
# running grout in the main network namespace.
GROUT_OVERRIDE_DEFAULT_ROUTE=true
# Set loose reverse path filtering on control plane TAP interfaces.
# Required when net.ipv4.conf.all.rp_filter is set to 1 (strict) in the network
# namespace where grout is running.
GROUT_OVERRIDE_RP_FILTER=true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants