Bug description After an installed IPv4 BGP route loses its only recursive NEXT_HOP resolver, BIRD 2.19.3 and 2.20.0 mark the route unreachable but continue advertising it to an ordinary eBGP neighbor. In three isolated ordinary forwarding runs on 2.19.3 and one on 2.20.0, the neighbor kept a unicast route via the DUT while the DUT returned ICMP host-unreachable for that destination. The DUT sent another announcement, rather than a withdrawal, after resolver removal. This is an ordinary forwarding router: no route reflector, route server, graceful restart, deliberate discard policy, or policy to announce routes independently of forwarding. A reachable control prefix and both BGP sessions remain healthy. This may be a concrete instance of the recursive-route behavior discussed in 2013 and 2019; I am not claiming a newly introduced regression or an issue unknown to the project. Software version Tested source and executed binary identities: - BIRD 2.19.3: source 5c6376d3075a58d1d378f6d6a7fd50fc0750a72a; binary SHA256 302cdf17722d7601a15aa06d821c8cb92e5b309324a56d19021394e78f2e9793. - BIRD 2.20.0: source 0355729c3a546c910e20bcde423bf36da901f8b5; binary SHA256 f644d09d8a7fa781417394142ecf898bccf146f1c350d339ca7e9d44f9daf91a. Both were built without source patches, using --disable-client --without-libssh --disable-lto and CFLAGS='-O1 -g0'. The nine implicated next-hop, selection and export functions are identical in the two fixed sources. BIRD 3.x and development master have not been tested. The side-by-side validation used openEuler 24.03, Linux x86_64 / WSL2 kernel 6.18.54.1-microsoft-standard-WSL2. The earlier 2.19.3 build log records an older build kernel; it is not used as evidence of the runtime kernel. An additional 2.19.3 ordinary run was completed after recording the current environment. Dependencies: Python 3.11.6, iproute2 6.6.0, util-linux 2.39.1, coreutils 9.4, tcpdump 4.99.4 / libpcap 1.10.4, tshark 4.4.16, iputils 20221126, GCC 12.3.1 (openEuler 38). How to reproduce Dependencies: Linux root with network/mount namespace support, Python 3 standard library, iproute2, util-linux (unshare/nsenter), coreutils timeout, systemd (for the resource-limited command below), tcpdump, tshark, iputils ping, and a BIRD binary built from the stated source. The program uses BIRD's Unix control socket directly; birdc is needed only for the optional manual commands below. It does not import any private test framework or corpus. The attached standalone-next-hop-loss.py is the exact source executed in all six complete runs (four ordinary and two guard controls). Its SHA256 is 6c3bdf292b29ddc8238cccc97e456c19170e0a69ab450c41000fd70981500009. Place it in a short absolute directory, for example /tmp/bird-nh, and create the output directory there. Choose a fresh output location; Unix control-socket paths must remain below 104 bytes. Example invocation: BIRD=/absolute/path/to/bird PROGRAM=/tmp/bird-nh/standalone-next-hop-loss.py OUT=/tmp/bird-nh/out mkdir -p "$OUT" BIRD_SHA=$(sha256sum "$BIRD" | cut -d ' ' -f 1) sudo systemd-run --collect --wait --pipe \ -p CPUQuota=50% -p MemoryMax=256M -p MemorySwapMax=0 -p TasksMax=32 \ timeout --signal=TERM 180 unshare --net --mount --fork \ python3 "$PROGRAM" --bird "$BIRD" --output-root "$OUT" \ --expected-binary-sha256 "$BIRD_SHA" The binary hash argument binds the executable you supplied; compare its version/source separately. A different locally compiled binary is expected to have a different hash. Run the same command with --guard appended for the configuration control. Inspect standalone-result.json: all_gates must be true, error must be null, and cleanup.json must confirm that all owned processes exited. A successful process exit alone is insufficient. The complete sequence is as follows; the attachment also contains the exact ordinary/guard configurations used, so the sequence can be reproduced manually without the Python driver: 1. Create an isolated outer network namespace and three new child namespaces, with no default routes or external interfaces. Use distinct namespace/process identities and record them for cleanup. Create two veth links: source bc_peer=192.0.2.2/24 to DUT bc_dut=192.0.2.1/24, and DUT bc_out=192.0.3.1/24 to observer bc_observer=192.0.3.2/24. Bring the links and each loopback up. On the source, add 198.18.253.1/32 to loopback and create up dummy interfaces bc_target=198.51.155.1/24 and bc_control=198.18.250.1/24. Add source return route 192.0.3.0/24 via 192.0.2.1. Enable IPv4 forwarding only in the DUT namespace. 2. Start one BIRD process in each child namespace with its own config, pid file and Unix control socket (bird -f -c CONFIG -s SOCKET -P PIDFILE). Use the attached baseline configs. Source and DUT use AS65001; observer uses AS65003. Router IDs are 10.0.0.2, 10.0.0.1 and 10.0.0.3 respectively. All sessions are direct, with hold time 30 and keepalive time 10; GR/LLGR are off. The DUT's iBGP input has gateway recursive, igp table bc_igp and import table off. bc_igp contains the two connected subnets and static protocol bc_resolver's route 198.18.253.1/32 via 192.0.2.2. Its eBGP output accepts BGP-learned routes. Both DUT and observer export to their own namespace's kernel FIB. Initially the source advertises only control 198.18.250.0/24. 3. Wait for both sessions to be Established and control routes to be unicast. Verify target 198.51.155.0/24 is absent from DUT and observer. Start full-snaplen captures on both DUT links and wait until tcpdump reports readiness. Change only the source config to the attached peer-announced.conf and issue configure. This adds the connected target route and sets its BGP NEXT_HOP to 198.18.253.1. Wait for DUT and observer target routes to become unicast. From the observer, send two pings each to 198.51.155.1 and 198.18.250.1; both must receive two replies. Save routes, kernel FIB and captures. 4. Without changing the source advertisement, either peer, interfaces or BGP policy, issue disable bc_resolver on the DUT's control socket. For example, use birdc -s DUT_SOCKET 'disable bc_resolver'. Confirm that show route for 198.18.253.1 table bc_igp all reports no match. For at least 35 seconds, keep capturing both links and sample show protocols all, show route 198.51.155.0/24 all, show route export bc_out all, and each namespace's ip -j route show. The source must send no target announcement/withdrawal, both-direction keepalives must continue, and there must be no SYN, FIN, RST or NOTIFICATION. Send the target/control pings again. Verify captures report zero kernel drops. 5. Issue enable bc_resolver. Require target route and successful target/control pings to recover without restarting BGP or sending a new source target UPDATE. Then restore the source baseline config, issue configure, observe the actual source withdrawal and target absence on both receivers. Restart the three owned BIRD processes with baseline configs and verify fresh Established sessions, healthy control and target absence. Stop owned captures/processes and remove only the namespaces/resources created for this reproduction. The program performs this cleanup even after an error and leaves all evidence files in OUT. 6. Repeat from a fresh setup with the sole DUT export-policy change: insert `if dest = RTD_UNREACHABLE then reject;` before `if source = RTS_BGP then accept;`. This is the --guard control. It should withdraw the target after resolver loss and announce it again on recovery. This is a diagnostic workaround, not a proposed blanket policy for route servers or deliberate discard routes. Expected behavior For this forwarding-router deployment, losing the only resolver should make the learned route ineligible for ordinary dissemination and cause withdrawal from the observer; resolution recovery should make it eligible again without requiring another source UPDATE. Keeping an internal candidate for recovery is compatible with this expectation. This follows RFC 4271 sections 9.1.2, 9.1.2.1 and 9.1.3: eligibility depends on next-hop resolvability, and a previously advertised route that is no longer eligible must be withdrawn. Section 9.1.3's exception for policy that intentionally advertises independently of forwarding does not describe this fixture. All 21 listed RFC errata were reviewed; none changes these requirements. No assertion here depends on the formatting or star marker of BIRD's internal route display. https://www.rfc-editor.org/rfc/rfc4271.html#section-9.1.3 https://errata.rfc-editor.org/search/?rfc_number=4271&presentation=records Actual behavior All three ordinary 2.19.3 runs and the ordinary 2.20.0 run passed all 111 prerequisite/control/cleanup checks. After resolver loss the DUT had an unreachable target route, its kernel held an unreachable route, and the observer retained a unicast target via 192.0.3.1. The loss capture contained one DUT target reannouncement and zero target withdrawals in each run. The measured loss observation windows were 37.0 to 42.1 seconds (the program schedules seven five-second waits plus observations). Baseline and recovery target pings each returned 2/2 replies; during loss the DUT returned host-unreachable while control pings still returned 2/2. The source input and BGP sessions stayed unchanged. One guard run on each version also passed all 111 checks. During loss it contained one target withdrawal and no target reannouncement; the observer removed the target. Resolution recovery restored the route and successful pings. In each ordinary run, the abnormal result is continued route advertisement toward a router that cannot forward it; failure of a target ping after intentionally removing the only resolver is not, by itself, the bug. No alternate-path failover, forwarding loop or unrelated destination failure is claimed. Additional information The attachment contains the exact standalone program, role configs, and raw captures, route/FIB snapshots, ping outputs, config hashes and cleanup records for the ordinary/guard 2.20.0 pair. The six-run audit identifies every run; the attached raw records cover the ordinary/guard 2.20.0 pair. Two earlier standalone setup failures (unsupported version-option spelling and an overlong Unix-socket path) are retained separately and are not counted as DUT failures. The final attached source is the corrected, unchanged source that completed all six runs. The attached ZIP is self-contained: no access to our evidence repository is required. It includes the exact executable source, role configurations, six-run audit, recovery-wire audit and the raw ordinary/guard 2.20.0 evidence pair. The source hash above identifies the program actually run. Related design discussions: https://bird.network.cz/pipermail/bird-users/2013-January/008045.html https://bird.network.cz/pipermail/bird-users/2019-April/013331.html They discuss retaining recursive BGP candidates and limiting propagation of invalid routes. The present report supplies a small ordinary-router forwarding reproduction, rather than asserting that the architectural issue is new. The documented limitation with import table on is avoided by explicitly using import table off. This is also distinct from choosing among multiple IGP resolver routes: the fixture removes its sole matching resolver. Code location and suggested fix The following chain is read from the exact 2.19.3 source identified above. Source statements are verified; the correspondence with the observed execution is an inference supported by the independent captures and guard control. I did not instrument native callback entry/exit or test a C patch. 1. proto/bgp/packets.c, bgp_apply_next_hop() (around 1101): recursive input obtains a hostentry and applies it to the route attributes. 2. nest/rt-table.c, rt_update_hostentry() (3664), rt_update_hostcache() and rta_apply_hostentry() (2554): removing the last IGP match resets the hostentry to RTD_UNREACHABLE; dependent routes receive that destination and lose their forwarding nexthop. 3. nest/rt-table.c, rt_next_hop_update_net() (2833): the retained route participates in best-route recomputation and export notification. proto/bgp/attrs.c, bgp_rte_better() (2165), favors reachable alternatives but does not exclude the sole unreachable candidate. 4. proto/bgp/attrs.c, bgp_preexport() (1880), specifically rejects unreachable FlowSpec, but has no corresponding failed-recursion eligibility gate for this ordinary IPv4 BGP route. The configured RTS_BGP export filter accepts it. 5. proto/bgp/packets.c, bgp_use_next_hop(), bgp_use_gateway() and bgp_update_next_hop_ip() (1211, 1252, 1283): on this different-interface ordinary eBGP output, the unusable forwarding gateway is not used and NEXT_HOP becomes the DUT's channel address. This matches the captured reannouncement to the observer. Source links: https://gitlab.nic.cz/labs/bird/-/blob/5c6376d3075a58d1d378f6d6a7fd50fc0750a... https://gitlab.nic.cz/labs/bird/-/blob/5c6376d3075a58d1d378f6d6a7fd50fc0750a... https://gitlab.nic.cz/labs/bird/-/blob/5c6376d3075a58d1d378f6d6a7fd50fc0750a... Suggested correction: separate retained recursive-candidate state from eligibility for ordinary forwarding-router export. In or before bgp_preexport(), reject a BGP-learned unicast-family candidate whose recursive hostentry is unresolved before NEXT_HOP rewriting makes it appear usable to the receiver; hostcache changes must still trigger withdrawal and subsequent re-advertisement when resolution returns. A useful discriminator to review is non-null attrs->hostentry combined with RTD_UNREACHABLE, rather than rejecting every unreachable destination. The exact ordinary-router applicability/opt-out contract should be explicit so intentional non-forwarding route-server/reflector deployments retain their supported behavior. In filter/f-inst.c, SA_DEST deliberately clears hostentry when setting blackhole/unreachable/prohibit. That distinction should be preserved; likewise, keep the retained candidate needed for recovery. Do not turn the observed kernel unreachable entry alone into a blanket request to remove defensive discard routes. Suggested regressions: initial reachable route, resolver-only loss, captured withdrawal, resolver-only recovery, usable alternate BGP candidate, explicit discard policy, and intended RR/RS exceptions. IPv6 and MP variants need separate tests. No source patch has been written, compiled or executed; only the stated export-filter diagnostic was tested. Checklist - Exact binary/source identity and complete independent program retained; the attached source hash matches all six executed-source copies. - Three ordinary 2.19.3 runs, one ordinary 2.20.0 run and one guard per version completed with explicit recovery and cleanup. - Original announcement, unchanged source/session controls, actual egress UPDATEs and independent ICMP evidence verified. - RFC applicability, listed errata, documented import-table limitation and related historical discussions reviewed. - Full written sequence, role configs, dependencies and executable program provided; no private framework required. - Source mechanism, inference, configuration workaround and untested source-code proposal are distinguished. - No native callback instrumentation or source patch was executed; the export-filter control is a tested diagnostic workaround only.