bgpd: Fix GR helper retaining stale routes after Hard Reset - #21823
Conversation
Greptile SummaryThis PR fixes four related gaps that allowed
Confidence Score: 5/5Safe to merge β all four code paths are addressed with minimal, targeted one-liner guards; no existing GR behaviour is changed for normal (non-Hard-Reset) sessions. Each of the four gaps described in the PR is directly closed by the corresponding change. Memory management in bgp_packet.c is unaffected (outer.raw_data is freed unconditionally at line 2689 regardless of whether inner and outer share the pointer). The bgp_fsm.c clear is placed in bgp_peer_process_gr_cap_clear_stale, which is the single site already responsible for resetting NSF state on session establishment. The new topotests cover both the TCP-error-handler and doppelganger code paths. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant R2
participant R1_recv as R1 bgp_notify_receive
participant R1_tcp as R1 TCP handler
participant R1_dp as R1 bgp_accept (doppelganger)
participant R1_fsm as R1 bgp_establish
R2->>R1_recv: NOTIFICATION Cease/Hard-Reset (subcode 9)
Note over R1_recv: peer->notify.hard_reset = true (unconditional, Fix 1)
R2->>R1_tcp: TCP RST / close
alt TCP error handler path (Fix 2)
R1_tcp->>R1_tcp: bgp_process_conn_error
Note over R1_tcp: !peer->notify.hard_reset -> skip NSF_WAIT
R1_tcp->>R1_tcp: Flush stale routes
else Doppelganger path (Fix 3)
R2->>R1_dp: New TCP SYN (fast reconnect)
Note over R1_dp: bgp_accept: peer still Established+NSF_MODE
Note over R1_dp: !peer->notify.hard_reset -> skip NSF_WAIT
R1_dp->>R1_tcp: "TCP_connection_closed event -> flush routes"
end
R2->>R1_fsm: New BGP session establishes
Note over R1_fsm: bgp_peer_process_gr_cap_clear_stale (Fix 4)
Note over R1_fsm: peer->notify.hard_reset = false (cleared for next session)
Reviews (2): Last reviewed commit: "bgpd: Fix GR helper retaining stale rout..." | Re-trigger Greptile |
Problem: When a BGP peer sends a Hard Reset (Cease/subcode 9, per RFC 8538), the GR helper side must flush peer routes immediately rather than retaining them as stale. Four code gaps allow the NSF_WAIT flag to be set (or the hard_reset flag to be missed) after a Hard Reset, causing stale routes to remain in the RIB: 1. bgp_packet.c (bgp_notify_receive): peer->notify.hard_reset is only set when the received Hard Reset has a non-empty encapsulated payload (outer.length > 0). A valid bare Hard Reset (Cease/subcode 9 with no inner notification) leaves peer->notify.hard_reset = false. This bypasses the existing !hard_reset guard for the TCP error handler and doppelganger handler paths below. 2. bgpd.c (bgp_process_conn_error / TCP error handler): When the TCP socket reports an error or close, the handler unconditionally sets NSF_WAIT if NSF_MODE is set. If the peer sent a Hard Reset and the TCP stack delivered the RST before the NOTIFICATION was read from the socket, peer->notify.hard_reset is already true but is ignored here, causing GR helper mode to be entered and routes kept stale. 3. bgp_network.c (bgp_accept / doppelganger handler): Same issue -- when an incoming TCP collision tears down the existing established session, NSF_WAIT is set without checking peer->notify.hard_reset. A peer that sends Hard Reset and immediately reconnects can trigger this path before the NOTIFICATION event is processed by the FSM. 4. bgp_fsm.c (bgp_establish): peer->notify.hard_reset is NOT cleared when a new BGP session is established. After a Hard Reset the peer reconnects and establishes a new session. If that new session later drops normally (TCP drop, no notification), the TCP error handler sees the stale hard_reset=true from the previous session and skips NSF_WAIT, permanently defeating GR for that neighbor until a restart. Note: bgp_packet.c already has correct !hard_reset guards for the NOTIFICATION send path (~line 1058) and receive path (~line 2704). The bgp_packet.c fix here closes the gap that lets those guards be bypassed. FRR upstream master and stable/10.4 have the same gaps as of April 2026. The bgp_packet.c send/recv path fix corresponds to upstream PR FRRouting#18498 (merged to master 2026-03-26, not backported to stable/10.4). Solution: - bgp_packet.c: Set peer->notify.hard_reset = true whenever hard_reset is true, independently of whether outer.length > 0. - bgpd.c: Add !peer->notify.hard_reset check in the TCP error handler before setting NSF_WAIT. - bgp_network.c: Add !peer->notify.hard_reset check in the doppelganger handler before setting NSF_WAIT. - bgp_fsm.c: Clear peer->notify.hard_reset in bgp_establish() alongside PEER_STATUS_NSF_WAIT to prevent the flag from leaking across sessions. Topotest: - TC3: Add test_bgp_hard_reset_gr() -- Hard Reset with delayopen=60 (both sides). Exercises bgp_process_conn_error (bgpd.c fix). Verifies R1 does not retain stale routes after receiving a Hard Reset. - TC4: Add test_bgp_hard_reset_gr_doppelganger() -- Hard Reset with delayopen=5 on R1 only, no delayopen on R2. R2 reconnects immediately after Hard Reset, maximising the probability that R2's new TCP SYN reaches R1's listen socket while R1 is still in Established state, triggering the bgp_accept() doppelganger handler (bgp_network.c fix). R1's delayopen=5 holds the new session in OpenWait for ~5 s providing a stable observation window. Verifies routes are flushed in either code path (TCP error handler or doppelganger). Resolves: FRRouting#21822 Signed-off-by: selva <selva@nexthop.ai>
0511210 to
eba9757
Compare
| } | ||
|
|
||
| UNSET_FLAG(peer->sflags, PEER_STATUS_NSF_WAIT); | ||
| peer->notify.hard_reset = false; |
There was a problem hiding this comment.
That's the completely wrong place to set peer->notify foobar... It should be either at the reception or when we send it...
There was a problem hiding this comment.
Thanks @ton31337 for your comment.
The race condition this fix is handling like below;
- Hard Reset NOTIFICATION received β bgp_notify_receive β peer->notify.hard_reset = true --> BGP_Stop FSM event
- FSM --> bgp_stop() - session tearing down
- TCP RST arrives late --> bgp_process_conn_error fires --> must see hard_reset = true to skip NSF_WAIT
If we do clear it "at reception" (i.e., in bgp_notify_receive or bgp_stop), step 3 would see hard_reset = false β NSF_WAIT gets set β routes retained as stale. That's the exact bug we're fixing.
There was a problem hiding this comment.
Why we do need to keep peer->notify at all once the session is established?
|
@greptile review |
|
lints need to be fixed. |
|
@ton31337 could you do the needful to merge? |
Problem:
When a BGP peer sends a Hard Reset (Cease/subcode 9, per RFC 8538), the GR helper side must flush peer routes immediately rather than retaining them as stale. Four code gaps allow the NSF_WAIT flag to be set (or the hard_reset flag to be missed) after a Hard Reset, causing stale routes to remain in the RIB:
bgp_packet.c (bgp_notify_receive): peer->notify.hard_reset is only set when the received Hard Reset has a non-empty encapsulated payload (outer.length > 0). A valid bare Hard Reset (Cease/subcode 9 with no inner notification) leaves peer->notify.hard_reset = false. This bypasses the existing !hard_reset guard for the TCP error handler and doppelganger handler paths below.
bgpd.c (bgp_process_conn_error / TCP error handler): When the TCP socket reports an error or close, the handler unconditionally sets NSF_WAIT if NSF_MODE is set. If the peer sent a Hard Reset and the TCP stack delivered the RST before the NOTIFICATION was read from the socket, peer->notify.hard_reset is already true but is ignored here, causing GR helper mode to be entered and routes kept stale.
bgp_network.c (bgp_accept / doppelganger handler): Same issue -- when an incoming TCP collision tears down the existing established session, NSF_WAIT is set without checking peer->notify.hard_reset. A peer that sends Hard Reset and immediately reconnects can trigger this path before the NOTIFICATION event is processed by the FSM.
bgp_fsm.c (bgp_establish): peer->notify.hard_reset is NOT cleared when a new BGP session is established. After a Hard Reset the peer reconnects and establishes a new session. If that new session later drops normally (TCP drop, no notification), the TCP error handler sees the stale hard_reset=true from the previous session and skips NSF_WAIT, permanently defeating GR for that neighbor until a restart.
Note: bgp_packet.c already has correct !hard_reset guards for the NOTIFICATION send path (~line 1058) and receive path (~line 2704). The bgp_packet.c fix here closes the gap that lets those guards be bypassed.
FRR upstream master and stable/10.4 have the same gaps as of April 2026. The bgp_packet.c send/recv path fix corresponds to upstream PR #18498 (merged to master 2026-03-26, not backported to stable/10.4).
Solution:
Topotest:
Resolves: #21822