Files
netbird/client/internal/routemanager
Riccardo Manfrin 85db35194e [client] Prolong HA routes on the current routing peer across relay and direct transitions (#8192)
* [client] Do not move an HA route on latency before both peers have a sample

A direct connection reports no latency until the first sample of its new ICE
agent, and a drop to relay clears the previous one. Route selection ranked
such a peer at unknownLatency, so a routing peer whose direct connection came
back within the relayed switch delay lost the route to an equivalent peer on
a "lower latency" switch right after recovering, resetting every masqueraded
flow the delay was meant to keep alive. The sample usually lands before the
evaluation, but nothing orders the two, and a pair with no round trip time
yet postpones it to the next sampling tick.

Latency-only switches now require a sample on both peers. Categorical
criteria (connected, metric, direct versus relayed) are unaffected.

* [client] Hold a never-direct HA routing peer before a direct peer takes over

The peers of an HA group come up over relay first and turn direct one after
the other, as when lazy connections wake the group or the client reconnects.
A routing peer that was relayed when it carried the route gave way at once
to the first equivalent peer that turned direct, so in a group of N peers
the route moved right after the session started in roughly (N-1)/N of the
cases, resetting every masqueraded flow, typically an SSH session opened on
the wake-up traffic.

Such a peer is now kept for the relayedSwitch delay, counted from the first
evaluation that finds a direct contender. If it turns direct within the
delay the route stays; otherwise it moves when the delay runs out, so the
route does not stay on a relay for longer than the delay. A peer that
dropped from direct keeps counting from the drop.

* [client] Keep allowed IPs when only the chosen routing peer's status changes

recalculateRoutes did not tell a status change of the chosen routing peer
(idle to connected when traffic wakes it, connected to relayed and back) from
a switch to another peer: it removed the allowed IPs and added them back on
the same peer. In between the prefix had no owner and packets for it were
dropped, and default routes published a spurious "disconnected due to high
availability change" event followed by two "connected" ones.

When the same route stays on the same peer with the same definition, only
the recorded status is updated now.

* [client] Limit the "never" relayed switch policy to peers that dropped from direct

NB_HA_RELAYED_SWITCH_DELAY=never keeps a routing peer whose direct
connection dropped to relay for as long as it stays connected. Once the
relayed delay also covered peers that were never direct, "never" pinned
those to the relay for good, although a direct equivalent was available
and the peer showed no sign of turning direct.

A peer that was never direct now uses the default delay under "never",
which keeps the policy's original scope.

* [client] Merge the unchanged-route checks of the HA route watcher

shouldSkipRecalculation and the status-only fast path in recalculateRoutes
both handled the same route staying on the same routing peer, one for an
unchanged status and one for a changed one. They are now a single
keepChosenRoute check.

Pure refactor, no behavior change.

* [client] Re-apply routed allowed IPs when a kept routing peer connects

A peer update can recreate a routing peer: modifyPeers removes it from the
interface, which drops its routed prefixes, and adds it back idle with its
overlay addresses only. The remove and re-add that recalculateRoutes used
to run on every status change of the chosen peer put the prefixes back as
a side effect once the peer connected. Without it, a watcher that sees the
same peer go idle and then connected leaves the routed subnet without an
owner until a route update or an HA switch.

When the chosen peer connects while keeping the route, the watcher now
re-applies its routed allowed IPs through the route manager's reconcile,
which is add-only and idempotent. Lazy connections already do the same when
they arm an idle peer.

* [client] Bound the HA route hold on a routing peer without a latency sample

A latency-only switch waited for a sample on both peers. If the current
peer never got one, because its candidate pair stats kept failing or
reporting no round trip time, it kept the route indefinitely against a
measured peer, however much faster that one was.

The hold on an unsampled current peer now lasts unmeasuredLatencyHold
(30s), long enough for a recovered direct connection to report its first
sample. After that the peer is ranked at unknownLatency again and the usual
switch margins apply. relayedHold is armed so the switch is reconsidered
when the hold runs out.

* [client] Restart the relayed delay of a never-direct peer when its contender goes

For a routing peer that was never direct, the relayed delay runs from the
first evaluation that finds a direct contender. It was only reset when the
current peer changed or turned direct, so if that contender went away and
another one showed up later, the delay counted from the first one and the
new contender took over at once.

The delay of such a peer now restarts once no direct contender is left.
The delay of a peer that dropped from direct keeps running from the drop.

* [client] Retrigger pull request synchronization

* [client] Retry a failed routed allowed IPs reconcile on the next evaluation

keepChosenRoute recorded the connected status before re-applying the routed
allowed IPs and only logged a failed reconcile. Later evaluations then saw
an unchanged status and never retried, so a transient AddAllowedIP failure
left the recreated routing peer without its routed prefixes until another
status transition or a route switch.

The status is now recorded only once the reconcile succeeds. A failure
leaves the previous status in place, so the next evaluation, at the latest
the periodic re-evaluation, sees the change again and retries.
2026-10-15 10:53:55 +02:00
..