<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Abdul Wasey — Writing</title><description>Notes on networking, the Linux data path, firmware, and self-hosted infrastructure.</description><link>https://w453y.me/</link><language>en-us</language><managingEditor>awasey8905@gmail.com (Abdul Wasey)</managingEditor><webMaster>awasey8905@gmail.com (Abdul Wasey)</webMaster><copyright>© 2026 Abdul Wasey</copyright><atom:link href="https://w453y.me/rss.xml" rel="self" type="application/rss+xml"/><item><title>Line-card BFD for plain Linux, part 11: every bug came from a capture, none from a log</title><link>https://w453y.me/writing/bfd-11-every-bug-came-from-a-capture/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-11-every-bug-came-from-a-capture/</guid><description>Ten milestones, six upstream bug reports, and one method that survived all of it. Several of the bugs were introduced by the project&apos;s own tooling and design decisions, including ones I was confident about.</description><pubDate>Wed, 05 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This closes the series. &lt;a href=&quot;https://w453y.me/writing/bfd-01-measuring-the-folklore/&quot;&gt;Part 1&lt;/a&gt;
started with folklore and a stress ladder; nine parts later the engine does
dual-stack, echo, and multihop, and six bugs went upstream to FRR.&lt;/p&gt;
&lt;p&gt;The thing worth keeping is none of that. It is the method.&lt;/p&gt;
&lt;h2 id=&quot;every-bug-came-from-a-capture-none-from-a-log&quot;&gt;Every bug came from a capture, none from a log&lt;/h2&gt;
&lt;p&gt;That is not a rhetorical flourish. Going through them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Init-loop&lt;/strong&gt; from a stale transmit schedule after timer renegotiation.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;socket buffer&lt;/strong&gt; convincing a starved daemon that packets were arriving
on time, while the peer watched 300ms of silence.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;etf qdisc blackholing ARP&lt;/strong&gt;, taking the interface with it.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;pipeline scheduling Poll answers a second into the future&lt;/strong&gt;, producing a
perfectly periodic renegotiation loop.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;eaten Final&lt;/strong&gt;: userspace TX suppressed at exactly the moment the Final
needed sending, giving a stable 32.6ms failure loop.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;same timestamp race written twice by the same author&lt;/strong&gt;, on two sides of
the kernel boundary, three weeks apart, producing the identical 18-quintillion
millisecond log line both times.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;multihop injector aimed at the wrong port&lt;/strong&gt;, passing three tests while
validating nothing.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;echo returns dropped by GTSM&lt;/strong&gt; before the echo branch was reached,
because returns arrive at TTL 254 by definition.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of them produced healthy-looking logs and a wire that told a different
story. Several were introduced by the project’s own tooling and design
decisions, including ones I was confident about.&lt;/p&gt;
&lt;h2 id=&quot;the-three-things-that-survived&quot;&gt;The three things that survived&lt;/h2&gt;
&lt;p&gt;The first is an observer outside the system under test. The hypervisor saw what
the guests could not, and when a starved daemon’s logs said packets were
arriving on schedule while the peer reported 300ms of silence, only an
instrument outside both could settle which one was lying. Any measurement taken
by the thing being measured is suspect precisely when it matters most, which is
the same lesson the &lt;a href=&quot;https://w453y.me/writing/bfd-09-echo-mode/&quot;&gt;echo loss counter&lt;/a&gt; taught in a
different shape and which I had to learn twice.&lt;/p&gt;
&lt;p&gt;The second is distributions over averages. The p99-fine, max-fatal pattern from
&lt;a href=&quot;https://w453y.me/writing/bfd-01-measuring-the-folklore/&quot;&gt;part 1&lt;/a&gt; is invisible any other way: a
dashboard showing p99 packet spacing would have reported a perfectly healthy
daemon straight through 44 flaps. The same idea rescued the echo milestone,
where flap count turned out to vary from 0 to 20 across runs of identical code,
and replacing it with a per-session distribution turned an unsupportable claim
into a defensible bound.&lt;/p&gt;
&lt;p&gt;The third is a refusal to let any claim stand untested when a tcpdump could
settle it, whether the claim is the folklore’s, a reviewer’s, or my own. That is
why the repo ships every capture behind every number, and why the numbers that
did not survive contact with a capture are in there too.&lt;/p&gt;
&lt;h2 id=&quot;what-i-would-say-to-someone-starting-something-similar&quot;&gt;What I would say to someone starting something similar&lt;/h2&gt;
&lt;p&gt;Measure the thing you are about to replace, before you replace it. I nearly
skipped that step, and the naive daemon that
&lt;a href=&quot;https://w453y.me/writing/bfd-02-the-folklore-is-partly-wrong/&quot;&gt;refused to die&lt;/a&gt; reframed the
entire project: the problem was never “userspace”, it was the architecture of
the wakeup path.&lt;/p&gt;
&lt;p&gt;Write down the suggestions you rejected and why. That file is now the most
useful documentation in the repo.&lt;/p&gt;
&lt;p&gt;And treat a test that passes on the first try with suspicion. Twice in this
project a green result meant the harness was pointed somewhere the code was not.&lt;/p&gt;
&lt;h2 id=&quot;the-honest-limitations-restated&quot;&gt;The honest limitations, restated&lt;/h2&gt;
&lt;p&gt;All numbers are from VMs. The stress was applied inside the guest and hit every
backend identically, so the &lt;strong&gt;comparisons are load-bearing&lt;/strong&gt;, but the absolute
figures await a bare-metal reproduction, which remains the top item on the
roadmap.&lt;/p&gt;
&lt;p&gt;RX-clocked TX requires an asynchronously-clocked peer. Two RX-clocked
implementations facing each other would echo each other into silence.&lt;/p&gt;
&lt;p&gt;No authentication and no demand mode.&lt;/p&gt;
&lt;p&gt;And kernel-path TX is immune to scheduler starvation but shares fate with
softirq latency. It grazed the detect budget once in eleven minutes of hostile
load and self-healed in 3.8ms. That is a different universe from the userspace
failure mode, but it is a universe with physics in it.&lt;/p&gt;
&lt;p&gt;Everything is at &lt;a href=&quot;https://github.com/w453y/xdp-bfd&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;github.com/w453y/xdp-bfd&lt;/a&gt;,
captures included.&lt;/p&gt;</content:encoded><category>bfd</category><category>xdp</category><category>debugging</category><category>method</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Enabling echo mode took the session down, and the kernel picked the wrong address</title><link>https://w453y.me/writing/bfdd-ipv6-echo-source-address/</link><guid isPermaLink="true">https://w453y.me/writing/bfdd-ipv6-echo-source-address/</guid><description>A hardening fix made IPv6 echo reflection conditional on knowing the session. The sending side never chose its source address, so the guard rejected legitimate echoes and the session failed.</description><pubDate>Mon, 03 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Commit 7786df1467, “bfdd: gate IPv6 echo reflection on known sessions”, is a
good change. An unguarded echo reflector will bounce a self-addressed packet
back to whoever sends one, which is an amplification primitive. Requiring the
packet’s source and local address to name an existing BFD session closes that.&lt;/p&gt;
&lt;p&gt;The problem is that the sending side never picks a source address, so the guard
also rejects the legitimate echoes of its own peer. Turning on &lt;code&gt;echo-mode&lt;/code&gt; takes
a working IPv6 session out of service.&lt;/p&gt;
&lt;h2 id=&quot;who-chooses-the-source&quot;&gt;Who chooses the source&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ptm_bfd_echo_snd()&lt;/code&gt; sends IPv6 echoes through the shared per-VRF socket
&lt;code&gt;bvrf-&gt;bg_echov6&lt;/code&gt;. That socket is not bound to any address. So the kernel
chooses a source from the outgoing interface, following RFC 6724 source address
selection.&lt;/p&gt;
&lt;p&gt;On the receiving end, &lt;code&gt;bp_bfd_echo_in()&lt;/code&gt; builds its lookup key from that chosen
source together with the local address, and calls &lt;code&gt;bfd_key_lookup()&lt;/code&gt;. If the
kernel picked anything other than the session’s configured &lt;code&gt;local-address&lt;/code&gt;, the
key does not match a session, the echo is dropped, and the sender’s echo
detection eventually declares the path dead.&lt;/p&gt;
&lt;p&gt;The session goes down. Not a degraded feature: an outage caused by enabling a
feature.&lt;/p&gt;
&lt;h2 id=&quot;why-most-people-never-see-it&quot;&gt;Why most people never see it&lt;/h2&gt;
&lt;p&gt;RFC 6724 does longest-prefix match first. If your addresses live in different
prefixes, the rule picks the one in the peer’s prefix, which is the right one,
and everything works.&lt;/p&gt;
&lt;p&gt;You need &lt;strong&gt;more than one address inside the peer’s own prefix&lt;/strong&gt; for the
selection to become ambiguous. That happens with service or anycast addresses on
the link, during renumbering, or with privacy extensions turned on. Common
enough to matter, rare enough that the guard shipped without anyone hitting it.&lt;/p&gt;
&lt;p&gt;IPv4 is unaffected, and for an interesting reason. &lt;code&gt;bp_udp_send_fp()&lt;/code&gt; builds the
frame with source equal to destination, so the neighbour’s forwarding plane
loops it. The received source is therefore always the peer’s own address, by
construction. The v4 path never asks the kernel to choose.&lt;/p&gt;
&lt;h2 id=&quot;the-fix-and-why-it-is-not-a-setsockopt&quot;&gt;The fix, and why it is not a setsockopt&lt;/h2&gt;
&lt;p&gt;The source is attached per message as an &lt;code&gt;IPV6_PKTINFO&lt;/code&gt; control message rather
than set once with &lt;code&gt;setsockopt&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (pktinfo &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; NULL&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        size_t&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; used &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; msg.msg_controllen &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; CMSG_SPACE&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;sizeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(ttlval)) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        msg.msg_control &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; msgctl;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        msg.msg_controllen &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; used &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; CMSG_SPACE&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;sizeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;pktinfo));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        cmsg &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; used &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; CMSG_NXTHDR&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;msg, &lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;CMSG_FIRSTHDR&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;msg)) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; CMSG_FIRSTHDR&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;msg);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        cmsg-&gt;cmsg_level &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; IPPROTO_IPV6;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        cmsg-&gt;cmsg_type &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; IPV6_PKTINFO;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        cmsg-&gt;cmsg_len &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; CMSG_LEN&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;sizeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;pktinfo));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;        memcpy&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;CMSG_DATA&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(cmsg), pktinfo, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;sizeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;pktinfo));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;used&lt;/code&gt; dance at the top is because the hop-limit cmsg may already be in the
buffer, so this appends rather than overwrites. That is the sort of detail that
is invisible in a description and unavoidable in the code.&lt;/p&gt;
&lt;p&gt;It cannot be sticky. The same socket carries every session’s echoes &lt;strong&gt;and&lt;/strong&gt; the
reflection path. A source set for one session would leak into another session’s
reflection, which trades one wrong-source bug for a different one. The
reflection call site passes NULL and keeps the kernel’s choice, which is correct
there.&lt;/p&gt;
&lt;p&gt;The caller side is three lines and says exactly what it is defending against:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * The echo socket is shared and unbound, so the kernel would&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * otherwise pick any address on the outgoing interface. The peer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * reflects only echoes whose source names a known session, so an&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * arbitrary source gets the echo dropped there.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;memcpy&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;pktinfo.ipi6_addr, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;key.local, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;sizeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(pktinfo.ipi6_addr));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;ifp)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        pktinfo.ipi6_ifindex &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;ifp&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;ifindex;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;pktinfop &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;pktinfo;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;writing-a-test-for-source-address-selection&quot;&gt;Writing a test for source address selection&lt;/h2&gt;
&lt;p&gt;This was the genuinely fiddly part, and most of the diff.&lt;/p&gt;
&lt;p&gt;The topotest needs the kernel to pick the &lt;em&gt;wrong&lt;/em&gt; address on purpose. My first
instinct was address ordering: add the decoy afterwards and let the rules sort
it out. That does not work. Among equally preferred candidates, Linux keeps
selecting the address that has been on the interface longest, so adding a decoy
later has no effect at all.&lt;/p&gt;
&lt;p&gt;What does work is RFC 6724 rule 3, which prefers non-deprecated addresses. Put a
second global address on r1 in the session’s prefix, then mark the session’s own
&lt;code&gt;local-address&lt;/code&gt; deprecated with &lt;code&gt;preferred_lft 0&lt;/code&gt;. Now the kernel actively
prefers the decoy.&lt;/p&gt;
&lt;p&gt;The test also asserts, as a precondition, that the kernel really does select the
decoy. Without that assertion, a future change to source selection would make
the test pass because the bug can no longer be triggered, rather than because
the bug is fixed. A test that quietly stops testing anything is worse than no
test.&lt;/p&gt;
&lt;p&gt;Echo mode is configured on both routers, because bfdd only opens the per-VRF
IPv6 echo socket when a session uses echo. With echo on the sender alone, the
peer never reaches the reflection path at all.&lt;/p&gt;
&lt;p&gt;Verified both ways: patched, everything passes. With the fix reverted, the
reflection and stability assertions fail, r1 logs &lt;code&gt;up -&gt; down reason:echo-failed&lt;/code&gt;
about every 1.3 seconds, and the source-selection precondition still passes,
which is exactly the signature you want from a regression test.&lt;/p&gt;
&lt;p&gt;Open as &lt;a href=&quot;https://github.com/FRRouting/frr/pull/22920&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#22920&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-from-it&quot;&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;The hardening commit was right, and it exposed a latent gap rather than creating
one: the sender was always relying on the kernel to guess, and nothing had
checked the guess before. Adding a correctness requirement on the receive side
turned a silent assumption on the send side into an outage.&lt;/p&gt;
&lt;p&gt;And the test taught me more than the fix. Making a kernel reliably do the wrong
thing, on demand, took several attempts and a rule in RFC 6724 I had not needed
before.&lt;/p&gt;</content:encoded><category>bfd</category><category>frr</category><category>ipv6</category><category>rfc-6724</category><category>Upstream bugs in FRR’s BFD daemon</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 10: multihop, and a test that validated nothing</title><link>https://w453y.me/writing/bfd-10-multihop/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-10-multihop/</guid><description>One comparison changes from equals 255 to greater than a minimum. The TTL work passed all three cases and proved nothing, because the injector was sending to the wrong port.</description><pubDate>Sun, 26 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Multihop looks like the smallest milestone in the project.&lt;/p&gt;
&lt;p&gt;Single-hop BFD proves adjacency with the TTL: a control packet must arrive at
255, so anything routed has been decremented and is rejected. Multihop gives
that up by definition, since packets cross routers and arrive lower, and the
receiver enforces a configured minimum instead.&lt;/p&gt;
&lt;p&gt;One comparison changes from &lt;code&gt;== 255&lt;/code&gt; to &lt;code&gt;&gt;= minimum&lt;/code&gt;. The plumbing was already
there and being thrown away: the dataplane registration carries a TTL byte and a
multihop flag, bfdd fills both, and the engine parsed the byte and discarded it
while the reject mask refused any session carrying the flag.&lt;/p&gt;
&lt;h2 id=&quot;where-the-comparison-can-live&quot;&gt;Where the comparison can live&lt;/h2&gt;
&lt;p&gt;The GTSM check sits in the parser, before any session lookup, and that placement
is the point: a spoofed flood at TTL 64 dies in a handful of instructions having
touched no map.&lt;/p&gt;
&lt;p&gt;But a per-session minimum is not known until the session is found. Enforcing it
there would mean every rejected packet paying for a hash lookup first, trading
away the cheapest rejection path in the engine to support a mode most
deployments never enable.&lt;/p&gt;
&lt;p&gt;The resolution is a single bit in a flags map saying whether any multihop
session exists at all. When it is clear, the parser behaves exactly as before,
byte for byte. When it is set, the verdict for a below-255 packet defers to
after the config lookup:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/* Deferred GTSM. A control packet that did not arrive at 255 is&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * acceptable only if it names a configured session whose minimum&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * admits it. An unconfigured pair must still drop: the&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * promiscuous PASS above exists for observation, not to relax&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * GTSM. Single-hop sessions carry min_ttl 255, so nothing below&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * 255 reaches them and their behaviour is unchanged. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;__u8 pttl &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; iph &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; iph-&gt;ttl &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (ip6 &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; ip6-&gt;hop_limit &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (pttl &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 255&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        __u32 mt &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (cfg &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cfg-&gt;min_ttl) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cfg-&gt;min_ttl &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 255&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;cfg &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;||&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; pttl &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; mt) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;                count&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_DROP;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;!cfg&lt;/code&gt; half of that condition matters more than it looks. The lookup’s miss
path returns &lt;code&gt;XDP_PASS&lt;/code&gt; for the standalone observer, so dropping only on
&lt;code&gt;pttl &amp;#x3C; mt&lt;/code&gt; would have quietly started leaking low-TTL packets for unconfigured
address pairs straight to userspace, undoing one of the hardening guarantees
from &lt;a href=&quot;https://w453y.me/writing/bfd-06-hardening-and-a-review-pass/&quot;&gt;part 6&lt;/a&gt;. A relaxation added
for one feature reopening a hole closed by another is the kind of interaction
that no single test looks for.&lt;/p&gt;
&lt;p&gt;Note also &lt;code&gt;min_ttl&lt;/code&gt; defaulting to 255 when there is no config. Single-hop
sessions carry 255, so the comparison is unchanged for them and the multihop
code costs them nothing but a branch that is never taken.&lt;/p&gt;
&lt;p&gt;The test that matters is not that multihop works. It is that &lt;strong&gt;a packet at TTL
200 aimed at a single-hop session is still dropped while multihop is active
elsewhere on the same box&lt;/strong&gt;. Enabling a relaxation for one session must not
relax anything for another.&lt;/p&gt;
&lt;h2 id=&quot;then-the-milestone-taught-the-same-lesson-the-project-keeps-teaching&quot;&gt;Then the milestone taught the same lesson the project keeps teaching&lt;/h2&gt;
&lt;p&gt;At this point I had a clean change, a defensible design for where the check
lives, and a harness that exercised all three cases: above the minimum accepted,
below it dropped, single-hop still strict. Everything passed. I was ready to
call it done in an afternoon, which should have been the warning.&lt;/p&gt;
&lt;p&gt;It validated nothing. The injector was sending to UDP 3784, and &lt;strong&gt;RFC 5883
multihop runs on 4784.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Real multihop traffic was reaching the parser, failing a port check that only
knew about single-hop, and being passed to a stack where nothing was listening
for it. The capture said so immediately: the neighbour transmitting to 4784, the
engine replying to 3784, tcpdump labelling one Multihop and the other Control.&lt;/p&gt;
&lt;p&gt;A test that agrees with you because it exercises the wrong thing is worse than
no test, and the only defence is to check what the wire actually carries rather
than what the harness was told to send.&lt;/p&gt;
&lt;h2 id=&quot;the-reply-had-a-subtler-version-of-the-same-problem&quot;&gt;The reply had a subtler version of the same problem&lt;/h2&gt;
&lt;p&gt;The kernel bounce rewrites the received frame in place and sends it back, which
for single-hop is correct &lt;strong&gt;by accident&lt;/strong&gt;: the packet arrives at 255 and leaves
at 255.&lt;/p&gt;
&lt;p&gt;For multihop the reply would go out already decremented, lose more crossing
back, and be measured against the peer’s own minimum. The session would
establish in one direction and fail in the other, and it would present as a peer
that flaps rather than as a TTL problem.&lt;/p&gt;
&lt;p&gt;RFC 5883 asks for multihop to be sent at 255 precisely so the receiver can count
hops, which the userspace path had always done via a socket option. The bounce
now matches it, with an incremental checksum update for v4 and a plain
assignment for v6.&lt;/p&gt;
&lt;h2 id=&quot;one-ipv6-asymmetry-worth-recording&quot;&gt;One IPv6 asymmetry worth recording&lt;/h2&gt;
&lt;p&gt;The single-hop socket sets &lt;code&gt;IPV6_MINHOPCOUNT&lt;/code&gt; to 255, so the kernel discards
low-hop packets before userspace ever sees them. A good defence there and a
fatal one for multihop, where every packet arrives below 255 by definition.&lt;/p&gt;
&lt;p&gt;The multihop socket deliberately omits it and lets XDP enforce the per-session
minimum instead, which loses nothing, because that is where the minimum is known
anyway.&lt;/p&gt;
&lt;h2 id=&quot;reading-the-validation-took-a-trick&quot;&gt;Reading the validation took a trick&lt;/h2&gt;
&lt;p&gt;Both families were validated the same way: above the minimum accepted, below it
dropped, a single-hop session still strict at 255 with multihop live, and an
injected packet below 255 reflected back at 255.&lt;/p&gt;
&lt;p&gt;The last of those needed a trick to read, because injected packets and their
replies are indistinguishable from live session traffic by address and port. But
scapy leaves the IPv6 traffic class at zero while FRR sets it, and the bounce
preserves whatever arrived, so the replies to injected packets identify
themselves.&lt;/p&gt;
&lt;p&gt;That surfaced a small trap of its own: &lt;strong&gt;tcpdump omits the class and flowlabel
fields entirely when both are zero&lt;/strong&gt;, so a pattern written against the normal
output format silently matches none of them. Twenty replies sat in the capture
looking, to a careless grep, like nothing at all.&lt;/p&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-11-every-bug-came-from-a-capture/&quot;&gt;The method&lt;/a&gt;, which is the only
part of this project I would defend without qualification.&lt;/p&gt;</content:encoded><category>bfd</category><category>xdp</category><category>rfc-5883</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 9: echo mode, and instrumentation that cannot see its own failure</title><link>https://w453y.me/writing/bfd-09-echo-mode/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-09-echo-mode/</guid><description>XDP cannot originate packets, so echo split along the line the hardware draws. Then the loss counter read zero through a 2.6 second stall, and both readings were correct.</description><pubDate>Sat, 25 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Echo was the feature I expected to be easy.&lt;/p&gt;
&lt;p&gt;It is the one part of BFD where the packet belongs to neither endpoint’s control
plane. The originator sends a packet addressed to itself, the neighbour’s
forwarding plane loops it straight back without its BFD daemon ever seeing it,
and the originator times the round trip. It tests the neighbour’s data path
rather than its software, which is exactly the sort of thing a dataplane engine
should be good at. Every other milestone had been about dragging work down into
the kernel; this one looked like it belonged there already.&lt;/p&gt;
&lt;p&gt;It turned out to be the only feature in the project that the architecture
forbids outright, and working out why took longer than any of the code.&lt;/p&gt;
&lt;h2 id=&quot;xdp-cannot-originate-packets-and-there-is-no-way-around-it&quot;&gt;XDP cannot originate packets, and there is no way around it&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;XDP_TX&lt;/code&gt; is a verdict on a frame that has just arrived. That is precisely what
makes &lt;a href=&quot;https://w453y.me/writing/bfd-04-rx-clocked-tx/&quot;&gt;the control path&lt;/a&gt; work here, the peer’s
packet is the clock, but an echo has no inbound packet to clock off.&lt;/p&gt;
&lt;p&gt;There is no timer callback that can transmit. No helper produces a second packet
from one. The redirect helpers move the frame rather than copy it. The
kernel-side escape is &lt;code&gt;bpf_clone_redirect&lt;/code&gt;, which exists only for TC and lwt,
and TC never sees anything at the echo cadence here because control packets are
&lt;code&gt;XDP_TX&lt;/code&gt;’d straight past it.&lt;/p&gt;
&lt;p&gt;Putting echo transmission in the kernel would have meant moving the control
bounce out of XDP into TC: skb allocation in the hot path, and a rewrite of the
one mechanism this whole project rests on.&lt;/p&gt;
&lt;p&gt;That is the trade in full. Gain a kernel-side echo originator, lose the
RX-clocked control path that is the entire result. I declined it, and I want to
be honest that declining took a while, because the alternative is admitting a
feature does not fit and shipping something less than you intended. Writing down
why took longer than the feature would have.&lt;/p&gt;
&lt;h2 id=&quot;so-the-milestone-split-along-the-line-the-hardware-draws&quot;&gt;So the milestone split along the line the hardware draws&lt;/h2&gt;
&lt;p&gt;Answering a neighbour’s echo is a reflection, and a reflection has a packet in
hand, so it lives entirely in XDP: MAC swap, TTL decrement, checksum recompute,
&lt;code&gt;XDP_TX&lt;/code&gt;, no session lookup, payload untouched. It is the same trick as the
control path, applied to somebody else’s packet.&lt;/p&gt;
&lt;p&gt;Originating one has no such luxury, so it runs from userspace over a raw socket.
A raw socket rather than a normal UDP one, because a self-addressed packet
through the latter is routed to loopback and never reaches the wire at all.&lt;/p&gt;
&lt;p&gt;That split is not a compromise I am papering over. One half is a production
capability and the other is a diagnostic, they have different reliability
properties, and the documentation says so in those words.&lt;/p&gt;
&lt;p&gt;The reflector’s argument is capability rather than speed. With &lt;code&gt;ip_forward=0&lt;/code&gt;
the host stack discards a self-addressed echo as a martian, so a non-router host
cannot participate in echo at all. The reflector lets it, without advertising
the box as a forwarder.&lt;/p&gt;
&lt;p&gt;The guard matters as much as the mechanism, and it is four lines:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (iph&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;ttl &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 255&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;      /* GTSM: single-hop echoes only */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_PASS;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/* A classic echo is self-addressed to the originator. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (iph&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;saddr &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; iph&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;daddr)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_PASS;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/* Reflect only for a peer of an echo-active session; otherwise&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * this is an arbitrary 3785 packet (amplification vector). */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;struct&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; bfd_addr esrc;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;key_set_v4&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;esrc&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, iph&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;saddr&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;bpf_map_lookup_elem&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;echo_peers&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;esrc&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_PASS;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without that last lookup this is a reflector for anyone who sends a
self-addressed packet to port 3785, which is an amplification primitive with my
name on it. The map is written only for peers of sessions that have echo
enabled, so the box answers exactly the neighbours that asked and nobody else.
&lt;code&gt;XDP_PASS&lt;/code&gt; rather than &lt;code&gt;XDP_DROP&lt;/code&gt; on the rejects, because an unrecognised packet
on 3785 is not necessarily hostile and the stack can have it.&lt;/p&gt;
&lt;h2 id=&quot;two-spec-errors-surfaced-the-moment-there-was-code&quot;&gt;Two spec errors, surfaced the moment there was code&lt;/h2&gt;
&lt;p&gt;The design document proposed demuxing returned echoes by source address and
port, following the unaffiliated-echo draft. That cannot work. The return is
still addressed to our own local address, which names no session, and several
sessions may share one.&lt;/p&gt;
&lt;p&gt;The discriminator written into the payload is the only thing that identifies the
session, and classic echo never loops Your Discriminator, so it survives the
trip untouched. Which makes the return path a lookup on the payload rather than
on the headers:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;__u32 ed &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_ntohl&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(eb&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;my_disc&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;struct&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; session_key &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;ek &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_map_lookup_elem&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;echo_disc&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;ed&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;ek)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_PASS;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;struct&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; session_state &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;es &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_map_lookup_elem&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;bfd_sessions&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, ek);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;es)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_PASS;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;es&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;echo_last_seen_ns &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_ktime_get_ns&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two lookups where the address would normally have given it in one: the
discriminator names a session key, the key names the session. That indirection
is the whole cost of the addresses being useless here.&lt;/p&gt;
&lt;p&gt;The second was subtler and cost a debugging round. Every return was being
dropped before the echo branch was reached, because the v4 parser applies GTSM
first and returns arrive at TTL 254 &lt;strong&gt;by definition&lt;/strong&gt;. The symptom was the
reject counter climbing at exactly the echo rate while the echo counter stayed
flat, which is the kind of thing a counter tells you and a log does not.&lt;/p&gt;
&lt;h2 id=&quot;detection-is-advisory-permanently&quot;&gt;Detection is advisory, permanently&lt;/h2&gt;
&lt;p&gt;The sweep marks each echo-active session alive or not and reports it, but never
feeds the session state machine.&lt;/p&gt;
&lt;p&gt;With userspace transmission, a local scheduling stall is indistinguishable from
a path failure: echoes stop leaving, returns stop arriving, the timestamp goes
stale. Wiring that into the state machine would convert our own scheduling delay
into a teardown, which is the exact failure this engine was built to avoid.&lt;/p&gt;
&lt;p&gt;Verified from both sides: disabling forwarding on the neighbour stops the
returns, loss climbs one for one, echo liveness flips, and all 64 control
sessions stay up.&lt;/p&gt;
&lt;h2 id=&quot;the-sharpest-lesson-in-the-milestone&quot;&gt;The sharpest lesson in the milestone&lt;/h2&gt;
&lt;p&gt;Under load, echo transmission stalled for &lt;strong&gt;2.6 seconds at a time&lt;/strong&gt;. The loss
counter read zero throughout. The liveness flag read healthy.&lt;/p&gt;
&lt;p&gt;Both were correct. Both were useless.&lt;/p&gt;
&lt;p&gt;Loss only increments when an echo is outstanding as the next one falls due, and
during a total stall nothing ever falls due. The liveness figure is printed on
transmit, and transmit is what stopped.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Instrumentation driven by the thing being measured cannot observe that thing
failing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A windowed inter-send gap, sampled independently, showed it immediately: 13ms
idle against a 10ms interval, already 30% over budget with nothing running.&lt;/p&gt;
&lt;h2 id=&quot;and-a-lesson-about-my-own-habits&quot;&gt;And a lesson about my own habits&lt;/h2&gt;
&lt;p&gt;Measuring the cost of the always-on additions produced a second lesson.&lt;/p&gt;
&lt;p&gt;Flap count had been the working metric since the bake-off. At 64 sessions it
turned out to vary from &lt;strong&gt;0 to 20 across runs of identical reference code&lt;/strong&gt;,
which is wider than any effect worth measuring.&lt;/p&gt;
&lt;p&gt;A full day went into chasing that variance. The cause was a &lt;code&gt;debug bfd peer&lt;/code&gt;
line persisted in the neighbour’s config file, surviving every restart,
contaminating every run including the supposed baselines. Several confident
conclusions were drawn and retracted in the process, each overturned by the next
run.&lt;/p&gt;
&lt;p&gt;The metric was replaced with the per-session maximum transmit gap, which yields
64 numbers per run instead of one rare event. The answer became a bound rather
than a claim: the reference build’s own median spans 22.3 to 27.1ms across two
runs, the echo build sits at 23.2 inside that spread, so the additions cost less
than roughly 5ms of median. Which is not the same as zero, and the docs say that
too.&lt;/p&gt;
&lt;h2 id=&quot;the-upstream-bug-had-two-halves-facing-each-other&quot;&gt;The upstream bug had two halves facing each other&lt;/h2&gt;
&lt;p&gt;RFC 5880 requires that echo not be transmitted faster than the neighbour
advertises it can receive. For an offloaded session bfdd never performs that
negotiation, which I wrote up
&lt;a href=&quot;https://w453y.me/writing/bfdd-echo-interval-negotiation/&quot;&gt;separately&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But that fix has no input unless the dataplane reports the neighbour’s
advertisement upward, and &lt;strong&gt;this engine was hardcoding it to zero&lt;/strong&gt;. The engine
also had the mirror of the same bug outbound: it advertised its own echo receive
interval as zero, which the RFC defines as “cannot receive echo packets”, so no
conforming neighbour would ever have echoed at it.&lt;/p&gt;
&lt;p&gt;Which explains, in retrospect, why every reflector test until then had used a
hand-built frame.&lt;/p&gt;
&lt;p&gt;With both halves fixed the chain closes and can be watched end to end: the
neighbour advertises 200ms, the daemon negotiates it against a locally
configured 50ms, and the wire cadence moves from 50 to 200. And the reflector
finally answered a real implementation rather than a scapy script: &lt;strong&gt;433 echoes,
433 reflected, 12 microseconds minimum turnaround&lt;/strong&gt;, on a host that would have
dropped every one of them as a martian.&lt;/p&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-10-multihop/&quot;&gt;Multihop&lt;/a&gt;, which looks like the smallest milestone
and taught the same lesson the project keeps teaching.&lt;/p&gt;</content:encoded><category>bfd</category><category>xdp</category><category>echo</category><category>instrumentation</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>The RFC says negotiate, but nobody was listening for offloaded sessions</title><link>https://w453y.me/writing/bfdd-echo-interval-negotiation/</link><guid isPermaLink="true">https://w453y.me/writing/bfdd-echo-interval-negotiation/</guid><description>BFD echo packets must not be sent faster than the peer says it can receive them. For sessions handed to a dataplane, bfdd skipped that negotiation entirely and sent whatever was configured locally.</description><pubDate>Sat, 25 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Echo mode is the part of BFD where you send a packet addressed to yourself, the
peer’s forwarding plane loops it back, and you time the round trip. It tests the
peer’s data path rather than its control plane, which is the whole point.&lt;/p&gt;
&lt;p&gt;RFC 5880 section 6.8.9 puts a limit on it: you must not transmit echo packets
faster than the interval the remote system advertises in its Required Min Echo
RX field. The effective interval is the larger of what you want and what the
peer says it can take.&lt;/p&gt;
&lt;p&gt;While implementing echo in &lt;a href=&quot;https://github.com/w453y/xdp-bfd&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;xdp-bfd&lt;/a&gt;, I found
that for offloaded sessions bfdd never performed that negotiation.&lt;/p&gt;
&lt;h2 id=&quot;where-the-negotiation-lives-and-why-it-was-unreachable&quot;&gt;Where the negotiation lives, and why it was unreachable&lt;/h2&gt;
&lt;p&gt;For a normal session, bfdd does the calculation in &lt;code&gt;bs_echo_timer_handler()&lt;/code&gt;.
That function has exactly two callers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the control packet receive path in &lt;code&gt;bfd_packet.c&lt;/code&gt;, which bfdd does not execute
for offloaded sessions, because the dataplane is processing control packets&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bfd_set_echo()&lt;/code&gt;, where the call sits behind &lt;code&gt;if (bs-&gt;bdc == NULL)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;bs-&gt;bdc&lt;/code&gt; is the dataplane context. It is non-NULL precisely when the session is
offloaded. So the guard excludes exactly the sessions that reach this code
through the first path being unavailable.&lt;/p&gt;
&lt;p&gt;The result is that &lt;code&gt;bs-&gt;echo_xmt_TO&lt;/code&gt; stays at 0, and
&lt;code&gt;_bfd_dplane_session_fill()&lt;/code&gt; puts the locally configured
&lt;code&gt;bs-&gt;timers.desired_min_echo_tx&lt;/code&gt; into the &lt;code&gt;min_echo_tx&lt;/code&gt; field of
&lt;code&gt;DP_ADD_SESSION&lt;/code&gt;. The dataplane faithfully transmits at that rate.&lt;/p&gt;
&lt;p&gt;Configure the peer to advertise 200ms and the local side to want 50ms, and you
get echoes every 50ms: four times faster than the peer said it could receive
them.&lt;/p&gt;
&lt;h2 id=&quot;the-information-was-already-there&quot;&gt;The information was already there&lt;/h2&gt;
&lt;p&gt;The frustrating part, and the reason the fix is small, is that bfdd already has
the peer’s advertisement. &lt;code&gt;bfd_dplane_session_state_change()&lt;/code&gt; receives
&lt;code&gt;state-&gt;required_echo_rx&lt;/code&gt; from the dataplane and stores it into
&lt;code&gt;bs-&gt;remote_timers.required_min_echo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It was being recorded and never used. &lt;code&gt;show bfd peer&lt;/code&gt; even displays it
correctly, which makes this genuinely hard to spot from the CLI: the operator
sees the peer’s 200ms reported back accurately while the wire carries 50ms.
Only the value sent to the dataplane, and the actual packet cadence, show the
problem.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;bfd_dplane_echo_negotiate()&lt;/code&gt; that takes the larger of the two values, called
from the state-change handler and from &lt;code&gt;bfd_set_echo()&lt;/code&gt;, so that enabling echo
on an already-established session negotiates too rather than waiting for the
next state change:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bfd_dplane_echo_negotiate&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;struct&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; bfd_session &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;bs&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        uint32_t&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; negotiated;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (bs-&gt;bdc &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; NULL&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; !&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;CHECK_FLAG&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bs-&gt;flags, BFD_SESS_FLAG_ECHO))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        negotiated &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; bs-&gt;remote_timers.required_min_echo &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;                             bs-&gt;timers.desired_min_echo_tx&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                     ?&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; bs-&gt;remote_timers.required_min_echo&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                     :&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; bs-&gt;timers.desired_min_echo_tx;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (bs-&gt;echo_xmt_TO &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; negotiated)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        bs-&gt;echo_xmt_TO &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; negotiated;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;        bfd_dplane_update_session&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bs);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The whole fix is that comparison, and the reason it took a bug report rather
than a glance is that both of its inputs were already sitting in the session
struct. Nothing had to be plumbed anywhere. The two guards are what keep it
inert: it does nothing for a session that is not offloaded or has echo disabled,
and it does not push an update to the dataplane unless the value actually
changed.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;min_echo_tx&lt;/code&gt; then carries the negotiated interval, and the field’s comment says
so explicitly, since the next person to read it deserves to know it is not the
configured value.&lt;/p&gt;
&lt;p&gt;Two things I flagged for reviewers, because both look like behaviour changes and
neither is:&lt;/p&gt;
&lt;p&gt;The wire format does not change. &lt;code&gt;min_echo_tx&lt;/code&gt; now carries a negotiated value
instead of a configured one, but no dataplane could have implemented echo
correctly before this, because it was never told what the peer required. There
was no working behaviour to break.&lt;/p&gt;
&lt;p&gt;The fix is inert against a dataplane that does not populate &lt;code&gt;required_echo_rx&lt;/code&gt;
in &lt;code&gt;BFD_STATE_CHANGE&lt;/code&gt;. In that case &lt;code&gt;remote_timers.required_min_echo&lt;/code&gt; stays 0,
the negotiation is a no-op, and the configured value is used exactly as before.
Which also means testing bfdd on its own shows no change at all, and I said so
explicitly rather than letting a reviewer discover it.&lt;/p&gt;
&lt;p&gt;Verified against a dataplane that does report the field: observed cadence moved
from 50ms to 200ms against a peer advertising 200ms, with no other configuration
change. Merged as &lt;a href=&quot;https://github.com/FRRouting/frr/pull/22805&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#22805&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-from-it&quot;&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;This bug needed two implementations to be visible. The control plane had the
peer’s requirement and did not use it. The dataplane used what it was told and
had no way to know better. Each side was locally reasonable.&lt;/p&gt;
&lt;p&gt;It also only shows up on the wire. Every piece of CLI output was correct
throughout. If I had been checking &lt;code&gt;show bfd peer&lt;/code&gt; rather than counting packet
intervals, I would have concluded it worked.&lt;/p&gt;</content:encoded><category>bfd</category><category>frr</category><category>rfc-5880</category><category>Upstream bugs in FRR’s BFD daemon</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 8: IPv6, and the bake-off argument in miniature</title><link>https://w453y.me/writing/bfd-08-ipv6/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-08-ipv6/</guid><description>Dual-stack in five steps. The measurement that justifies the kernel reply runs both families on one box at the same instant, with the only variable being which side of the kernel boundary the transmit clock lives on.</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-07-multi-session-and-scale/&quot;&gt;Part 7&lt;/a&gt; got the engine to sixty-four
sessions. IPv6 was the next item on &lt;a href=&quot;https://w453y.me/writing/bfd-05-results-and-putting-frr-in-the-loop/&quot;&gt;the
list&lt;/a&gt;, and on the face of
it the least interesting one: parse a different header, branch where the
protocols differ, done.&lt;/p&gt;
&lt;p&gt;There was a question buried in it though, and it had been nagging since the
&lt;a href=&quot;https://w453y.me/writing/bfd-03-the-tx-bakeoff/&quot;&gt;bake-off&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The entire argument for this project rests on one claim: that moving
transmission into the kernel is what buys the resilience, rather than any of the
other things that changed along the way. That claim came from comparing
different programs on different runs. A skeptic could reasonably say the XDP
build was better for some other reason, or that the load was not identical, or
that six weeks of incidental fixes did the work.&lt;/p&gt;
&lt;p&gt;Adding a second address family creates the chance to answer that properly. If
v6 transmits from userspace while v4 rides the kernel path, on the same engine,
on the same box, at the same instant, under the same load, then everything is
controlled except the one variable in question.&lt;/p&gt;
&lt;p&gt;So the milestone had two jobs: make the engine dual-stack, and settle whether
the kernel reply is actually load-bearing.&lt;/p&gt;
&lt;h2 id=&quot;the-structural-move-came-first-and-alone&quot;&gt;The structural move came first, and alone&lt;/h2&gt;
&lt;p&gt;The session key widened from two &lt;code&gt;__be32&lt;/code&gt; fields to two 16-byte addresses, with
v4 stored v4-mapped (&lt;code&gt;::ffff:a.b.c.d&lt;/code&gt;), so both families share one hash map, one
slot-socket pool, and the same fast path with no possibility of key collision.&lt;/p&gt;
&lt;p&gt;That step contains &lt;strong&gt;no v6 code at all&lt;/strong&gt;. It exists to prove the layout change
against the live v4 session before anything interesting is built on it. A
10,000-packet regression run, RX-clocked lockstep intact through the new key.&lt;/p&gt;
&lt;p&gt;Each of the five landing steps carried a v4 regression check, so the shared code
never regressed the working path.&lt;/p&gt;
&lt;h2 id=&quot;the-v6-parse-path&quot;&gt;The v6 parse path&lt;/h2&gt;
&lt;p&gt;A branch on ethertype: fixed 40-byte &lt;code&gt;ipv6hdr&lt;/code&gt;, GTSM as &lt;code&gt;hop_limit&lt;/code&gt; 255, and a
deliberate refusal to walk extension headers. UDP hidden behind an extension
chain never reaches a session, while non-UDP first headers are &lt;code&gt;XDP_PASS&lt;/code&gt;ed to
the stack so ICMPv6 neighbour discovery survives an attached program.&lt;/p&gt;
&lt;p&gt;The kernel reply is family-branched where the protocols actually differ. v4
keeps &lt;code&gt;udp-&gt;check = 0&lt;/code&gt; and the IP-checksum trim recompute. v6 has no IP checksum
but a mandatory UDP one, so the echo recomputes it over the swapped
pseudo-header as a 34-word fold.&lt;/p&gt;
&lt;p&gt;The ordering constraint is the part worth writing down: the fold must complete
&lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;bpf_xdp_adjust_tail&lt;/code&gt;, which invalidates every packet pointer, and
must never read past the 24 BFD bytes that survive the trim.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (ip6) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        __u32 csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        __u16 &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;w &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (__u16 &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;ip6-&gt;saddr;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; i &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;; i &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 16&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;; i&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;   /* saddr + daddr */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;                csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; w&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;[i];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; udp-&gt;len;&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;              /* pseudo length */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_htons&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(IPPROTO_UDP);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        w &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (__u16 &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)udp;&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;              /* UDP hdr, check == 0 */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; i &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;; i &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;; i&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;                csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; w&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;[i];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        w &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (__u16 &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)bfd;&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;              /* 24-byte payload */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; i &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;; i &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 12&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;; i&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;                csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; w&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;[i];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;ffff&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 16&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;ffff&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 16&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        __u16 c &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; ~&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;csum &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;ffff&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        udp-&gt;check &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; c &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; c &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;ffff&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;   /* RFC 768: 0 -&gt; 0xffff */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (excess &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_xdp_adjust_tail&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(ctx, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;excess))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_DROP;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thirty-four words, fixed count, every pointer bounds-proven above so the
verifier accepts it. The correctness of the whole block lives in the order of
those two statements: fold first, trim second. Reverse them and you are folding
over freed pointers, which the verifier will not catch because the pointers were
valid when it checked them.&lt;/p&gt;
&lt;p&gt;The oversized-frame case, 16 trailing bytes echoed back as a valid 24-byte reply
with a recomputed checksum and a patched &lt;code&gt;payload_len&lt;/code&gt;, was verified against
injected traffic rather than just reasoned about.&lt;/p&gt;
&lt;h2 id=&quot;the-experiment&quot;&gt;The experiment&lt;/h2&gt;
&lt;p&gt;Here is the answer to the question this milestone opened with.&lt;/p&gt;
&lt;p&gt;For a window during the landing, the engine was genuinely dual-stack in parsing
but only v4 had the kernel reply. That window is the experiment. Both families
ran the L3+L4 ladder concurrently at 3x300ms: v4 on kernel RX-clocked TX, v6
transmitting from userspace at RFC pacing.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;v4: 0 flaps.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;v6: 19 flaps, TX gaps to 1900ms.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The RT throttle’s ~950ms per second starvation exceeds the 900ms detect budget
once per cycle, each flap recovering autonomously in about 3ms.&lt;/p&gt;
&lt;p&gt;Same run. Same instant. Same engine. Same host, same load, same code except for
one thing: which side of the kernel boundary the transmit clock lives on.&lt;/p&gt;
&lt;p&gt;With the kernel reply enabled, the rerun of the same ladder: &lt;strong&gt;v6 0 flaps, TX
max 300.5ms&lt;/strong&gt;, indistinguishable from v4.&lt;/p&gt;
&lt;p&gt;That settles it. The resilience is not an accident of the rewrite, or of six
weeks of incidental fixes, or of a favourable run. It is the kernel reply, and
here is the same engine failing and then not failing with only that changed.&lt;/p&gt;
&lt;p&gt;You rarely get a controlled experiment that clean outside a textbook, and I got
it by accident, from the ordinary business of landing a feature in stages.&lt;/p&gt;
&lt;h2 id=&quot;scale-and-closing-an-upstream-loop&quot;&gt;Scale, and closing an upstream loop&lt;/h2&gt;
&lt;p&gt;The v6 spoof harness repeated the m5 validation from a third host: wrong
&lt;code&gt;your_disc&lt;/code&gt; and &lt;code&gt;hop_limit&lt;/code&gt; 64 both &lt;code&gt;XDP_DROP&lt;/code&gt;ped and counted, correct
credentials passed.&lt;/p&gt;
&lt;p&gt;The mixed-family scale run closed the milestone: &lt;strong&gt;32 v4 plus 32 v6 at the
64-session cap&lt;/strong&gt;, 3x10ms timers, through the same L3+L4 ladder. Zero flaps in
either family, zero wire transitions, and per-slot maximum TX gaps sitting in a
single 13 to 14.5ms band with the two families statistically indistinguishable.&lt;/p&gt;
&lt;p&gt;The v4-only baseline had shown correlated stall flaps at this session count. The
dual-stack engine beats its own earlier result.&lt;/p&gt;
&lt;p&gt;The scale work also found the 8KB output buffer bug in bfdd that
&lt;a href=&quot;https://w453y.me/writing/bfdd-dataplane-buffer-overflow/&quot;&gt;stranded sessions silently&lt;/a&gt;, and the
validation session that followed found
&lt;a href=&quot;https://w453y.me/writing/bfdd-shutdown-delete-messages-lost/&quot;&gt;two more&lt;/a&gt; in the same file. All
three were invisible without a real dataplane at scale, which is the recurring
theme of this project’s relationship with FRR.&lt;/p&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-09-echo-mode/&quot;&gt;Echo mode&lt;/a&gt;, where the feature I most wanted to put
in the kernel turned out to be the one XDP fundamentally cannot do.&lt;/p&gt;</content:encoded><category>bfd</category><category>xdp</category><category>ipv6</category><category>ebpf</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>A read of zero bytes that looked exactly like a closed socket</title><link>https://w453y.me/writing/bfdd-counters-teardown-zero-length-read/</link><guid isPermaLink="true">https://w453y.me/writing/bfdd-counters-teardown-zero-length-read/</guid><description>Running &quot;show bfd peers counters&quot; killed the dataplane connection on every other invocation. Two defects combined: consumed buffer space was never reclaimed, and a full buffer produced a zero-length read that was read as EOF.</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is my favourite of the FRR bugs I found, because the failure alternates.
Run &lt;code&gt;show bfd peers counters&lt;/code&gt; and it works. Run it again and it tears down the
dataplane connection partway through. Run it again and it works. Every time,
period two.&lt;/p&gt;
&lt;p&gt;Anything with period two is a state machine with somewhere to hide, and in this
case the hiding place was the input buffer.&lt;/p&gt;
&lt;h2 id=&quot;two-defects-and-neither-is-enough-on-its-own&quot;&gt;Two defects, and neither is enough on its own&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;First: consumed space is never reclaimed on the synchronous path.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bfd_dplane_expect()&lt;/code&gt; calls &lt;code&gt;stream_pulldown()&lt;/code&gt; only when &lt;code&gt;reads &gt;= 3&lt;/code&gt; within a
single call. A counters query consumes exactly one message per call, the reply
whose id matches, and then breaks. So the pulldown never fires.&lt;/p&gt;
&lt;p&gt;The event-driven reader, &lt;code&gt;bfd_dplane_read()&lt;/code&gt;, does pull down, but only when a
read event fires. Between two CLI invocations the dataplane sends nothing
unprompted, so no read event happens and nothing reclaims the space.&lt;/p&gt;
&lt;p&gt;After a successful sweep of N sessions, &lt;code&gt;endp&lt;/code&gt; is parked at N times the reply
size with the buffer logically empty. The bytes are consumed. The cursor does
not know that.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second: a full buffer is misdiagnosed as the peer closing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When the buffer fills mid-message, the parse loop’s &lt;code&gt;goto read_again&lt;/code&gt; jumps back
past the buffer-full guard at the top of the function. &lt;code&gt;stream_read_try()&lt;/code&gt; is
then called with size 0. Its guard is &lt;code&gt;STREAM_WRITEABLE &amp;#x3C; size&lt;/code&gt;, which evaluates
to &lt;code&gt;0 &amp;#x3C; 0&lt;/code&gt;, which is false, so it proceeds and issues the equivalent of
&lt;code&gt;read(fd, buf, 0)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A zero-length read returns 0 by definition. And the &lt;code&gt;rv == 0&lt;/code&gt; path means “the
socket closed”, so bfdd calls &lt;code&gt;bfd_dplane_ctx_free()&lt;/code&gt; and tears down a perfectly
healthy connection, with the tail of a valid reply still unread in the kernel
buffer. That unread data is why the wire shows an RST rather than a FIN.&lt;/p&gt;
&lt;p&gt;The comment above the entry guard shows the bogus-close hazard was already known
to whoever wrote it. The mid-loop re-read path just was not covered by it.&lt;/p&gt;
&lt;h2 id=&quot;the-arithmetic-is-exact-which-is-how-i-knew-i-had-it&quot;&gt;The arithmetic is exact, which is how I knew I had it&lt;/h2&gt;
&lt;p&gt;64 sessions, 80-byte counters replies, an 8192-byte buffer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A successful sweep leaves &lt;code&gt;64 x 80 = 5120&lt;/code&gt; bytes of dead space.&lt;/li&gt;
&lt;li&gt;The next sweep has &lt;code&gt;8192 - 5120 = 3072&lt;/code&gt; bytes of headroom, which is 38 full
replies.&lt;/li&gt;
&lt;li&gt;Reply 39 can only partially append. Buffer exactly full. Zero-length read.
Forged EOF. Teardown.&lt;/li&gt;
&lt;li&gt;The reconnect calls &lt;code&gt;stream_reset()&lt;/code&gt;, which clears the residue, so the cycle
starts over. Period two.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;64 + 39 = 103&lt;/code&gt; replies per connection, deterministically, every time. When a
prediction like that lands on the nose, you are done guessing.&lt;/p&gt;
&lt;h2 id=&quot;why-this-is-worse-than-a-crash&quot;&gt;Why this is worse than a crash&lt;/h2&gt;
&lt;p&gt;The failing invocation still prints a full peer list. Every session past the
failure point shows stale or zero counters. The operator gets wrong data
presented as good data, with no error, unless &lt;code&gt;debug bfd distributed&lt;/code&gt; happens to
be on.&lt;/p&gt;
&lt;p&gt;Each occurrence also drops the connection and re-triggers the entire session
registration burst on reconnect. Monitoring that polls this command hits it
persistently.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;Pull the input buffer down at function entry, so consumed space is reclaimed
regardless of which caller you are. And never issue a zero-length read: reclaim
first, and if the buffer is still full of an incomplete message, treat that as
the protocol error it actually is, namely a single message larger than the
entire buffer.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;stream_pulldown&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;inbuf&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * Never issue a zero-length read: `read()` returns 0 and would be&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * misdiagnosed below as the peer closing the connection. If there&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * is no headroom, reclaim consumed space first; a buffer that is&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * still full after that holds a message larger than the buffer,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * which is a protocol violation.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;STREAM_WRITEABLE&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;inbuf&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;        stream_pulldown&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc-&gt;inbuf);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;STREAM_WRITEABLE&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc-&gt;inbuf) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;                zlog_err&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;%s&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;: input buffer full with incomplete message&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, __func__);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;                bfd_dplane_ctx_free&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The unconditional &lt;code&gt;stream_pulldown()&lt;/code&gt; at the top is the actual fix: it runs for
every caller, on every call, so no path can accumulate dead space any more. The
block below it is the belt to that braces, and it turns the impossible case into
a loud one instead of a zero-length read.&lt;/p&gt;
&lt;p&gt;A review comment pushed that second half further. My first version returned -1
on the buffer-full branch without freeing the context, which leaves a wedged
connection behind. It now calls &lt;code&gt;bfd_dplane_ctx_free()&lt;/code&gt; first, matching the
socket-closed and socket-error paths: a peer claiming a message larger than the
whole 8KB buffer is a protocol violation the parser cannot recover from, so
tearing the connection down and reconnecting is the right answer.&lt;/p&gt;
&lt;p&gt;While there I noticed the pre-existing bad-version check a few lines up has
exactly the same gap, &lt;code&gt;return -1&lt;/code&gt; with no free. I left it out rather than widen
a bug fix into unrelated cleanup, and said so on the PR with an offer to send a
follow-up. Scope discipline is easier to defend than to practise.&lt;/p&gt;
&lt;p&gt;Validation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Responder harness, 64 sessions: unpatched delivers exactly 103 replies per
connection before the RST, repeating every two invocations. Patched delivers
384 out of 384 across six consecutive invocations on one uninterrupted
connection, zero resets.&lt;/li&gt;
&lt;li&gt;Against a real dataplane with 64 established sessions: unpatched alternates
strictly, 39 of 64 across ten trials, each failure logging
&lt;code&gt;bfd_dplane_expect: socket closed&lt;/code&gt;. Patched completes ten consecutive sweeps
with zero disconnects.&lt;/li&gt;
&lt;li&gt;Built together with the shutdown drain from #22692 to confirm they coexist.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-review-asked-for-a-test-and-the-test-needed-a-dataplane&quot;&gt;The review asked for a test, and the test needed a dataplane&lt;/h2&gt;
&lt;p&gt;Donald Sharp’s response to the fix was, reasonably, “please write a topotest
that shows this problem is fixed.”&lt;/p&gt;
&lt;p&gt;That is harder than it sounds. The bug lives in &lt;code&gt;bfd_dplane_expect()&lt;/code&gt;, which
only runs when bfdd is in distributed mode connected to an external dataplane
over the bffdp protocol. A standalone bfdd never reaches the code at all, so
there was nothing in the FRR test tree that could trigger it. I had a Python
responder that worked, but wiring a Python helper into a topotest is a
one-off.&lt;/p&gt;
&lt;p&gt;He pointed me at &lt;code&gt;fpm_listener&lt;/code&gt; in &lt;code&gt;fpm_testing_topo1&lt;/code&gt;, which solves the
identical shape of problem for zebra’s FPM: a mock receiver on the far end of
an offload socket that dumps its state on &lt;code&gt;SIGUSR1&lt;/code&gt; so a test can inspect it.&lt;/p&gt;
&lt;p&gt;So the PR grew a second and third commit:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;bfdd/bfd_dplane_listener&lt;/code&gt;&lt;/strong&gt;, a minimal in-tree stand-in for a dataplane. It
accepts the bffdp connection, tracks the sessions bfdd registers and reports
them up, answers counter requests, and dumps what it received on &lt;code&gt;SIGUSR1&lt;/code&gt;. It
runs no BFD state machine and sends no BFD packets. A session is simply declared
up once registered, which is enough for bfdd to treat it as established.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;bfd_dplane_counters_topo1&lt;/code&gt;&lt;/strong&gt;, the test itself: eight peers, twenty counter
sweeps, then assertions that every request was answered, the connection is still
up, and bfdd never reconnected. Against the unpatched daemon it fails with
&lt;code&gt;103 of 160 counter requests were answered&lt;/code&gt;, which is exactly where the buffer
boundary sits.&lt;/p&gt;
&lt;p&gt;Getting that merged-shaped took a round of unglamorous work that I would not
have predicted:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The listener needs &lt;code&gt;XREF_SETUP()&lt;/code&gt;, because anything in &lt;code&gt;sbin_PROGRAMS&lt;/code&gt; gets an
xrelfo pass. Missing it broke the build.&lt;/li&gt;
&lt;li&gt;RPM needs an &lt;code&gt;%exclude&lt;/code&gt; in &lt;code&gt;redhat/frr.spec.in&lt;/code&gt;; Debian needs an entry in
&lt;code&gt;debian/frr-test-tools.install&lt;/code&gt;. The two distros handle &lt;code&gt;fpm_listener&lt;/code&gt;
differently, so both were needed and neither was optional.&lt;/li&gt;
&lt;li&gt;OpenBSD builds with clang and &lt;code&gt;-Werror&lt;/code&gt;, which flagged &lt;code&gt;sigterm_handler&lt;/code&gt; under
&lt;code&gt;-Wmissing-noreturn&lt;/code&gt;. Fixed with &lt;code&gt;FRR_NORETURN&lt;/code&gt;, the same way &lt;code&gt;fpm_listener&lt;/code&gt;
declares its own.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Open as &lt;a href=&quot;https://github.com/FRRouting/frr/pull/22694&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#22694&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-from-it&quot;&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;read(fd, buf, 0)&lt;/code&gt; returning 0 is correct. Treating a 0 return as EOF is
correct. Both of those are true and the combination is a bug, which is the
shape a lot of the interesting ones have.&lt;/p&gt;
&lt;p&gt;The guard that would have caught it existed. It was at the top of the function,
and the failing path jumped over it with a &lt;code&gt;goto&lt;/code&gt;. A precondition you can skip
is a precondition you do not have.&lt;/p&gt;
&lt;p&gt;The larger lesson was about what a fix costs. The actual repair is a handful of
lines. Making it &lt;em&gt;provable&lt;/em&gt; meant contributing test infrastructure that did not
exist, and then chasing it across three packaging systems and a compiler that
nobody in the conversation was using. The reviewer was right to ask. Untestable
code paths stay broken, and the dataplane paths in bfdd had been untestable
since they were written.&lt;/p&gt;</content:encoded><category>bfd</category><category>frr</category><category>debugging</category><category>Upstream bugs in FRR’s BFD daemon</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Deleting sessions into a buffer nobody would ever flush</title><link>https://w453y.me/writing/bfdd-shutdown-delete-messages-lost/</link><guid isPermaLink="true">https://w453y.me/writing/bfdd-shutdown-delete-messages-lost/</guid><description>On shutdown bfdd enqueues a delete for every dataplane session, then closes the socket and frees the buffer with the messages still in it. Fixing an earlier bug turned a total loss into a partial one, which was worse.</description><pubDate>Fri, 17 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This one is the direct sequel to &lt;a href=&quot;https://w453y.me/writing/bfdd-dataplane-buffer-overflow/&quot;&gt;the 8KB buffer
bug&lt;/a&gt;, and it exists partly because
that fix landed.&lt;/p&gt;
&lt;p&gt;When bfdd shuts down it tears down every session. For dataplane sessions,
&lt;code&gt;bfd_session_free()&lt;/code&gt; calls &lt;code&gt;bfd_dplane_delete_session()&lt;/code&gt;, which enqueues one
&lt;code&gt;DP_DELETE_SESSION&lt;/code&gt; message per session into the dataplane client’s output
buffer.&lt;/p&gt;
&lt;p&gt;Nothing ever writes those bytes to the socket.&lt;/p&gt;
&lt;h2 id=&quot;why-they-never-leave&quot;&gt;Why they never leave&lt;/h2&gt;
&lt;p&gt;Enqueuing schedules a write event. On the shutdown path, the event loop does not
run again, so that event never fires. &lt;code&gt;bfd_dplane_ctx_free()&lt;/code&gt; sees
&lt;code&gt;bglobal.bg_shutdown&lt;/code&gt; set, goes straight to &lt;code&gt;free_resources&lt;/code&gt;, closes the socket,
and frees &lt;code&gt;bdc-&gt;outbuf&lt;/code&gt; with the messages sitting in it.&lt;/p&gt;
&lt;p&gt;What the dataplane actually receives depends on which version you are running,
and this is the uncomfortable part:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On 10.5.1&lt;/strong&gt;, before &lt;a href=&quot;https://github.com/FRRouting/frr/pull/22645&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#22645&lt;/a&gt;,
enqueue only schedules the write. &lt;strong&gt;Zero&lt;/strong&gt; deletes reach the dataplane. The
whole burst is silently lost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On current master&lt;/strong&gt;, with #22645, the synchronous flush I added fires once
when the buffer fills mid-burst. So exactly one buffer’s worth is delivered,
and the tail is freed unsent. At 140 bytes per message that is
&lt;code&gt;floor(8192/140) = 58&lt;/code&gt; delivered, and everything past 58 dropped.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I would argue the second case is worse. Losing everything is at least
unambiguous: the dataplane sees no teardown at all and its existing reconnect
logic handles it. Losing an arbitrary subset means the dataplane observes a
partial teardown, in hash order, with no signal that the rest were also meant to
go. It looks like a successful operation.&lt;/p&gt;
&lt;p&gt;So my own fix converted a clean failure into a misleading one. It did not cause
this bug, which was there the whole time, but it changed its shape, and I would
rather say that plainly than let someone else find it.&lt;/p&gt;
&lt;p&gt;#22645 also cannot cover this case. After the last delete is enqueued there is
no further enqueue to trigger a flush, and no event-loop iteration left to
service the scheduled write.&lt;/p&gt;
&lt;h2 id=&quot;reproducing-without-a-dataplane&quot;&gt;Reproducing without a dataplane&lt;/h2&gt;
&lt;p&gt;Same approach as before, and the reason both reports were actionable: you do not
need an offload engine. A TCP socket that accepts a connection, parses the
bfddp message stream, and counts messages per type is enough. Configure 64
sessions, let the registration burst land, stop bfdd cleanly, and count the
deletes.&lt;/p&gt;
&lt;p&gt;The sessions never come up against a sink that does not answer, and that does not
matter. The shutdown delete walk runs for every configured dataplane session
regardless of state.&lt;/p&gt;
&lt;p&gt;Unpatched, the sink reports 58 deletes and 8120 bytes: exactly one full buffer.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;A synchronous drain in &lt;code&gt;bfd_dplane_ctx_free()&lt;/code&gt;, before &lt;code&gt;socket_close()&lt;/code&gt;, run
only when &lt;code&gt;bg_shutdown&lt;/code&gt; is set:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/* Nothing to write or nowhere to write it. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;sock &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; ||&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;connecting)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;while&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; ((remaining &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; STREAM_READABLE&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;outbuf&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        rv &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; write&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc-&gt;sock, &lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;stream_pnt&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc-&gt;outbuf), remaining);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (rv &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                break&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        bdc-&gt;out_bytes &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;uint64_t&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)rv;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;        stream_forward_getp&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc-&gt;outbuf, (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;size_t&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)rv);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;remaining &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; STREAM_READABLE&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;outbuf&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (remaining)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;        zlog_warn&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;%s&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;%zu&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt; bytes of data plane messages lost on shutdown&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, __func__,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;                  remaining);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It is deliberately best-effort. A single pass writing whatever the socket will
take, giving up if it would block or errors, and saying so in the log if
anything is left. The event loop is already gone by this point and the socket
belongs to the frrevent machinery, so re-arming a write event is not available
and blocking on shutdown would be worse than the problem. Unsent deletions are
timed out by the dataplane instead, which is a mechanism that already has to
exist for the case where bfdd is killed rather than stopped.&lt;/p&gt;
&lt;p&gt;The warning matters as much as the loop. The original bug was not that messages
were lost, it was that they were lost &lt;em&gt;silently&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I deliberately did not reuse &lt;code&gt;bfd_dplane_flush()&lt;/code&gt;. Its error path frees the very
context this code is being called from, which would be a use-after-free.&lt;/p&gt;
&lt;p&gt;Results, with the sink and with a real engine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;64 sessions, clean stop: unpatched 58/64, patched 64/64, byte count exact at
140 bytes per message, no loss warning.&lt;/li&gt;
&lt;li&gt;128 sessions: patched delivers 128/128 and 17920 bytes, two full buffer-loads
plus a tail, which confirms the drain loops rather than flushing once.&lt;/li&gt;
&lt;li&gt;Against a live dataplane holding 64 established sessions, 32 IPv4 and 32 IPv6:
unpatched, a clean restart stranded 6 sessions as orphans. Patched, all 64
deletes arrive, zero orphans across the restart, and all 64 re-register with
&lt;code&gt;Output full events: 0&lt;/code&gt;. A second restart in the same run behaves identically,
which shows the registration-side behaviour from #22645 is undisturbed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Open as &lt;a href=&quot;https://github.com/FRRouting/frr/pull/22692&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#22692&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-from-it&quot;&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;Shutdown paths get written as if the work is already done. The event loop is
usually still there to finish anything outstanding, so “enqueue and return”
reads as complete. It is complete right up until the moment you are enqueueing
because you are shutting the loop down.&lt;/p&gt;
&lt;p&gt;The other thing: fixing a bug is a good time to look for the one next to it. The
buffer flush I added made this failure visible by changing 0 into 58, and 58 is
a much stranger number to see in a log than 0.&lt;/p&gt;</content:encoded><category>bfd</category><category>frr</category><category>networking</category><category>Upstream bugs in FRR’s BFD daemon</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>How 8KB of buffer silently lost BFD sessions in FRR</title><link>https://w453y.me/writing/bfdd-dataplane-buffer-overflow/</link><guid isPermaLink="true">https://w453y.me/writing/bfdd-dataplane-buffer-overflow/</guid><description>A distributed-BFD dataplane registration burst overflowed an 8KB output buffer and stranded every session past the limit. Neither implementation owned them, and nothing logged it.</description><pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;While building &lt;a href=&quot;https://github.com/w453y/xdp-bfd&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;xdp-bfd&lt;/a&gt;, an offload engine that
runs the BFD fast path in XDP, I needed FRR’s &lt;code&gt;bfdd&lt;/code&gt; to hand session lifecycle
over to my dataplane. This turned up during &lt;a href=&quot;https://w453y.me/writing/bfd-07-multi-session-and-scale/&quot;&gt;the 64-session scale
run&lt;/a&gt;. Below roughly twenty peers it worked. Above it, sessions
started disappearing, and nothing in the logs said why.&lt;/p&gt;
&lt;h2 id=&quot;what-the-dataplane-protocol-does-at-connect-time&quot;&gt;What the dataplane protocol does at connect time&lt;/h2&gt;
&lt;p&gt;When &lt;code&gt;bfdd&lt;/code&gt; connects to a distributed-BFD dataplane, it walks every configured
session and enqueues one registration message per session into the dataplane
client’s output buffer. That buffer is 8KB, and the whole walk happens in a
single event-loop pass, before the write event ever gets a chance to run.&lt;/p&gt;
&lt;p&gt;At roughly 140 bytes per message, 8KB holds about 58 registrations. With 64
sessions configured, the last six never made it.&lt;/p&gt;
&lt;h2 id=&quot;why-the-failure-was-invisible&quot;&gt;Why the failure was invisible&lt;/h2&gt;
&lt;p&gt;A failed enqueue was not the end of it. For each session past the limit, &lt;code&gt;bfdd&lt;/code&gt;
had already disabled the software implementation, on the assumption that the
dataplane was taking over. It then reset &lt;code&gt;bs-&gt;bdc&lt;/code&gt; to &lt;code&gt;NULL&lt;/code&gt; when registration
failed.&lt;/p&gt;
&lt;p&gt;That combination is the actual bug. The session was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never delivered to the dataplane, so the dataplane did not know it existed&lt;/li&gt;
&lt;li&gt;no longer handled in software, because that path had been switched off&lt;/li&gt;
&lt;li&gt;left with a null dataplane context, so every subsequent operation on it became
a silent no-op&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The session still appeared in &lt;code&gt;show bfd peers&lt;/code&gt;. It simply never ran anywhere.
No warning was logged, because from the enqueue path’s point of view a full
buffer is an ordinary condition.&lt;/p&gt;
&lt;h2 id=&quot;reproducing-it-without-a-dataplane&quot;&gt;Reproducing it without a dataplane&lt;/h2&gt;
&lt;p&gt;The useful part of the investigation was making it reproducible for someone who
does not have an offload engine sitting around. A dataplane does not have to do
anything for this bug to show up: it only has to accept a TCP connection and
count bytes.&lt;/p&gt;
&lt;p&gt;A socket that accepts the connection and reads without ever replying is enough.
Configure N sessions, connect, and count the bytes that arrive. The count stops
at exactly one buffer’s worth, every time, and the cutoff is deterministic.&lt;/p&gt;
&lt;p&gt;That mattered more than the fix. A bug report with a reproducer that needs a
specific piece of unreleased software is a bug report nobody can act on.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;Two commits, each useful on its own.&lt;/p&gt;
&lt;p&gt;The first makes the enqueue path try to flush before giving up. During the
registration burst the socket is almost always writable, and the packet capture
showed zero TCP backpressure, so the common case now simply succeeds:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (buflen &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; STREAM_WRITEABLE&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;outbuf&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    !&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;client &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;connecting)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;        /*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;         * On socket failure or close the context is freed by&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;         * the flush. The buffer is non empty here, so a zero&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;         * return means exactly that: don&apos;t touch `bdc` again.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;         */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;bfd_dplane_flush&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; -&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is a subtlety in the error path. A zero return from the flush with a
non-empty buffer means the context was freed on a socket error, so it must not
be touched again. Clients that are still connecting are skipped entirely,
because flushing would spin on &lt;code&gt;EAGAIN&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A review bot flagged this as possibly leaving queued messages with no scheduled
writer, and a maintainer asked whether it needed fixing. It does not, and being
able to say exactly why is the useful part: &lt;code&gt;bfd_dplane_flush()&lt;/code&gt; loops on
&lt;code&gt;STREAM_READABLE()&lt;/code&gt; and treats &lt;code&gt;EAGAIN&lt;/code&gt;, &lt;code&gt;EWOULDBLOCK&lt;/code&gt;, and &lt;code&gt;EINTR&lt;/code&gt; as retry, so
it cannot return with data still buffered. Its only two exits are a fully
drained buffer, at which point cancelling the write event is correct because
there is nothing left to write, or freeing the context on socket error. After a
successful flush the whole buffer is writeable, so the space check that follows
can only fail for a single message larger than 8KB. The largest bfddp message I
measured is 203 bytes.&lt;/p&gt;
&lt;p&gt;The second commit stops a failed registration from stranding the session. On
failure it logs a warning and returns the session to the software
implementation, which mirrors the recovery that already existed when a dataplane
detaches. Software BFD is worse than offloaded BFD, but it is dramatically
better than nothing at all, which is what the session had before.&lt;/p&gt;
&lt;h2 id=&quot;result&quot;&gt;Result&lt;/h2&gt;
&lt;p&gt;With 128 sessions configured and a byte-counting sink, the unpatched build
delivers exactly 58 registrations, 8120 bytes, and silently loses the other 70.
The patched build delivers all 128, with the byte count exact and
&lt;code&gt;Output full events&lt;/code&gt; at zero.&lt;/p&gt;
&lt;p&gt;End to end against the engine, the reconnect burst that previously registered 40
of 64 sessions now registers all 64.&lt;/p&gt;
&lt;p&gt;The fix is merged to FRR master as
&lt;a href=&quot;https://github.com/FRRouting/frr/pull/22645&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#22645&lt;/a&gt;. It also changed the shape of a
second bug on the shutdown path, which I wrote up
&lt;a href=&quot;https://w453y.me/writing/bfdd-shutdown-delete-messages-lost/&quot;&gt;separately&lt;/a&gt;. Packaged releases up to
10.5.1 still carry the bug, so if you are running distributed BFD with more than
about twenty peers on a released build, add peers through &lt;code&gt;vtysh&lt;/code&gt; after the
dataplane connects rather than putting them in &lt;code&gt;frr.conf&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-from-it&quot;&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;The bug was not in the buffer size. 8KB is a reasonable buffer. The bug was that
a resource limit was being treated as an unremarkable condition, in a code path
where crossing it meant a session belonged to nobody.&lt;/p&gt;
&lt;p&gt;Failure paths that quietly reduce functionality are worse than failure paths
that crash, because a crash gets reported. This one produced a router that
believed it was monitoring a link it was not monitoring, which is precisely the
thing BFD exists to prevent.&lt;/p&gt;</content:encoded><category>bfd</category><category>frr</category><category>networking</category><category>Upstream bugs in FRR’s BFD daemon</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 7: one session to sixty-four</title><link>https://w453y.me/writing/bfd-07-multi-session-and-scale/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-07-multi-session-and-scale/</guid><description>The maps had been sized for 64 sessions since the beginning, and exactly one had ever been tested. Going to 64 found five bugs, one of them upstream, and the design&apos;s own false-flap boundary.</description><pubDate>Mon, 13 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-06-hardening-and-a-review-pass/&quot;&gt;Part 6&lt;/a&gt; took session continuity
and validation off the list from &lt;a href=&quot;https://w453y.me/writing/bfd-05-results-and-putting-frr-in-the-loop/&quot;&gt;part
5&lt;/a&gt;. The next item on it is
the one I had been quietly avoiding, because it invalidates every number
published so far.&lt;/p&gt;
&lt;p&gt;Everything measured up to this point, the whole bake-off, the final matrix, the
FRR integration, has been &lt;strong&gt;one session&lt;/strong&gt;. The maps and the daemon were sized
for 64 from the hardening work onward, and I had written “multi-session capable”
in the docs on the strength of that sizing. Sizing a table for 64 and testing
one entry in it are not the same claim, and the distance between them turned out
to be five bugs wide.&lt;/p&gt;
&lt;p&gt;There is also a reason to expect trouble specifically here. The whole design
rests on &lt;a href=&quot;https://w453y.me/writing/bfd-04-rx-clocked-tx/&quot;&gt;the peer’s packet being the
clock&lt;/a&gt;: a session’s timing comes from its own
inbound stream, processed in softirq. With one session that is a clean story.
With sixty-four it is sixty-four streams sharing one receive path, and the
question of whether they stay independent is exactly the sort of thing that is
obvious in hindsight and untested in fact.&lt;/p&gt;
&lt;h2 id=&quot;four-things-that-only-worked-because-there-was-one-of-them&quot;&gt;Four things that only worked because there was one of them&lt;/h2&gt;
&lt;p&gt;The receive path matched sessions on the peer’s IP address alone, and never
recovered the local destination address at all. With one session that is
sufficient and looks like a design. With two sessions to the same peer from
different local addresses it is a coin flip. Matching now happens on the address
pair, using &lt;code&gt;IP_PKTINFO&lt;/code&gt; with &lt;code&gt;recvmsg&lt;/code&gt; to recover the destination, and it only
matters for bootstrap packets where &lt;code&gt;your_disc&lt;/code&gt; is still zero, since established
sessions demux on the discriminator as before.&lt;/p&gt;
&lt;p&gt;Source ports were next. RFC 5881 says they should be unique per session, which I
had ignored because with one session it is a distinction without a difference.
Each slot now transmits from &lt;code&gt;49152 + slot&lt;/code&gt;, which meant moving userspace
transmission to per-slot sockets, opened lazily and kept for the life of the
process so that a reused slot reuses its socket and teardown needs no file
descriptor bookkeeping.&lt;/p&gt;
&lt;p&gt;The interesting one is map ownership, and it is the same class of mistake as
&lt;a href=&quot;https://w453y.me/writing/bfd-04-rx-clocked-tx/&quot;&gt;the timestamp race in part 4&lt;/a&gt;: two things
touching one value from two contexts, correct in every test until they are not.&lt;/p&gt;
&lt;p&gt;The kernel had been clearing &lt;code&gt;cfg-&gt;poll&lt;/code&gt; in place when the peer’s Final arrived,
while userspace pushed the whole &lt;code&gt;tx_cfg&lt;/code&gt; struct whenever it updated its mirror.
A push landing at the wrong moment could resurrect a Poll sequence that had
already finished. What makes it worth writing about is that the repair is not a
lock:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/* Poll termination (RFC 5880 s6.8.4): peer answered our P with&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * F. tx_cfg is userspace-owned, so ack via kernel-owned&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * final_seq instead of clearing cfg-&gt;poll in place (a racing&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * userspace mirror push could resurrect the finished poll). */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (cfg &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;poll &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;flags &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; BFD_F_FINAL))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        st&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;final_seq &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;poll_seq;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;tx_cfg&lt;/code&gt; became strictly userspace-owned. The kernel acknowledges the Final by
writing &lt;code&gt;final_seq&lt;/code&gt; into &lt;code&gt;session_state&lt;/code&gt;, a map it already owns and userspace
already polls, and &lt;a href=&quot;https://w453y.me/writing/bfd-04-rx-clocked-tx/&quot;&gt;the transmit path from part
4&lt;/a&gt; sets the Poll bit only while &lt;code&gt;final_seq&lt;/code&gt; and
&lt;code&gt;poll_seq&lt;/code&gt; disagree. That is the &lt;code&gt;st-&gt;final_seq != cfg-&gt;poll_seq&lt;/code&gt; condition in
the payload rebuild, which I glossed over at the time.&lt;/p&gt;
&lt;p&gt;Every map value ended up with exactly one writer. The race is not guarded
against, it is unrepresentable, which is a much better place to leave a
concurrency bug than behind a lock you have to remember to take.&lt;/p&gt;
&lt;p&gt;The last one is smaller but has the same shape. The receive path sets a
session’s &lt;code&gt;alive&lt;/code&gt; flag and the sweep clears it, and those run on different CPUs,
so plain byte writes could emit the same liveness event twice. It became a
&lt;code&gt;__u32&lt;/code&gt;, because BPF atomics are 32 or 64-bit only, with both transition sites
using &lt;code&gt;__sync_val_compare_and_swap&lt;/code&gt; and emitting only when the swap wins.&lt;/p&gt;
&lt;h2 id=&quot;two-sessions-then-sixteen-then-sixty-four&quot;&gt;Two sessions, then sixteen, then sixty-four&lt;/h2&gt;
&lt;p&gt;Two concurrent sessions established, slot ports visible bound and on the wire.
Poll sequences driven through the new &lt;code&gt;final_seq&lt;/code&gt; path by changing the transmit
interval to 20ms and back while Up: both negotiated, zero down events, and zero
residual Poll bits on the wire afterwards.&lt;/p&gt;
&lt;p&gt;The test I cared about most was kill isolation, because one session dying must
not disturb its neighbour and until then that had been an assumption rather than
a measurement. FRR was stopped dead on one neighbour, with no farewell packet.
The sweep declared that session down after 31.2ms of silence, inside the same 30
to 33ms envelope as always, while the other session’s uptime ran on
uninterrupted.&lt;/p&gt;
&lt;p&gt;Under two-session L3 stress, the same condition that flaps stock bfdd 44 times,
both sessions held with zero flaps: p50 8.75 / p99 10.02 / max 14.7ms on one and
p50 8.74 / p99 10.02 / max 12.6ms on the other. Two RX-clocked streams do not
degrade each other, which is not obvious in advance, since they share a receive
path.&lt;/p&gt;
&lt;p&gt;Sixteen sessions next, eight per peer via secondary addresses: 0 flaps under L3,
and a mass-kill of 8 detected at 30.5 to 32.8ms with no batch drift.&lt;/p&gt;
&lt;h2 id=&quot;sixty-four-and-five-bugs&quot;&gt;Sixty-four, and five bugs&lt;/h2&gt;
&lt;p&gt;The full-capacity run found five things. Four were mine, and none of them could
have appeared earlier.&lt;/p&gt;
&lt;p&gt;The slot ports I had just introduced collided with bfdd’s own port allocator,
which is the sort of bug that only exists when two programs are running together
at scale; moving the slot range up to &lt;code&gt;65472&lt;/code&gt; and above resolved it. AdminDown
turned out to have no exit, so a session could enter it and stay there. Sourcing
from &lt;code&gt;INADDR_ANY&lt;/code&gt; broke the peer address demux I had also just introduced, so
per-slot sockets now bind their local address explicitly. And the mark-and-sweep
reconciliation added for graceful restart in the previous milestone was
unmarking incorrectly, tearing down sessions on a reconnect where the
discriminators had not changed at all.&lt;/p&gt;
&lt;p&gt;The fifth was not in my code. bfdd’s dataplane client silently and permanently
strands sessions when the registration burst overflows its 8KB output buffer,
which became
&lt;a href=&quot;https://w453y.me/writing/bfdd-dataplane-buffer-overflow/&quot;&gt;FRRouting#22638, fixed in #22645&lt;/a&gt;. It
is the first of three bugs this scale work eventually found in that one file.&lt;/p&gt;
&lt;p&gt;The pattern that started with the &lt;a href=&quot;https://w453y.me/writing/bfdd-unix-socket-einval/&quot;&gt;UNIX socket
bug&lt;/a&gt; held: every time the engine pushed FRR’s
dataplane path somewhere it had not been pushed before, something fell out.&lt;/p&gt;
&lt;h2 id=&quot;the-designs-own-boundary&quot;&gt;The design’s own boundary&lt;/h2&gt;
&lt;p&gt;The 32-session mass-kill was detected in 30.1 to 32.9ms with no drift, the same
envelope as one session. Good.&lt;/p&gt;
&lt;p&gt;But under L3 stress there was &lt;strong&gt;one correlated flap of 19 sessions at once&lt;/strong&gt;,
from a single RX-softirq stall.&lt;/p&gt;
&lt;p&gt;That is worth stating plainly rather than burying, because it is the honest
limit of the design at this scale. RX-clocked TX shares fate with softirq
latency, as &lt;a href=&quot;https://w453y.me/writing/bfd-05-results-and-putting-frr-in-the-loop/&quot;&gt;part 5&lt;/a&gt;
already said for one session. At 64 sessions that fate is &lt;strong&gt;correlated&lt;/strong&gt;: a
single stall in the receive path delays every session’s reply at once, so they
do not fail independently. Recovery was autonomous and took about 50ms, but 19
sessions flapping together is a different operational event from one session
flapping.&lt;/p&gt;
&lt;p&gt;It is the quantified false-flap boundary of the architecture, and finding it was
the point of the milestone. &lt;a href=&quot;https://w453y.me/writing/bfd-01-measuring-the-folklore/&quot;&gt;Part
1&lt;/a&gt; argued that the maximum matters more
than the percentile; this is the same argument turned on my own engine, and the
per-session maximum is what exposed it.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-from-it&quot;&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;“The maps are sized for 64” is not a claim about behaviour, and I had let it
stand in the documentation as though it were. Four of the five bugs here were
mine, all latent for weeks, all invisible to every test I had run, because a
single session cannot exercise demux, cannot collide with a port allocator, and
cannot race another session’s map writes.&lt;/p&gt;
&lt;p&gt;The engine did not get faster in this milestone. It got true.&lt;/p&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-08-ipv6/&quot;&gt;IPv6&lt;/a&gt;, which turned out to contain the cleanest
controlled experiment in the project, and which found the second and third bugs
in the same bfdd file as the first.&lt;/p&gt;</content:encoded><category>bfd</category><category>xdp</category><category>scale</category><category>concurrency</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 6: hardening, and a review that found real edges</title><link>https://w453y.me/writing/bfd-06-hardening-and-a-review-pass/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-06-hardening-and-a-review-pass/</guid><description>Session continuity across control-plane restarts, RFC 5880 validation, and an external code review whose two most annoying suggestions turned out to be genuine bugs.</description><pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-05-results-and-putting-frr-in-the-loop/&quot;&gt;Part 5&lt;/a&gt; ended with a
working benchmark and an embarrassing list: no session continuity, no real
validation, one session tested out of sixty-four, and three RFC 5880 features
missing outright.&lt;/p&gt;
&lt;p&gt;This part takes the first two off that list. It is the unglamorous middle of the
project, where nothing gets faster and the interesting question is no longer
“does it work” but “does it work when someone is trying to break it, or when the
control plane restarts underneath it”.&lt;/p&gt;
&lt;p&gt;Nothing here changes the steady-state path, so &lt;a href=&quot;https://w453y.me/writing/bfd-05-results-and-putting-frr-in-the-loop/&quot;&gt;the numbers from part
5&lt;/a&gt; stand unchanged
throughout.&lt;/p&gt;
&lt;h2 id=&quot;surviving-a-control-plane-restart&quot;&gt;Surviving a control-plane restart&lt;/h2&gt;
&lt;p&gt;The most visible gap was that restarting bfdd dropped every session. The
dataplane tore them down when the control plane disconnected and bfdd re-added
them on reconnect, which is correct in the sense that nothing lies about state,
and useless in the sense that a routine daemon restart takes your routes with
it.&lt;/p&gt;
&lt;p&gt;The fix is a hold mode, &lt;code&gt;--dp-hold&lt;/code&gt;, that keeps wire sessions alive across the
gap. On disconnect the sessions are orphaned rather than deleted. When bfdd
comes back and re-adds them, they are adopted by address pair with discriminator
continuity, and a mark-and-sweep pass reconciles anything that did not come
back. I took the shape of this from Rafael Zalamena’s guidance on the FRR dev
list, since the protocol’s intent matters more than my reading of it. Two
back-to-back FRR restarts now produce zero peer-visible events.&lt;/p&gt;
&lt;p&gt;It is not the default, and should not be. Drop-and-recreate is what the protocol
expects unless an operator has decided otherwise, so continuity is something you
ask for.&lt;/p&gt;
&lt;p&gt;The same milestone added self-initiated Poll sequences on parameter change, per
RFC 5880 s6.8.3, along with transitional transmission during the change. Before
that, altering a timer mid-session could flap it against either side, which is a
poor reward for reconfiguring something.&lt;/p&gt;
&lt;h2 id=&quot;refusing-traffic-properly&quot;&gt;Refusing traffic properly&lt;/h2&gt;
&lt;p&gt;The validation story until this point was thin. It now enforces GTSM at TTL 255,
demux validation per s6.8.6, and session creation gated on control-plane config
so nothing can conjure a session by sending packets at the box. Everything that
fails lands on &lt;code&gt;XDP_DROP&lt;/code&gt;, so a rejected packet never reaches userspace at all
rather than being filtered somewhere later.&lt;/p&gt;
&lt;p&gt;The demux check is the one that does the real work against spoofing:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;__u8 rstate &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; BFD_STATE&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bfd);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (cfg &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;my_disc) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        __u32 ydisc &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_ntohl&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bfd-&gt;your_disc);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (ydisc &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cfg-&gt;my_disc &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;            !&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(ydisc &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; rstate &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;                count&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_DROP;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A packet has to name our discriminator back at us. The exception is the reason
it is not simply an equality test: a peer that has lost its state and is
restarting legitimately sends &lt;code&gt;your_disc&lt;/code&gt; of 0, so that is allowed, but only
while it also reports Down or AdminDown. Without the state condition, zero
becomes a wildcard anyone can send.&lt;/p&gt;
&lt;p&gt;What makes this a validation fix rather than a parsing detail is what happens on
success. A packet that gets past here refreshes the session’s liveness and gets
echoed back by &lt;a href=&quot;https://w453y.me/writing/bfd-04-rx-clocked-tx/&quot;&gt;the transmit path&lt;/a&gt;. Anything
that can forge its way through is therefore able to keep a dead session up
indefinitely, which is precisely the failure BFD exists to prevent. All of it
was tested by injecting from a third host.&lt;/p&gt;
&lt;p&gt;One measurement note cost me an hour and is worth passing on. &lt;code&gt;XDP_DROP&lt;/code&gt;
consumes the frame before any capture hook sees it, so tcpdump shows you
nothing at all. The only trustworthy signal that a drop happened is the map
counters read through &lt;code&gt;bpftool&lt;/code&gt;. A test that “sees no packets” is otherwise
indistinguishable from a test that sent none, and I briefly believed the wrong
one.&lt;/p&gt;
&lt;h2 id=&quot;the-review-pass&quot;&gt;The review pass&lt;/h2&gt;
&lt;p&gt;An external code review of the three source files, taken seriously enough to
answer each point on the wire.&lt;/p&gt;
&lt;p&gt;The structural item was the shared ABI header. The BPF map value structs had
been living as three hand-synchronised copies, one per file that touched them,
which works right up until one side gains a field the others do not and the two
halves start reading different offsets out of the same bytes. They became a
single included definition.&lt;/p&gt;
&lt;p&gt;Two of the flagged edges turned out to be real, and both are the same kind of
mistake.&lt;/p&gt;
&lt;p&gt;The first is IP options. Options push the UDP header to a variable offset, and
the parser was reading the GTSM and discriminator checks from fixed positions.
A packet carrying options therefore walked straight past both guards from the
previous section, the two that exist specifically to stop that packet, and
arrived in userspace unvalidated.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/* IP options (ihl != 5) on a UDP packet: a single-hop BFD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * control packet never carries them. Passing would skip the&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * GTSM/your_disc checks below (UDP header sits at a variable&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * offset with options) and leak the packet to the userspace&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * socket unvalidated - the same bypass class as an XDP_PASS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * reject. Drop it. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (iph&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;ihl &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 5&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;        count&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_DROP;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;udp &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)(iph &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The fix is three lines, and the last line is the reason they are needed:
&lt;code&gt;udp = (void *)(iph + 1)&lt;/code&gt; assumes a 20-byte header. That assumption is fine, and
it is load-bearing, and nothing had been enforcing it. A single-hop BFD control
packet never legitimately carries options, so refusing them outright is cheaper
and safer than learning to parse them.&lt;/p&gt;
&lt;p&gt;The second is bfddp framing. On a framing error the reader reset its buffer and
carried on reading the same stream, which means it can resync onto arbitrary
mid-stream bytes and start interpreting them as messages. It now drops the
connection cleanly, and because &lt;code&gt;--dp-hold&lt;/code&gt; already exists that drop turns into
a hitless reconnect rather than an outage.&lt;/p&gt;
&lt;p&gt;Two RFC-correctness fixes rode along: the s6.8.7 jitter cap at 90% when
&lt;code&gt;detect_mult&lt;/code&gt; is 1, and trimming an over-length echoed frame back to 24 BFD
bytes with a recomputed checksum.&lt;/p&gt;
&lt;p&gt;Every change was checked the same way as everything else here, with an injection
harness and a capture. None touched the steady-state path, so the resilience
numbers from part 5 stand unchanged.&lt;/p&gt;
&lt;p&gt;The rejected suggestions, and the reasoning for rejecting them, are in the repo
under &lt;code&gt;docs/refactor-abi&lt;/code&gt;. Writing down why you did &lt;em&gt;not&lt;/em&gt; take a suggestion is
worth as much as the changes you did take, and it is the part people skip.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-from-it&quot;&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;The two review comments I was most inclined to wave away were the two that were
real. Both were of the form “what if the header is not where you think it is”,
which is exactly the class of assumption that a fast path is built on and a
parser has to defend.&lt;/p&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-07-multi-session-and-scale/&quot;&gt;Sixty-four sessions&lt;/a&gt;, which is the
next item on that list and the one that found the most bugs.&lt;/p&gt;</content:encoded><category>bfd</category><category>xdp</category><category>security</category><category>rfc-5880</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Two bytes of padding, and a transport that never worked</title><link>https://w453y.me/writing/bfdd-unix-socket-einval/</link><guid isPermaLink="true">https://w453y.me/writing/bfdd-unix-socket-einval/</guid><description>bfdd&apos;s UNIX socket dataplane transport failed every connect with EINVAL. The address was correct; the length was two bytes too long, and only AF_UNIX cares.</description><pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I was pointing &lt;a href=&quot;https://github.com/w453y/xdp-bfd&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;xdp-bfd&lt;/a&gt; at FRR’s distributed
BFD dataplane socket and wanted the UNIX transport rather than TCP, since both
ends were on the same host. It never connected. Every three seconds:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;bfdd[13438]: bfd_dplane_client_connect: data plane connection failed: Invalid argument&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The TCP transports worked perfectly against the same engine. Only &lt;code&gt;unixc:&lt;/code&gt; failed.&lt;/p&gt;
&lt;h2 id=&quot;the-address-was-fine-the-length-was-not&quot;&gt;The address was fine, the length was not&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;strace&lt;/code&gt; gave it away immediately:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;connect(16, {sa_family=AF_UNIX, sun_path=&quot;/tmp/frr-repro.sock&quot;}, 112) = -1 EINVAL&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The path is right. The &lt;code&gt;112&lt;/code&gt; is not. On x86_64 with glibc:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sizeof(struct sockaddr_un)  = 110&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sizeof(struct sockaddr_in6) = 28&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sizeof(union addr)          = 112&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bfd_dplane_client_init()&lt;/code&gt; copies the caller’s address into a union of
&lt;code&gt;sockaddr&lt;/code&gt;, &lt;code&gt;sockaddr_in&lt;/code&gt;, &lt;code&gt;sockaddr_in6&lt;/code&gt;, and &lt;code&gt;sockaddr_un&lt;/code&gt;, then records the
length. It records the wrong one:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (salen &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x3C;=&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; sizeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;addr)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;        memcpy&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bdc-&gt;addr, sa, salen);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        bdc-&gt;addrlen &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; sizeof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(bdc-&gt;addr);&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;   /* should be salen */&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It stores the size of the whole union instead of the size of the address it was
given. The union is 112 bytes because &lt;code&gt;sockaddr_in6&lt;/code&gt; forces 8-byte alignment,
padding it past the 110 that &lt;code&gt;sockaddr_un&lt;/code&gt; actually occupies.&lt;/p&gt;
&lt;h2 id=&quot;why-only-af_unix-noticed&quot;&gt;Why only AF_UNIX noticed&lt;/h2&gt;
&lt;p&gt;This is the part worth keeping. The kernel’s AF_UNIX path validates the address
length strictly. &lt;code&gt;unix_validate_addr()&lt;/code&gt; in &lt;code&gt;net/unix/af_unix.c&lt;/code&gt; rejects any
&lt;code&gt;addrlen&lt;/code&gt; greater than &lt;code&gt;sizeof(struct sockaddr_un)&lt;/code&gt; outright, so 112 is refused
with &lt;code&gt;EINVAL&lt;/code&gt; before anything else happens.&lt;/p&gt;
&lt;p&gt;Inet sockets do not. Pass an oversized &lt;code&gt;addrlen&lt;/code&gt; to an AF_INET connect and it is
tolerated, because the kernel only needs the leading bytes it understands. So
the exact same bug, on the exact same line, is invisible on the TCP transports
and fatal on the UNIX one.&lt;/p&gt;
&lt;p&gt;That asymmetry is why this survived so long. &lt;code&gt;git log -S addrlen -- bfdd/dplane.c&lt;/code&gt;
shows the logic unchanged since client mode was introduced in 6655b43d51. As far
as I can tell, &lt;code&gt;unixc:&lt;/code&gt; never worked on Linux, in any release.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;memcpy&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;addr&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, sa, salen);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bdc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;addrlen &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; salen;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One line. The truncating branch keeps &lt;code&gt;sizeof(bdc-&gt;addr)&lt;/code&gt;, which is correct
there, because that path really did fill the whole union.&lt;/p&gt;
&lt;p&gt;I rebuilt bfdd from the 10.5.1 source with only that change and it connected to
the same listener that stock bfdd had been refusing for hours. Merged to master
as &lt;a href=&quot;https://github.com/FRRouting/frr/pull/22621&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;#22621&lt;/a&gt;, reviewed by the
dataplane author.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-from-it&quot;&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;I spent longer than I should have looking at permissions, socket paths, and my
own listener, because “Invalid argument” on a connect to a socket that plainly
exists reads like the address is wrong. It was not the address. It was the
number sitting next to it.&lt;/p&gt;
&lt;p&gt;The wider lesson is about the shape of the bug rather than the bug. A single
&lt;code&gt;sizeof&lt;/code&gt; on a union that holds several address families is a length that happens
to be right for the largest member and wrong for the rest. It survived because
the family that cares about it strictly was the one nobody was testing.&lt;/p&gt;
&lt;p&gt;Packaged releases up to 10.5.1 still carry this, so use the TCP transport with
them.&lt;/p&gt;</content:encoded><category>bfd</category><category>frr</category><category>linux</category><category>sockets</category><category>Upstream bugs in FRR’s BFD daemon</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 2: the folklore is partly wrong</title><link>https://w453y.me/writing/bfd-02-the-folklore-is-partly-wrong/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-02-the-folklore-is-partly-wrong/</guid><description>I wrote a 160-line BFD daemon expecting it to fail like bfdd did. It ran clean through the load that flapped bfdd 44 times. Userspace was never the problem as a category. The wakeup path was.</description><pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-01-measuring-the-folklore/&quot;&gt;Part 1&lt;/a&gt; measured FRR’s bfdd under a
stress ladder and found it flapping 44 times in 120 seconds under timer
pressure, with a p99 that looked perfectly healthy throughout.&lt;/p&gt;
&lt;p&gt;Before writing any kernel code I wanted a second data point on the “userspace
BFD cannot hold aggressive timers” claim. So I wrote the dumbest possible BFD
daemon: 160 lines, one thread, a &lt;code&gt;recv()&lt;/code&gt; with a 2ms timeout as the main loop
clock, the RFC 5880 state machine, nothing else.&lt;/p&gt;
&lt;p&gt;The plan was to watch it die the same way bfdd did, and then be justified in
building the XDP path.&lt;/p&gt;
&lt;h2 id=&quot;it-did-not-die&quot;&gt;It did not die&lt;/h2&gt;
&lt;p&gt;Under the exact timer and hrtimer stress that flapped bfdd 44 times in 120
seconds, the naive loop ran clean. &lt;strong&gt;Zero flaps, p99 gap 13ms against a 30ms
budget.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I ran it twice because I did not believe it.&lt;/p&gt;
&lt;h2 id=&quot;why-and-it-is-not-luck&quot;&gt;Why, and it is not luck&lt;/h2&gt;
&lt;p&gt;It is scheduler mechanics.&lt;/p&gt;
&lt;p&gt;A task that wakes 500 times a second, does microseconds of work, and sleeps
again accumulates almost no vruntime. So whenever it wakes, CFS considers it the
most deserving thing on the runqueue and it preempts the CPU hogs nearly
instantly.&lt;/p&gt;
&lt;p&gt;bfdd is the opposite shape. It is a heavyweight event loop serving zebra IPC,
config machinery, and many timers, waking on exact intervals. The
timer-subsystem stress was hitting precisely its wakeup path.&lt;/p&gt;
&lt;p&gt;So “userspace” was never the problem as a category. &lt;strong&gt;The architecture of the
wakeup path was.&lt;/strong&gt; The folklore needed rewriting: userspace BFD reliability
depends on winning a scheduling war, and your event-loop architecture decides
how well armed you are.&lt;/p&gt;
&lt;p&gt;Which raises the obvious next question. Is there a war no userspace architecture
wins?&lt;/p&gt;
&lt;h2 id=&quot;yes-sched_fifo&quot;&gt;Yes: SCHED_FIFO&lt;/h2&gt;
&lt;p&gt;Four real-time priority hogs on four vCPUs, and the naive loop finally broke:
&lt;strong&gt;25 flaps in 60 seconds, max TX gap 324ms.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The kernel’s RT throttling reserves about 50ms per second for normal tasks when
RT work saturates the CPUs. No amount of CFS-friendliness helps when you only
exist for one breath per second.&lt;/p&gt;
&lt;p&gt;This matters because it is not an artificial condition. On a loaded router,
packet forwarding, softirq storms, and other RT-priority work outranking a BFD
daemon is the normal state of affairs, not the exception.&lt;/p&gt;
&lt;h2 id=&quot;the-lesson-hiding-inside-the-failure&quot;&gt;The lesson hiding inside the failure&lt;/h2&gt;
&lt;p&gt;During the RT runs, the naive daemon’s own log showed &lt;strong&gt;zero detect timeouts&lt;/strong&gt;.
From its perspective, the peer’s packets kept arriving on schedule.&lt;/p&gt;
&lt;p&gt;They had not. They had been queueing in the socket buffer while the daemon was
starved. When it finally got CPU, it drained the backlog and its state machine
saw a smooth stream of arrivals.&lt;/p&gt;
&lt;p&gt;The peer, watching the actual wire, saw 300ms of silence and correctly declared
the session dead.&lt;/p&gt;
&lt;p&gt;Process logs lie under exactly the conditions you most need them. This is why
every number in this series comes from tcpdump on the hypervisor bridge instead
of from inside the guest, and it is a theme that
&lt;a href=&quot;https://w453y.me/writing/bfd-11-every-bug-came-from-a-capture/&quot;&gt;recurs&lt;/a&gt; until the end of the
project.&lt;/p&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;The question had sharpened. Not “userspace versus kernel”, but “which transmit
architectures survive RT starvation, and at what cost”. I tested five, and the
most instructive result was the one that
&lt;a href=&quot;https://w453y.me/writing/bfd-03-the-tx-bakeoff/&quot;&gt;failed while producing the best p99 of any backend&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>bfd</category><category>linux</category><category>scheduling</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 3: the TX bake-off</title><link>https://w453y.me/writing/bfd-03-the-tx-bakeoff/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-03-the-tx-bakeoff/</guid><description>Five transmit architectures under identical RT starvation. The SO_TXTIME approach produced the best p99 of any backend tested and flapped 48 times, worse than doing nothing special at all.</description><pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-02-the-folklore-is-partly-wrong/&quot;&gt;Part 2&lt;/a&gt; established that
userspace BFD survives ordinary load and loses to SCHED_FIFO. So the question
became: which transmit architectures survive RT starvation, and at what cost?&lt;/p&gt;
&lt;p&gt;Five backends, identical conditions: 4x SCHED_FIFO prio-50 hogs, 60 seconds, the
3x10ms session, wire-truth capture from the hypervisor.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table, scrollable&quot;&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;TX backend&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;flaps&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;p50&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;p99&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;max gap&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;FRR bfdd&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;continuous&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.8&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;287&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;960ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;naive userspace loop&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;25&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;12.0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;324ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;userspace + &lt;code&gt;chrt -f 90&lt;/code&gt; + pinned core&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;13.0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;15.0ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SO_TXTIME + etf, one packet in flight&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;48&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;13.2&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;1517ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;SO_TXTIME + etf, pipelined 5 deep&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;48&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.1&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;994ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;XDP RX-clocked&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.75&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;11.0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;12.5ms&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h2 id=&quot;the-chrt-row-and-its-asterisk&quot;&gt;The chrt row, and its asterisk&lt;/h2&gt;
&lt;p&gt;Running the daemon at RT priority 90, above the prio-50 hogs, works completely.
Zero flaps, 15ms max.&lt;/p&gt;
&lt;p&gt;The asterisk is that you won the priority war because nothing else contested it.
On a real router you cannot assume that. Forwarding work, softirq processing,
and other components are fighting for the same priorities, and a priority
arms-race between your failure detector and your data plane is not a design, it
is a standoff.&lt;/p&gt;
&lt;h2 id=&quot;the-etf-rows-are-the-most-instructive-failure-of-the-project&quot;&gt;The etf rows are the most instructive failure of the project&lt;/h2&gt;
&lt;p&gt;The theory is genuinely attractive. &lt;code&gt;SO_TXTIME&lt;/code&gt; lets userspace enqueue a packet
with an explicit future launch time, and the &lt;code&gt;etf&lt;/code&gt; qdisc releases it at that
nanosecond via hrtimer. Wire timing becomes independent of when userspace got
scheduled.&lt;/p&gt;
&lt;p&gt;And it delivers on exactly that promise. The pipelined variant produced &lt;strong&gt;the
best p99 of every backend tested&lt;/strong&gt;, 10.1ms.&lt;/p&gt;
&lt;p&gt;It also flapped 48 times. Worse than doing nothing special at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero packets were dropped by the qdisc&lt;/strong&gt; in those runs. etf did its job
perfectly on every packet it was given. The problem is that a starved daemon
gives it nothing.&lt;/p&gt;
&lt;p&gt;Timestamping fixes jitter. It cannot manufacture liveness.&lt;/p&gt;
&lt;p&gt;Pipelining five packets ahead buys 50ms of starvation tolerance. The failure
signature in the gap data is unmistakable: max gaps clustering at 915 to 995ms,
which is the RT throttle’s breathing pattern. Userspace gets its ~50ms of CPU
once per second, and a 50ms pipeline against a 950ms drought loses every time.&lt;/p&gt;
&lt;p&gt;Deepening the pipeline does not fix it either, and this is the part I find
genuinely interesting. Pre-built packets carry pre-built protocol state. A
pipeline deep enough to bridge the drought is a second of frozen, potentially
stale announcements. &lt;strong&gt;etf’s tolerance equals pipeline depth, and pipeline depth
is bounded by state staleness, not by etf.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;two-operational-landmines&quot;&gt;Two operational landmines&lt;/h2&gt;
&lt;p&gt;Documented so nobody steps on them twice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A software etf qdisc silently drops every packet on its band that lacks a
launch timestamp. Including ARP.&lt;/strong&gt; Install it on all queues of an interface and
you blackhole the interface. Neighbour resolution dies quietly and everything
above it follows. Scope the tc filter to exactly the timestamped flow, and tear
the qdisc down with the experiment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pipelined sending splits your traffic into two scheduling classes&lt;/strong&gt;, urgent
(handshake, Poll answers) and pipelined (steady state). Getting a packet into
the wrong class produces second-scale protocol latency with zero packet loss. My
first pipelined build scheduled Poll Finals a full second into the future and
produced a perfectly periodic renegotiation loop.&lt;/p&gt;
&lt;p&gt;Both bugs were invisible in logs and obvious in the pcap.&lt;/p&gt;
&lt;h2 id=&quot;what-the-bake-off-left-standing&quot;&gt;What the bake-off left standing&lt;/h2&gt;
&lt;p&gt;One conclusion: to survive RT starvation without priority privileges, packet
transmission has to leave userspace entirely.&lt;/p&gt;
&lt;p&gt;Which is a problem, because &lt;a href=&quot;https://w453y.me/writing/bfd-04-rx-clocked-tx/&quot;&gt;XDP cannot originate a
packet&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>bfd</category><category>linux</category><category>so_txtime</category><category>qdisc</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 4: RX-clocked TX</title><link>https://w453y.me/writing/bfd-04-rx-clocked-tx/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-04-rx-clocked-tx/</guid><description>XDP is an ingress hook. It cannot originate a packet. The way out is to stop trying: rewrite the packet the peer just sent you and bounce it back, so your transmit clock becomes the peer&apos;s transmit clock.</description><pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-03-the-tx-bakeoff/&quot;&gt;Part 3&lt;/a&gt; ended with one conclusion standing:
transmission has to leave userspace entirely.&lt;/p&gt;
&lt;p&gt;The obvious way to do that in BPF does not exist. XDP is an ingress hook. You
can act on packets that arrive, but you cannot originate one. &lt;code&gt;bpf_timer&lt;/code&gt;
callbacks run without a packet context, so there is nothing to &lt;code&gt;XDP_TX&lt;/code&gt; from a
timer. I checked again on kernel 7.0 hoping something had landed. It had not.
The only kernel-side send path, &lt;code&gt;BPF_PROG_RUN&lt;/code&gt; with live frames, is triggered by
a userspace syscall, which puts the scheduler right back in the loop you were
trying to escape.&lt;/p&gt;
&lt;h2 id=&quot;stop-trying-to-originate&quot;&gt;Stop trying to originate&lt;/h2&gt;
&lt;p&gt;In steady state, a BFD peer hands you a packet every 10ms. &lt;strong&gt;Every one of those
packets is a packet context.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So instead of building packets, rewrite the one you just received, in place:
swap the MAC addresses, swap the IP addresses, set our source port, rebuild the
24-byte BFD payload from a config map that userspace keeps current, and return
&lt;code&gt;XDP_TX&lt;/code&gt;. The frame goes back out the same interface about 30 microseconds after
it arrived, entirely in softirq.&lt;/p&gt;
&lt;p&gt;The payload rebuild is the whole of it:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;/* BFD payload from config. P while a Poll sequence is&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt; * active, F when answering the peer&apos;s P; never both. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;vers_diag   &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 5&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;diag &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;1f&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;flags       &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; ((cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;state &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; 0x&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x3C;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 6&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; send_final;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;send_final &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;poll &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; st&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;final_seq &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;poll_seq)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;flags &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;|=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; BFD_F_POLL;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;detect_mult &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;mult;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;len         &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 24&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;my_disc     &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_htonl&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;my_disc&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;your_disc   &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_htonl&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;your_disc&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;min_tx      &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_htonl&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;min_tx_us&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;min_rx      &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_htonl&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;min_rx_us&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;bfd&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;min_echo_rx &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_htonl&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(cfg&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;min_echo_rx_us&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;st&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;tx_pkts&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_TX;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There is no allocation, no socket, and no syscall in there. Every field comes
either from the frame that just arrived or from a map that userspace wrote at
its leisure, which is exactly why none of it cares whether userspace is
currently running.&lt;/p&gt;
&lt;p&gt;If the received packet had the Poll bit set, &lt;code&gt;send_final&lt;/code&gt; is set and the reply
carries Final. Poll sequences get answered for free, faster than any userspace
implementation could.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Our transmit clock is now the peer’s transmit clock.&lt;/strong&gt; There is no timer to
service, no wakeup to miss, no process to starve. SCHED_FIFO hogs can pin every
core at 100% and the replies keep flowing, because softirq processing preempts
them all.&lt;/p&gt;
&lt;p&gt;On the wire this shows up as a signature. Every userspace backend in the
bake-off produced a p50 gap of 10.00ms, its own timer. The XDP path produces
8.75ms: the peer’s RFC-jittered distribution, echoed back.&lt;/p&gt;
&lt;h2 id=&quot;detection-needed-the-mirror-image-trick&quot;&gt;Detection needed the mirror-image trick&lt;/h2&gt;
&lt;p&gt;XDP cannot see silence any more than it can originate. A dead link delivers no
packets, so the program that would notice never runs.&lt;/p&gt;
&lt;p&gt;The fix is one global &lt;code&gt;bpf_timer&lt;/code&gt; sweeping the session map every 5ms, comparing
now minus last-seen against each session’s negotiated detect time, and pushing
an event to userspace through a ring buffer when a session goes quiet.&lt;/p&gt;
&lt;p&gt;Detection latency gets quantised by the sweep interval, measured at 33 to 34ms
against the 30ms RFC detect time, under full stress, and the sweep runs
regardless of what userspace is doing.&lt;/p&gt;
&lt;h2 id=&quot;the-same-bug-twice-by-the-same-author&quot;&gt;The same bug, twice, by the same author&lt;/h2&gt;
&lt;p&gt;Worth confessing because it only shows up under load.&lt;/p&gt;
&lt;p&gt;The sweep snapshots “now”, then walks sessions. A packet can arrive on another
CPU between the snapshot and the check, stamping last-seen newer than now.
Unsigned subtraction wraps to 18 quintillion milliseconds, which comfortably
exceeds any detect time, and you get a phantom session-down.&lt;/p&gt;
&lt;p&gt;The guard is two lines, and they are the reason the sweep can be trusted:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;__s64 delta &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (__s64)(now &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; st&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;last_seen_ns);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (delta &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;   /* packet raced past our now-snapshot */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; ((__u64)delta &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; detect_ns &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;    __sync_val_compare_and_swap&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;st&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;alive&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;        emit&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(k, st, now, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cast to &lt;code&gt;__s64&lt;/code&gt; is the whole fix. Subtract in unsigned and a two-microsecond
race becomes an eighteen-quintillion-millisecond silence; subtract in signed and
it becomes a small negative number you can recognise and ignore.&lt;/p&gt;
&lt;p&gt;I fixed it there, then three weeks later wrote the identical bug into the
userspace map-polling path and got the identical 18-quintillion log line.&lt;/p&gt;
&lt;p&gt;Concurrent readers of monotonic timestamps get you exactly once per privilege
level, apparently.&lt;/p&gt;
&lt;h2 id=&quot;what-userspace-keeps&quot;&gt;What userspace keeps&lt;/h2&gt;
&lt;p&gt;Everything that does not need to be fast: the RFC 5880 state machine, session
bring-up, the 1-second slow-rate transmission the RFC requires while a session
is down, and exactly one packet at the moment of transition to Up.&lt;/p&gt;
&lt;p&gt;That last one earned its place the hard way. My first version suppressed all
userspace TX the moment the session entered Up. Clean division of labour: kernel
speaks, userspace shuts up.&lt;/p&gt;
&lt;p&gt;But the transition to Up is often triggered by a packet carrying the peer’s
Poll, and the Final answering it was the exact packet being suppressed. The
kernel could not send it either: the triggering packet was already consumed, and
XDP only speaks when the next one arrives, which the peer, waiting on its
unanswered Poll, was sending at the 1-second slow rate.&lt;/p&gt;
&lt;p&gt;The result was a beautifully stable failure loop with a 32.6ms period.
Diagnosed, like everything else in this project, not from the logs, which showed
a healthy state machine, but from the pcap, which showed a missing packet.&lt;/p&gt;
&lt;h2 id=&quot;one-structural-limitation-stated-plainly&quot;&gt;One structural limitation, stated plainly&lt;/h2&gt;
&lt;p&gt;RX-clocked TX requires the peer to have its own clock. Two RX-clocked
implementations facing each other would echo each other into silence. Nobody
sends first after a gap.&lt;/p&gt;
&lt;p&gt;The userspace slow-rate path doubles as the recovery spark, and detection of a
dead peer never depended on receiving anyway. But the design assumes an
asynchronous peer, and that assumption should be written on the box.&lt;/p&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-05-results-and-putting-frr-in-the-loop/&quot;&gt;The results&lt;/a&gt;, and handing
session control to FRR.&lt;/p&gt;</content:encoded><category>bfd</category><category>xdp</category><category>ebpf</category><category>design</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 5: results, and putting FRR in the loop</title><link>https://w453y.me/writing/bfd-05-results-and-putting-frr-in-the-loop/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-05-results-and-putting-frr-in-the-loop/</guid><description>Zero flaps through the full stress ladder, one instructive exception that self-healed in 3.8ms, and an unmodified FRR driving the whole thing over its own dataplane protocol.</description><pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://w453y.me/writing/bfd-04-rx-clocked-tx/&quot;&gt;Part 4&lt;/a&gt; described the RX-clocked design. This
is what it measured.&lt;/p&gt;
&lt;p&gt;The full matrix: the complete stress ladder plus a five-minute soak, running at
&lt;strong&gt;ordinary process priority&lt;/strong&gt;, interoperating with stock FRR on the far end.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table, scrollable&quot;&gt;














































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Level&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;p50&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;p99&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;max&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;flaps&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;L1 fair CPU&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.97&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.03&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;13.3&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;L2 sched churn&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.83&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.01&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;12.0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;L3 timer storm&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.72&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.11&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;28.0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;1&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;L4 RT hogs&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.72&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.01&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;11.5&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;5-min soak&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.77&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.01&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.7&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;0&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;For comparison, under that same L3 stress, bfdd flapped 44 times.&lt;/p&gt;
&lt;h2 id=&quot;the-one-flap-deserves-telling-in-full&quot;&gt;The one flap deserves telling in full&lt;/h2&gt;
&lt;p&gt;It is more informative than a column of zeros would have been.&lt;/p&gt;
&lt;p&gt;Under the hrtimer storm, one echoed reply out of roughly 66,000 was delayed 28ms
by softirq latency. The peer’s 30ms detect timer fired at the margin, correctly.&lt;/p&gt;
&lt;p&gt;What followed, reconstructed packet by packet from the capture: the peer’s Down
arrived, userspace noticed via the map within 3ms, transitioned, and answered
the peer’s re-establishment Poll with Final &lt;strong&gt;74 microseconds&lt;/strong&gt; after it
arrived. Down, Init, Up, Poll, Final. The complete RFC handshake, in 3.8
milliseconds.&lt;/p&gt;
&lt;p&gt;Total session downtime across eleven minutes of hostile load: &lt;strong&gt;under 4ms&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;So the honest characterisation is not “immune”. Kernel-path TX is immune to
scheduler starvation but shares fate with softirq latency. Under extreme timer
pressure it grazed the detect budget once and self-healed in less time than a
single packet interval.&lt;/p&gt;
&lt;p&gt;That is a different universe from the userspace failure mode. But it is a
universe with physics in it.&lt;/p&gt;
&lt;h2 id=&quot;making-frr-drive-it&quot;&gt;Making FRR drive it&lt;/h2&gt;
&lt;p&gt;bfdd has a distributed-BFD mode where it delegates all session processing to an
external dataplane over a small TCP or UNIX socket protocol called bfddp,
designed so vendors can attach hardware dataplanes without patching FRR.&lt;/p&gt;
&lt;p&gt;Implementing it took one protocol shim. bfdd creates and owns the sessions,
assigns discriminators, and displays state and counters. The packets ride the
XDP path. &lt;code&gt;show bfd peers&lt;/code&gt; and &lt;code&gt;show bfd peers counters&lt;/code&gt; report a session whose
packet counts are read out of a BPF map.&lt;/p&gt;
&lt;p&gt;The same SCHED_FIFO stress against the FRR-driven session: &lt;strong&gt;0 flaps, p50 8.99,
p99 11.01, max 13.01ms&lt;/strong&gt;, uptime uninterrupted in FRR’s own CLI.&lt;/p&gt;
&lt;p&gt;Putting FRR in the control loop cost nothing.&lt;/p&gt;
&lt;h2 id=&quot;and-the-integration-immediately-found-a-bug&quot;&gt;And the integration immediately found a bug&lt;/h2&gt;
&lt;p&gt;bfdd’s UNIX transport for that dataplane socket, &lt;code&gt;--dplaneaddr unixc:&lt;/code&gt;, failed
every connect attempt with &lt;code&gt;EINVAL&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That turned into the first of six upstream bug reports, and I wrote it up
&lt;a href=&quot;https://w453y.me/writing/bfdd-unix-socket-einval/&quot;&gt;separately&lt;/a&gt;, because the reason it had
survived since the feature was introduced is more interesting than the one-line
fix.&lt;/p&gt;
&lt;p&gt;It set a pattern that held for the rest of the project: every milestone that
pushed the engine somewhere new found something in bfdd that only a real
dataplane at scale could reach.&lt;/p&gt;
&lt;h2 id=&quot;where-this-left-things&quot;&gt;Where this left things&lt;/h2&gt;
&lt;p&gt;At this point I wrote the results up and stopped, because the interesting
question, “can plain Linux get line-card behaviour”, had an answer. It is worth
being precise about how narrow that answer was.&lt;/p&gt;
&lt;p&gt;What existed was &lt;strong&gt;lab-grade, single-hop, IPv4-only&lt;/strong&gt;, with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No authentication, echo, or demand mode.&lt;/strong&gt; Three of RFC 5880’s features
simply absent.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No session continuity.&lt;/strong&gt; Sessions dropped and re-established across a bfdd
restart. The dataplane tore them down on control-plane disconnect and bfdd
re-added them on reconnect, which is correct but not hitless.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One session, ever.&lt;/strong&gt; The maps were sized for 64. Exactly one had been
tested.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validation gaps.&lt;/strong&gt; No spoofing defence worth the name, and no proof that
malformed traffic could not reach userspace.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An asynchronous-peer requirement&lt;/strong&gt;, structural to the RX-clocked design.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Numbers from VMs.&lt;/strong&gt; The stress was applied inside the guest and hit every
backend identically, so the comparisons are load-bearing, but the absolute
figures still await bare metal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A benchmark that only holds for one unauthenticated IPv4 session against a
cooperative peer is a demo, not an implementation. Everything from here on is
the unglamorous work of closing that list, and each item closed found something
new, which is why the series keeps going.&lt;/p&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;First the correctness debt: &lt;a href=&quot;https://w453y.me/writing/bfd-06-hardening-and-a-review-pass/&quot;&gt;session continuity, RFC 5880 validation, and an
external review whose two most annoying suggestions turned out to be real
bugs&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>bfd</category><category>xdp</category><category>frr</category><category>benchmarks</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 0: what all of this means</title><link>https://w453y.me/writing/bfd-00-a-primer/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-00-a-primer/</guid><description>A plain-language primer on the terms the rest of this series uses. BFD, FRR, XDP, eBPF, softirq, p99, and the rest. Read this first if any of those are new.</description><pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Who this is for.&lt;/strong&gt; If you already know what BFD and XDP are, skip straight
to &lt;a href=&quot;https://w453y.me/writing/bfd-01-measuring-the-folklore/&quot;&gt;part 1&lt;/a&gt;; nothing here will be new.
This part exists for everyone else: people who found the series through the
Linux or eBPF side and do not do routing, people who do routing and have never
written a BPF program, and anyone who wants to follow the argument without
first reading three RFCs. Nothing later in the series assumes you read this,
but everything later assumes you know these words.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-problem-in-one-paragraph&quot;&gt;The problem in one paragraph&lt;/h2&gt;
&lt;p&gt;Two routers are connected. One of them dies, or the link between them breaks.
How fast does the other one find out? If the answer is “thirty seconds”, every
packet sent during those thirty seconds is dropped into a hole. The job of the
protocol this series is about is to make that answer “thirty milliseconds”
instead, and the whole project is about what it takes to keep that promise on an
ordinary Linux box that is busy doing other things.&lt;/p&gt;
&lt;h2 id=&quot;the-routing-side&quot;&gt;The routing side&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;BFD (Bidirectional Forwarding Detection).&lt;/strong&gt; A deliberately stupid protocol
whose only job is to notice that a neighbour has stopped answering. Two systems
send each other small UDP packets at a fixed interval, and if enough consecutive
packets go missing, the link is declared down. It is defined in RFC 5880, with
RFC 5881 covering the single-hop case and RFC 5883 the multihop one.&lt;/p&gt;
&lt;p&gt;It is stupid on purpose. Because it does one thing, it can do it far faster than
the routing protocols themselves, which is why they lean on it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Detect time, and “3x10ms”.&lt;/strong&gt; A session is configured with a transmit interval
and a multiplier. &lt;code&gt;3x10ms&lt;/code&gt; means “send every 10 milliseconds, declare the link
dead after 3 missed”, so a 30ms detect time. Aggressive timers mean fast
detection and less tolerance for lateness. That trade is the entire subject of
this series.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flap.&lt;/strong&gt; A session going down and then back up. Usually the link was fine and
the packets were merely late, which makes a flap worse than useless: it tears
down working routes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Session.&lt;/strong&gt; One BFD conversation between two endpoints. A router with many
neighbours runs many sessions at once, which is why the later parts care about
what happens at 64 of them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FRR (FRRouting).&lt;/strong&gt; The open-source routing suite most Linux routers run. It
implements BGP, OSPF, IS-IS, and the rest as separate daemons. &lt;strong&gt;bfdd&lt;/strong&gt; is its
BFD daemon, and it is both the thing measured in part 1 and the source of the
&lt;a href=&quot;https://w453y.me/writing/series/frr-bfdd-bugs/&quot;&gt;six upstream bugs&lt;/a&gt; this project ended up
reporting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Distributed BFD, and the “dataplane”.&lt;/strong&gt; FRR can hand the actual packet
handling to an external program over a small protocol called bfddp, keeping only
session setup and reporting for itself. It exists so hardware vendors can attach
their own forwarding hardware. This project attaches to it as a &lt;em&gt;software&lt;/em&gt;
dataplane, which is why FRR bugs kept turning up: that code path had rarely been
driven hard by anyone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Echo mode.&lt;/strong&gt; A BFD feature where you send a packet addressed to yourself, the
neighbour’s forwarding hardware loops it straight back without its software ever
seeing it, and you time the round trip. It tests the neighbour’s data path
rather than its software. Covered in
&lt;a href=&quot;https://w453y.me/writing/bfd-09-echo-mode/&quot;&gt;part 9&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GTSM, TTL 255.&lt;/strong&gt; Every IP packet carries a hop counter that decreases each
time a router forwards it. If you insist that arriving packets still read 255,
you know nobody forwarded them, so the sender must be directly attached. It is a
cheap way to reject spoofed traffic from far away.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discriminator.&lt;/strong&gt; A number each side picks to identify its end of a session, so
packets can be matched to the right conversation.&lt;/p&gt;
&lt;h2 id=&quot;the-linux-side&quot;&gt;The Linux side&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Kernel space and user space.&lt;/strong&gt; The kernel is the part of the operating system
that owns the hardware. Ordinary programs, including routing daemons, run in
user space and must be scheduled onto a CPU before they can do anything. That
“must be scheduled” is where the trouble starts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The scheduler, and starvation.&lt;/strong&gt; When more work wants CPU than there is CPU,
the kernel scheduler decides who runs. A program that is not chosen does not run
at all, and a BFD daemon that is not running is not sending packets. Being
starved for 300ms is enough to kill a 30ms session.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;SCHED_FIFO, and RT throttling.&lt;/strong&gt; Linux has real-time priorities. A
&lt;code&gt;SCHED_FIFO&lt;/code&gt; task outranks all ordinary tasks and runs until it yields. To stop
a runaway real-time task locking the machine, the kernel reserves a small slice,
roughly 50ms per second, for everyone else. If real-time work saturates every
core, an ordinary program gets that one slice and nothing more. This is the
condition no user-space design in &lt;a href=&quot;https://w453y.me/writing/bfd-03-the-tx-bakeoff/&quot;&gt;part 3&lt;/a&gt;
survives.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Softirq.&lt;/strong&gt; Work the kernel defers slightly, then runs at higher priority than
any ordinary program, including most packet processing. Code that runs in
softirq context keeps running when user-space programs are starved, which is
exactly the property this project needs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;eBPF.&lt;/strong&gt; A way to load small, verified programs into the running kernel. The
kernel checks them for safety first, so they cannot crash it or loop forever.
They are how you add behaviour to the kernel without patching it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;XDP (eXpress Data Path).&lt;/strong&gt; An eBPF hook at the earliest possible point of
packet receive, before the kernel has built any of its normal packet structures.
An XDP program looks at a packet and returns a verdict.&lt;/p&gt;
&lt;p&gt;If you have never seen one, the shape is the whole idea. This is not from the
project, it is the smallest thing that shows what a verdict means:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;SEC&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;xdp&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; hello&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;struct&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; xdp_md &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;ctx&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        void&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;data     &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;long&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)ctx-&gt;data;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        void&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;data_end &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)(&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;long&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)ctx-&gt;data_end;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        struct&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; ethhdr &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;eth &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; data;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;        /* The verifier requires proving every read is in bounds. */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; ((&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;)(eth &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; data_end)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_PASS;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (eth-&gt;h_proto &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; bpf_htons&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(ETH_P_IPV6))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_DROP;&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;   /* discard, never reaches the stack */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; XDP_PASS;&lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;           /* carry on to the normal kernel path */&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are three verdicts that matter here: &lt;code&gt;XDP_PASS&lt;/code&gt; to continue to the normal
stack, &lt;code&gt;XDP_DROP&lt;/code&gt; to discard the packet outright, and &lt;code&gt;XDP_TX&lt;/code&gt; to send it
straight back out the interface it arrived on.&lt;/p&gt;
&lt;p&gt;Note the bounds check. Every read from a packet has to be provably in-bounds
before the kernel will load the program, which is what makes it safe to run
untrusted code in the receive path, and also what makes writing it feel unlike
ordinary C.&lt;/p&gt;
&lt;p&gt;The crucial limitation, and the hinge of this whole project: &lt;strong&gt;XDP can only
react to a packet that has arrived. It cannot create one.&lt;/strong&gt; That is why
&lt;a href=&quot;https://w453y.me/writing/bfd-04-rx-clocked-tx/&quot;&gt;part 4&lt;/a&gt; stops trying to transmit and starts
bouncing the peer’s own packets back instead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BPF maps.&lt;/strong&gt; Shared tables that BPF programs and ordinary programs can both
read and write. They are how the kernel half and the user-space half of this
engine exchange configuration and state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;bpf_timer.&lt;/strong&gt; A timer that fires inside the kernel and runs a BPF function.
Used here to notice sessions that have gone quiet, since silence delivers no
packet and therefore triggers no XDP program.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Offload.&lt;/strong&gt; Moving work off the general-purpose CPU onto something that is
always available. Hardware routers offload BFD to dedicated line cards. This
project offloads it to the kernel’s receive path, which is the nearest thing an
ordinary Linux box has to a line card.&lt;/p&gt;
&lt;h2 id=&quot;the-measurement-side&quot;&gt;The measurement side&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;p50, p99, and max.&lt;/strong&gt; If you sort every measurement, p50 is the middle value
and p99 is the value 99% fall below. They describe the typical case and the
common bad case. &lt;strong&gt;Max&lt;/strong&gt; is the single worst.&lt;/p&gt;
&lt;p&gt;This distinction is not pedantry, it is &lt;a href=&quot;https://w453y.me/writing/bfd-01-measuring-the-folklore/&quot;&gt;the finding in part
1&lt;/a&gt;: a BFD daemon flapping badly enough
to tear down routes had a completely healthy-looking p99, because the events
that killed it were rare. Anything that summarises away the maximum hides this
class of failure entirely.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inter-packet gap.&lt;/strong&gt; The time between one packet leaving and the next. For BFD
this is the number that matters, because a long gap is what makes the far end
give up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pcap, and tcpdump.&lt;/strong&gt; A packet capture is a recording of what actually crossed
the wire. &lt;code&gt;tcpdump&lt;/code&gt; makes one. Every number in this series comes from a capture
taken &lt;strong&gt;outside&lt;/strong&gt; the machine under test, on the virtualisation host, because
&lt;a href=&quot;https://w453y.me/writing/bfd-02-the-folklore-is-partly-wrong/&quot;&gt;a starved program’s own logs
lie&lt;/a&gt; about what it sent and when.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;stress-ng.&lt;/strong&gt; A tool for deliberately loading a machine in specific ways: CPU,
context switching, timers. The “stress ladder” in this series is four escalating
levels of it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Topotest.&lt;/strong&gt; FRR’s integration test framework. It builds a small virtual
network of routers and asserts things about their behaviour. Getting one written
for a dataplane bug turned out to be &lt;a href=&quot;https://w453y.me/writing/bfdd-counters-teardown-zero-length-read/&quot;&gt;most of the
work&lt;/a&gt; in one of the upstream
fixes.&lt;/p&gt;
&lt;h2 id=&quot;ready&quot;&gt;Ready&lt;/h2&gt;
&lt;p&gt;That is the vocabulary. &lt;a href=&quot;https://w453y.me/writing/bfd-01-measuring-the-folklore/&quot;&gt;Part 1&lt;/a&gt; starts
by measuring whether the received wisdom about software BFD is actually true.&lt;/p&gt;</content:encoded><category>bfd</category><category>xdp</category><category>ebpf</category><category>glossary</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Line-card BFD for plain Linux, part 1: measuring the folklore</title><link>https://w453y.me/writing/bfd-01-measuring-the-folklore/</link><guid isPermaLink="true">https://w453y.me/writing/bfd-01-measuring-the-folklore/</guid><description>Everyone says software BFD cannot hold aggressive timers under load. Before building anything, I measured it. The number that should worry you is not the 970ms gap, it is the p99 sitting at 10.16ms in the same run.</description><pubDate>Mon, 06 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;BFD is the failure detector under BGP, OSPF, and IS-IS. Two routers exchange
small UDP packets at a negotiated interval, and if one side misses enough of
them, typically 3 packets in 30ms at aggressive timers, it declares the link dead
and the routing protocol withdraws routes.&lt;/p&gt;
&lt;p&gt;The entire value of the protocol is timing. An implementation that sends late is
worse than no BFD at all, because a false timeout tears down routes for a link
that is actually fine. The failure detector becomes the failure.&lt;/p&gt;
&lt;h2 id=&quot;the-folklore&quot;&gt;The folklore&lt;/h2&gt;
&lt;p&gt;Operational folklore says software BFD cannot be trusted with aggressive timers
under CPU load. It is why people configure 3x300ms instead of 3x10ms, why
SONiC’s software BFD documentation caps timers at 300ms, and why hardware
routers offload BFD to line cards. Juniper goes as far as dedicating an offload
CPU with DPDK flow filters on the vSRX, which is a vendor telling you the
forwarding plane cannot be trusted to deliver BFD packets to a busy control
plane on time.&lt;/p&gt;
&lt;p&gt;I wanted three things: to quantify the actual failure modes instead of repeating
the folklore, to understand &lt;em&gt;why&lt;/em&gt; userspace BFD fails when it fails, and to see
whether plain Linux on a commodity NIC can get line-card behaviour using XDP.&lt;/p&gt;
&lt;p&gt;This is a lab project, a few weeks of evenings on a Proxmox testbed. Every claim
in it is backed by a packet capture in
&lt;a href=&quot;https://github.com/w453y/xdp-bfd&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;the repo&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-testbed-and-one-decision-that-mattered&quot;&gt;The testbed, and one decision that mattered&lt;/h2&gt;
&lt;p&gt;Three Ubuntu 26.04 VMs (kernel 7.0, virtio-net with multiqueue) on an isolated
Proxmox bridge: a device under test, an FRR peer, and a load generator that
ended up mostly unused, because the interesting load is on the DUT itself.&lt;/p&gt;
&lt;p&gt;The measurement instrument is &lt;strong&gt;tcpdump on the hypervisor bridge&lt;/strong&gt;, not inside
the guest. The host sees actual wire times and does not care what the guest’s
processes believe.&lt;/p&gt;
&lt;p&gt;That choice turned out to matter more than expected, and &lt;a href=&quot;https://w453y.me/writing/bfd-02-the-folklore-is-partly-wrong/&quot;&gt;part
2&lt;/a&gt; has a concrete example of
process logs and the wire disagreeing completely.&lt;/p&gt;
&lt;h2 id=&quot;the-baseline&quot;&gt;The baseline&lt;/h2&gt;
&lt;p&gt;FRR 10.5.1’s bfdd, a single-hop session at 3x10ms (30ms detect time), and an
escalating stress-ng ladder on the DUT. Gaps are DUT-to-peer inter-packet times
from the host capture. Nominal spacing with RFC jitter is 7.5 to 10ms.&lt;/p&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;Table, scrollable&quot;&gt;












































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Level&lt;/th&gt;&lt;th&gt;Load&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;p50&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;p99&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;max gap&lt;/th&gt;&lt;th&gt;session&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;L1&lt;/td&gt;&lt;td&gt;4x CPU hogs (fair sched)&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.81&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.03&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;~10ms&lt;/td&gt;&lt;td&gt;survived&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;L2&lt;/td&gt;&lt;td&gt;8x CPU + context-switch churn&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.73&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.03&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;750ms&lt;/td&gt;&lt;td&gt;flapped&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;L3&lt;/td&gt;&lt;td&gt;CPU + timer/timerfd/hrtimer pressure&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.82&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;10.16&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;970ms&lt;/td&gt;&lt;td&gt;44 flaps in 120s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;L4&lt;/td&gt;&lt;td&gt;4x SCHED_FIFO prio-50 hogs&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;8.82&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;287&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;960ms&lt;/td&gt;&lt;td&gt;~40% of packets never sent&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h2 id=&quot;the-number-that-should-worry-you&quot;&gt;The number that should worry you&lt;/h2&gt;
&lt;p&gt;It is not the 970ms.&lt;/p&gt;
&lt;p&gt;It is the &lt;strong&gt;p99 sitting at 10.16ms in the same run&lt;/strong&gt;. Nominal. Perfect.&lt;/p&gt;
&lt;p&gt;The starvation events that kill sessions are rare, a handful per hundred
seconds, so they are invisible to percentile monitoring. A dashboard graphing
p99 packet spacing on that box would have shown a completely healthy BFD daemon
while it flapped 44 times in two minutes.&lt;/p&gt;
&lt;p&gt;If your alerting is percentile-based, you do not find out about this class of
failure from your monitoring. You find out from the routing protocol, after the
withdrawal.&lt;/p&gt;
&lt;p&gt;That pattern, p99 fine and max fatal, is invisible in any summary statistic that
is not a maximum, and it shows up repeatedly in the rest of this series.&lt;/p&gt;
&lt;h2 id=&quot;next&quot;&gt;Next&lt;/h2&gt;
&lt;p&gt;Having measured the folklore, the obvious next step was to build the XDP path
and prove the point. Instead I wrote the dumbest possible userspace BFD daemon
first, expecting it to die the same way, and it
&lt;a href=&quot;https://w453y.me/writing/bfd-02-the-folklore-is-partly-wrong/&quot;&gt;refused to&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>bfd</category><category>xdp</category><category>linux</category><category>measurement</category><category>Line-card BFD for plain Linux</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>Teaching an NGINX directive to take a variable, so my VPN could skip the login</title><link>https://w453y.me/writing/nginx-auth-jwt-runtime-variable/</link><guid isPermaLink="true">https://w453y.me/writing/nginx-auth-jwt-runtime-variable/</guid><description>I wanted JWT auth on everything except traffic from my VPN subnet. nginx makes that trivial with geo, except the directive I needed it on only accepted a literal on or off. Making it accept a variable changed a default, and that turned auth on for everyone.</description><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I run about thirty self-hosted services behind a single NGINX reverse proxy, all
of them sitting behind &lt;a href=&quot;https://github.com/w453y/ngx-http-auth-jwt-gateway&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;a JWT auth
gateway&lt;/a&gt; backed by Google
OAuth. That is the right default for anything reachable from the internet.&lt;/p&gt;
&lt;p&gt;It is a slightly irritating default when I am already on my own WireGuard
tunnel, on my own network, and I just want to look at a dashboard. I wanted one
rule: &lt;strong&gt;authenticate everything, except traffic arriving from the VPN subnet.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;nginx is very good at this sort of thing. The &lt;code&gt;geo&lt;/code&gt; module exists precisely to
turn a client address into a variable:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;nginx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;geo &lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;$jwt_enabled {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;    default&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;        on&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    10.10.10.0/&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;20&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  off&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;   &lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# VPN subnet&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then hand &lt;code&gt;$jwt_enabled&lt;/code&gt; to the directive that turns authentication on and off,
and you are done in four lines.&lt;/p&gt;
&lt;p&gt;Except you are not, because the directive would not take it.&lt;/p&gt;
&lt;h2 id=&quot;why-a-directive-cannot-always-take-a-variable&quot;&gt;Why a directive cannot always take a variable&lt;/h2&gt;
&lt;p&gt;This is the part I had not internalised before, and it is worth spelling out,
because from the config file everything looks like a variable.&lt;/p&gt;
&lt;p&gt;An nginx module declares each directive with a parse-time handler and a set of
flags. The one here looked like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;ngx_string&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;auth_jwt_enabled&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_HTTP_MAIN_CONF &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_HTTP_SRV_CONF &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_HTTP_LOC_CONF &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_CONF_FLAG,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; ngx_conf_set_flag_slot,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_HTTP_LOC_CONF_OFFSET,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; offsetof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;auth_jwt_conf_t&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, enabled),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; NULL&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;},&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NGX_CONF_FLAG&lt;/code&gt; with &lt;code&gt;ngx_conf_set_flag_slot&lt;/code&gt; means: at &lt;strong&gt;config load time&lt;/strong&gt;,
read the token, expect exactly the string &lt;code&gt;on&lt;/code&gt; or &lt;code&gt;off&lt;/code&gt;, and write a &lt;code&gt;1&lt;/code&gt; or &lt;code&gt;0&lt;/code&gt;
into an &lt;code&gt;ngx_flag_t&lt;/code&gt; field. By the time a request arrives there is no string
left, no expression, nothing to evaluate. Just an integer that was decided when
nginx started.&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;auth_jwt_enabled $jwt_enabled;&lt;/code&gt; does not fail at runtime. It fails at
startup, with a complaint about an invalid value, because &lt;code&gt;$jwt_enabled&lt;/code&gt; is
neither &lt;code&gt;on&lt;/code&gt; nor &lt;code&gt;off&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A variable is only meaningful to a directive that was written to defer.&lt;/p&gt;
&lt;h2 id=&quot;the-change&quot;&gt;The change&lt;/h2&gt;
&lt;p&gt;nginx already provides the machinery: complex values. A directive declared with
&lt;code&gt;ngx_http_set_complex_value_slot&lt;/code&gt; stores a compiled expression, and the module
evaluates it per request with &lt;code&gt;ngx_http_complex_value()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Three coordinated edits. The directive:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;ngx_string&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;auth_jwt_enabled&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_HTTP_MAIN_CONF &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_HTTP_SRV_CONF &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_HTTP_LOC_CONF &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;|&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_CONF_TAKE1,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; ngx_http_set_complex_value_slot,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_HTTP_LOC_CONF_OFFSET,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt; offsetof&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;auth_jwt_conf_t&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, enabled),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; NULL&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;},&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The field, from a flag to a pointer:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  ngx_flag_t&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; enabled;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  ngx_http_complex_value_t&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;enabled;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the decision, moved from startup to request time:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;ngx_int_t&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; enabled &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (jwtcf&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;enabled &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;!=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; NULL&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  ngx_str_t&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cv;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;ngx_http_complex_value&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(r, jwtcf-&gt;enabled, &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;cv) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_OK &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; cv.len &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#D2A8FF&quot;&gt;ngx_strncmp&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;(cv.data, &lt;/span&gt;&lt;span style=&quot;color:#A5D6FF&quot;&gt;&quot;off&quot;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;      enabled &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;NGX_CONF_FLAG&lt;/code&gt; became &lt;code&gt;NGX_CONF_TAKE1&lt;/code&gt;, because the directive now takes one
arbitrary argument rather than a boolean the parser recognises. Static configs
keep working unchanged: &lt;code&gt;auth_jwt_enabled on;&lt;/code&gt; is now a complex value that
happens to be constant, which evaluates to the string &lt;code&gt;on&lt;/code&gt; on every request.&lt;/p&gt;
&lt;p&gt;Note which way the comparison runs. Only the literal &lt;code&gt;off&lt;/code&gt; disables
authentication. An empty value, a typo, a &lt;code&gt;geo&lt;/code&gt; block that fell through to
nothing: all of those leave &lt;code&gt;enabled&lt;/code&gt; at 1 and the request gets authenticated.
For a security directive that is the correct direction to fail, and it was worth
being deliberate about rather than writing &lt;code&gt;!= &quot;on&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;the-default-flipped-and-that-broke-everything&quot;&gt;The default flipped, and that broke everything&lt;/h2&gt;
&lt;p&gt;Here is the part I did not see coming.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ngx_flag_t&lt;/code&gt; fields use nginx’s &lt;code&gt;NGX_CONF_UNSET&lt;/code&gt; sentinel, and the module’s
merge function resolved unset to 0:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;conf&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;enabled &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; prev&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;enabled &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_CONF_UNSET &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;?&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; :&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; prev&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;enabled;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Unconfigured meant off. That is what made the module inert in a config that
never mentioned it.&lt;/p&gt;
&lt;p&gt;A pointer cannot use that sentinel. Unset is now &lt;code&gt;NULL&lt;/code&gt;, inheritance is a plain
pointer copy, and my runtime code starts from &lt;code&gt;ngx_int_t enabled = 1&lt;/code&gt; and only
ever clears it on an explicit &lt;code&gt;off&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Read those two together: &lt;strong&gt;&lt;code&gt;NULL&lt;/code&gt; no longer means off, it means “nothing said
otherwise”, and the code says the default is on.&lt;/strong&gt; So after compiling my first
version, JWT authentication was applied globally, on an ordinary nginx config
with no JWT directives anywhere in it.&lt;/p&gt;
&lt;p&gt;That is about as loud as a regression gets. Every site on the box demanding a
token nobody had configured.&lt;/p&gt;
&lt;p&gt;My fix was a guard at the top of the handler: if no key and no keyfile are
configured, this module has nothing to validate against, so decline.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;c&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;// Only activate JWT logic if key or keyfile_path is set&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (jwtcf&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;key.len &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; jwtcf&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;keyfile_path.len &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; NGX_DECLINED;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using “is a key configured” as the proxy for “is this module in use” is not
elegant, but it is true: a JWT module with no key cannot do its job under any
configuration.&lt;/p&gt;
&lt;h2 id=&quot;the-review&quot;&gt;The review&lt;/h2&gt;
&lt;p&gt;I opened the PR in mid-August and, after a couple of weeks, sent a polite nudge
asking for even a quick reaction so I could refine the approach. The maintainer
replied the same day, called it a good idea, and came back with formatting
cleanup and tests.&lt;/p&gt;
&lt;p&gt;He also removed my guard, saying he thought it was redundant because similar
checks are handled further down, and asked me to add it back in the right place
if it really mattered.&lt;/p&gt;
&lt;p&gt;That is exactly the sort of comment worth taking seriously rather than
defending, because it is checkable. I rebuilt with his commits and ran a plain
config with no JWT directives against it. The global-auth problem did not
reappear. His restructuring had moved the early-exit somewhere that covered the
case my guard had been papering over, so the guard genuinely was redundant
against the new shape of the code. I said so and agreed to drop it.&lt;/p&gt;
&lt;p&gt;The tests he added are the neat part, because testing an IP-based feature in CI
is awkward. Instead of a subnet, drive the variable from a request header:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;nginx&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;map&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; $&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt;http_test_auth_enabled&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; $jwt_enabled {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;    default&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;  on&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;    on&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;       on&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;    off&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;      off&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;location&lt;/span&gt;&lt;span style=&quot;color:#FFA657&quot;&gt; /enabled/variable &lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    auth_jwt_enabled&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; $jwt_enabled;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then two curl cases: send &lt;code&gt;Test-Auth-Enabled: on&lt;/code&gt; and expect 401, send &lt;code&gt;off&lt;/code&gt; and
expect 200. That tests the mechanism that actually changed, which is “can this
directive read a runtime variable”, without needing a second network.&lt;/p&gt;
&lt;p&gt;Merged and &lt;a href=&quot;https://github.com/TeslaGov/ngx-http-auth-jwt-module/releases/tag/2.4.0&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;released as
v2.4.0&lt;/a&gt;,
about a month after I opened it.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-from-it&quot;&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;The config file lies to you about what is a variable. &lt;code&gt;$foo&lt;/code&gt; looks uniform
everywhere it appears, but whether a directive can see it is decided in C, at
module-definition time, by which parse handler the author picked. There is no
way to tell from the outside except by trying it and reading the error.&lt;/p&gt;
&lt;p&gt;The more useful lesson is about sentinels. Changing a field’s type quietly
changed what “unconfigured” meant, and the default went from off to on in a
security module. Nothing in the diff said “default changed”; it fell out of
&lt;code&gt;ngx_flag_t&lt;/code&gt; having a sentinel and a pointer not having one. When you change a
representation, the values that were previously impossible are the ones to go
looking at.&lt;/p&gt;
&lt;p&gt;And the feature does what I wanted. On the VPN, dashboards open. Off it, Google
OAuth. Four lines of &lt;code&gt;geo&lt;/code&gt; and one directive that finally accepts what the rest
of nginx has been handing it all along.&lt;/p&gt;</content:encoded><category>nginx</category><category>authentication</category><category>jwt</category><category>c</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item><item><title>The disk was fine, the page was not: a two minute smartctl and a GUI that gave up</title><link>https://w453y.me/writing/proxmox-smart-usb-disk-hang/</link><guid isPermaLink="true">https://w453y.me/writing/proxmox-smart-usb-disk-hang/</guid><description>Proxmox&apos;s Disks page hung with a communication failure while the drive worked perfectly at the command line. The cause was a SMART query that took two minutes to return, and a default timeout sized for spinning up a JBOD.</description><pubDate>Tue, 15 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I plugged a 2.5 inch SSD into a Proxmox box through a USB-to-SATA adapter, went
to &lt;strong&gt;Node → Disks&lt;/strong&gt; in the web UI, and got a spinner followed by
&lt;code&gt;Communication Failure&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The disk itself was completely fine. &lt;code&gt;lsblk&lt;/code&gt; listed it, &lt;code&gt;fdisk&lt;/code&gt; read it, I
mounted it and copied files to it. Every tool at the command line agreed the
drive was healthy and present. Only the GUI disagreed, and it disagreed by
hanging rather than by saying anything useful. &lt;code&gt;journalctl&lt;/code&gt;, syslog and the
pvedaemon logs had nothing.&lt;/p&gt;
&lt;p&gt;That gap, everything works except the one thing that is supposed to show you
that everything works, is what made it worth chasing.&lt;/p&gt;
&lt;h2 id=&quot;reading-someone-elses-perl&quot;&gt;Reading someone else’s Perl&lt;/h2&gt;
&lt;p&gt;Proxmox’s API is Perl, which is convenient here because it means the thing that
hung is sitting on disk in readable form. Following the call path:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PVE/API2/Disks.pm&lt;/code&gt; handles the disk listing endpoint&lt;/li&gt;
&lt;li&gt;which calls into &lt;code&gt;PVE/Diskmanage.pm&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;where &lt;code&gt;get_smart_data()&lt;/code&gt; shells out to &lt;code&gt;smartctl&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the call site looked like this:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;perl&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (!$nosmart) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;    eval&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        my&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; $smartdata = get_smart_data($devpath, !is_ssdlike($type));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        $health = $smartdata&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;health&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;} &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; $smartdata&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;-&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;health&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (is_ssdlike($type)) { &lt;/span&gt;&lt;span style=&quot;color:#8B949E&quot;&gt;# if we have an SSD, try to get the wearout indicator&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;            my&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; $wear_level = get_wear_leveling_info($smartdata);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;            $wearout = $wear_level &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt; defined&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;($wear_level);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;eval&lt;/code&gt; catches errors. It does not catch slowness. And &lt;code&gt;get_smart_data()&lt;/code&gt;
invoked &lt;code&gt;smartctl&lt;/code&gt; through &lt;code&gt;run_command&lt;/code&gt; with no timeout at all, so if that
command decided to take its time, the API request behind it simply waited.&lt;/p&gt;
&lt;p&gt;The cheapest possible confirmation was to delete the problem:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;perl&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; (!$nosmart) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then &lt;code&gt;systemctl restart pvedaemon&lt;/code&gt;, reload the page, and the Disks view came up
instantly. Not a fix, obviously, but it turned a theory into a fact in about
thirty seconds.&lt;/p&gt;
&lt;h2 id=&quot;the-question-that-reframed-it&quot;&gt;The question that reframed it&lt;/h2&gt;
&lt;p&gt;I filed &lt;a href=&quot;https://bugzilla.proxmox.com/show_bug.cgi?id=6224&quot; target=&quot;_blank&quot; rel=&quot;noopener noreferrer&quot;&gt;bug #6224&lt;/a&gt; with that
analysis, and the reply from Fabian Grünbichler was two short questions asked
one after the other.&lt;/p&gt;
&lt;p&gt;First: if you run the &lt;code&gt;smartctl&lt;/code&gt; command manually, does it ever return?&lt;/p&gt;
&lt;p&gt;It does:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# smartctl -a /dev/sdb&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;smartctl 7.3 2022-02-28 r5338 [x86_64-linux-6.8.12-6-pve] (local build)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Read NVMe Identify Controller failed: scsi error unsupported field in scsi command&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then: how long does that take? Prefix it with &lt;code&gt;time&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;real    2m0.758s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;user    0m0.013s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sys     0m0.000s&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Two minutes.&lt;/strong&gt; Not a hang. A return.&lt;/p&gt;
&lt;p&gt;I had been describing this as something hanging indefinitely, and the whole time
it was finishing, just far too late for anything to care. That difference
matters enormously for the fix. A genuine deadlock needs to be found and broken.
A slow return needs a deadline. They are not the same bug and they do not have
the same remedy, and I would have gone on describing it wrongly if I had not
been asked to put a number on it.&lt;/p&gt;
&lt;p&gt;Two questions, no speculation, both answerable in a terminal. It is a good model
for triage.&lt;/p&gt;
&lt;h2 id=&quot;the-root-cause-is-a-reasonable-default-meeting-an-unreasonable-device&quot;&gt;The root cause is a reasonable default meeting an unreasonable device&lt;/h2&gt;
&lt;p&gt;Daniel Kral found the rest of it, and it is more interesting than a plain bug.&lt;/p&gt;
&lt;p&gt;smartmontools uses a &lt;strong&gt;60 second default timeout&lt;/strong&gt; for SCSI commands. That is not
carelessness: it is sized so that a disk in a large JBOD has time to spin up
before the command gives up. On the hardware that default was written for, it is
the right number.&lt;/p&gt;
&lt;p&gt;Devices behind the USB Attached SCSI driver are the ones most likely to trip it,
because a USB-to-SATA bridge translating SCSI commands can fail in ways that
consume the full timeout instead of erroring promptly. My 2m0.758s is that 60
second budget being spent twice, plus change.&lt;/p&gt;
&lt;p&gt;So nothing here is broken in isolation. smartmontools waits as long as its
slowest supported hardware needs. The adapter fails slowly rather than quickly.
Proxmox calls the command without a deadline because it has no reason to expect
one is needed. Put all three together and a web page stops loading.&lt;/p&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The fix&lt;/h2&gt;
&lt;p&gt;A three-patch series. The one that matters is a single word:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark-default&quot; style=&quot;background-color:#0d1117;color:#e6edf3; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;perl&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; my&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; $returncode = &lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt;eval&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;-	run_command($cmd, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;noerr&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; 1, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;outfunc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; sub&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt;+	run_command($cmd, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;noerr&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; 1, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;timeout&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; 10, &lt;/span&gt;&lt;span style=&quot;color:#79C0FF&quot;&gt;outfunc&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#FF7B72&quot;&gt; sub&lt;/span&gt;&lt;span style=&quot;color:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ten seconds instead of sixty, on the reasoning that a healthy drive answers
almost immediately and anything slower is not worth blocking a UI over. The
other two patches make the failure land properly once it can happen: one
separates the error path so &lt;code&gt;get_smart_data()&lt;/code&gt; dies with the real message, and
one masks that error into an unknown health value, because the web UI expects a
health field and was being handed an error string instead.&lt;/p&gt;
&lt;p&gt;That last one is the tell that a timeout was never expected here. The moment the
call &lt;em&gt;could&lt;/em&gt; fail, the error handling around it turned out to be wrong too.&lt;/p&gt;
&lt;p&gt;The patch is in &lt;code&gt;pve-storage&lt;/code&gt; today. Amusingly, the bug is still marked
&lt;strong&gt;PATCH AVAILABLE&lt;/strong&gt; rather than resolved, so the tracker is more pessimistic
than the source tree.&lt;/p&gt;
&lt;h2 id=&quot;what-i-took-from-it&quot;&gt;What I took from it&lt;/h2&gt;
&lt;p&gt;The debugging lesson is the timing question. “It hangs” is a description of your
patience, not of the program. &lt;code&gt;time&lt;/code&gt; turns it into a measurement, and the
measurement pointed straight at a 60 second constant that a human had chosen
deliberately for a reason that was sound.&lt;/p&gt;
&lt;p&gt;The engineering lesson is that any call into an external command is a call into
someone else’s timeout policy, whether or not you have thought about it. Proxmox
did not choose 60 seconds. It inherited it from smartmontools, which inherited
its requirements from JBOD spin-up times, and the UI inherited the consequences
from both.&lt;/p&gt;
&lt;p&gt;I also did not write the fix, and that is fine. Filing something with the call
path traced, the offending call identified, a workaround that isolates it, and
then answering the maintainer’s questions with numbers, is a complete
contribution. The people who own the code are much better placed to decide
whether the right answer is ten seconds or thirty.&lt;/p&gt;</content:encoded><category>proxmox</category><category>linux</category><category>debugging</category><category>storage</category><author>awasey8905@gmail.com (Abdul Wasey)</author></item></channel></rss>