<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en" xml:base="https://vincent.bernat.ch">
  <title>Vincent Bernat — Network</title>
  <link href="https://vincent.bernat.ch/en/blog/atom-network.xml" rel="self"/>
  <link href="https://vincent.bernat.ch/en/blog#tag-network" rel="alternate"/>
  <id>http://www.luffy.cx/en/blog/atom-network.xml/</id>
  <updated>2026-10-09T05:45:43Z</updated>

  <entry>
    <title type="html">An interactive tour of the spanning tree protocol</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-spanning-tree" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-spanning-tree#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-08-24T15:00:00Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-spanning-tree.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<div class="admonition when-rss">
<p class="admonition-title">Warning</p>
<p>This post contains interactive examples. To visualize and interact
with them, you need to <a href="/en/blog/2026-spanning-tree" title="An interactive tour of the spanning tree protocol">leave your RSS reader</a>.</p>
</div>

<p>Imagine you rent office space for a three-day event. You quickly set up a few
Ethernet switches and tape some cables on the floor to get everyone online.
Unfortunately, Stan, your clumsiest coworker, kicks out a cable every time he
gets up for coffee. You could add extra cables, but then you’d get a broadcast
storm: Ethernet packets that loop and multiply until nothing else gets through.</p>
<p>That’s where the <em>spanning tree protocol</em> (<abbr title="Spanning Tree Protocol">STP</abbr>) comes in. <abbr title="Spanning Tree Protocol">STP</abbr> blocks just enough
of your spare cables to leave a loop-free tree. When Stan strikes again, it
rebuilds the tree in a second, leaving some time for Blobby, your one-person
support crew, to reconnect the cable.<sup id="fnref:sprites"><a class="footnote-ref" href="#fn:sprites">1</a></sup> See for yourself: the diagram
below runs a real <abbr title="Spanning Tree Protocol">STP</abbr> implementation in your browser!</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:demo

A1 @0,0 prio=4096
A2 @0,1
A3 @0,2
A4 @0,3

B1 @1,0 prio=8192
B2 @1,1
B3 @1,2
B4 @1,3

C1 @2,0 prio=8192
C2 @2,1
C3 @2,2
C4 @2,3

A1 -- A2 hazard=0
A2 -- A3 hazard=0
A3 -- A4 hazard=0
B1 -- B2
B2 -- B3
B3 -- B4
C1 -- C2 hazard=0
C2 -- C3 hazard=0
C3 -- C4 hazard=0

A1 -- B1 cost=10
B1 -- C1 cost=10
A4 -- B4 cost=20
B4 -- C4 cost=20

Leo @-0.3,0.7 proto=none icon=👦🏻
Mia @-0.3,1.3 proto=none icon=👧🏽
Joy @0.3,0.7  proto=none icon=👱🏻‍♀️
Roy @0.3,1.3  proto=none icon=👨🏾
A2 -- Leo hazard=0 A2:edge
A2 -- Mia hazard=0 A2:edge
A2 -- Joy hazard=0 A2:edge
A2 -- Roy hazard=0 A2:edge

Max @-0.3,1.7 proto=none icon=👨🏽
Zoe @-0.3,2.3 proto=none icon=👩🏾
Ada @0.3,1.7  proto=none icon=👵🏾
Amy @0.3,2.3  proto=none icon=👩🏼
A3 -- Max hazard=0 A3:edge
A3 -- Zoe hazard=0 A3:edge
A3 -- Ada hazard=0 A3:edge
A3 -- Amy hazard=0 A3:edge

Eli @0.7,0.7 proto=none icon=👦🏼
Jay @0.7,1.3 proto=none icon=👨🏻
Kai @1.3,0.7  proto=none icon=🧑🏽
Ben @1.3,1.3  proto=none icon=👱🏼
B2 -- Eli hazard=0.2 B2:edge
B2 -- Jay hazard=0.2 B2:edge
B2 -- Kai hazard=0.2 B2:edge
B2 -- Ben hazard=0.2 B2:edge

Ava @0.7,1.7 proto=none icon=👩🏻
Lea @0.7,2.3 proto=none icon=🧑🏾‍🦱
Ivy @1.3,1.7  proto=none icon=🧕🏽
Rex @1.3,2.3  proto=none icon=👴🏿
B3 -- Ava hazard=0.2 B3:edge
B3 -- Lea hazard=0.2 B3:edge
B3 -- Ivy hazard=0.2 B3:edge
B3 -- Rex hazard=0.2 B3:edge

Ana @1.7,0.7 proto=none icon=👩🏿
Eve @1.7,1.3 proto=none icon=👧🏼
Abe @2.3,0.7  proto=none icon=🧓🏿
Ian @2.3,1.3  proto=none icon=🧔🏾
C2 -- Ana hazard=0 C2:edge
C2 -- Eve hazard=0 C2:edge
C2 -- Abe hazard=0 C2:edge
C2 -- Ian hazard=0 C2:edge

Ned @1.7,1.7 proto=none icon=👨🏼‍🦳
Lou @1.7,2.3 proto=none icon=🧑🏿
Fay @2.3,1.7  proto=none icon=👧🏻
Sue @2.3,2.3  proto=none icon=👩🏽‍🦰
C3 -- Ned hazard=0 C3:edge
C3 -- Lou hazard=0 C3:edge
C3 -- Fay hazard=0 C3:edge
C3 -- Sue hazard=0 C3:edge
</code></pre></div>

<div class="admonition">
<p class="admonition-title">Note</p>
<p>This article is also available as a <a href="/en/blog/2026-spanning-tree-video" title="A non-interactive tour of the spanning tree protocol">video</a>, but I advise you to
keep reading here to try the interactive demonstrations.</p>
</div>
<div class="toc">
<ul>
<li><a href="#the-basics">The basics</a><ul>
<li><a href="#historical-interlude">Historical interlude</a></li>
<li><a href="#electing-the-root-bridge">Electing the root bridge</a></li>
<li><a href="#assigning-roles-to-ports">Assigning roles to ports</a></li>
<li><a href="#port-state-transition">Port state transition</a></li>
<li><a href="#topology-change-notification">Topology change notification</a></li>
<li><a href="#security">Security</a></li>
</ul>
</li>
<li><a href="#why-rstp-today">Why RSTP today?</a><ul>
<li><a href="#how-large-can-a-network-be">How large can a network be?</a></li>
<li><a href="#how-fast-is-rstp">How fast is RSTP?</a></li>
</ul>
</li>
<li><a href="#about-mstp">About MSTP</a></li>
<li><a href="#about-the-interactive-examples">About the interactive examples</a></li>
</ul>
</div>
<h1 id="the-basics">The basics</h1>
<p>Designed in the ’80s, the <em>spanning tree protocol</em> has evolved into a “rapid”
flavor (<abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>) and a “VLAN-aware” variation (<abbr title="Multiple Spanning Tree Protocol">MSTP</abbr>).<sup id="fnref:history"><a class="footnote-ref" href="#fn:history">2</a></sup> Any sound-minded
network engineer knows there are better alternatives, like <a href="/en/blog/2017-vxlan-bgp-evpn" title="VXLAN: BGP EVPN with FRR">BGP EVPN VXLAN</a>.
Yet, because any switch speaks it, the venerable spanning tree protocol still
fills a niche.</p>
<p>We focus on <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>: it replaced the original protocol in 2004. To eliminate
network loops, <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> implements a complex state machine. Timers, link state
changes, and the link-local control frames a bridge receives from its neighbors
drive its transitions. These Ethernet frames are the <em>Bridge Protocol Data
Units</em> (<abbr title="Bridge Protocol Data Units">BPDUs</abbr>). You can watch them in action below: hit the “Start” button.</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp
:tx-hold 10

A1 @0,1
C11 @1,0 prio=4096 icon=🌳
C12 @1,2 prio=4096 icon=🌳
C21 @2,0 prio=4096 icon=🌳
C22 @2,2 prio=4096 icon=🌳
A2 @3,1

H1 @0,0.2 proto=none icon=💻
H2 @0,1.8 proto=none icon=🖨️
H3 @3,0.2 proto=none icon=📠
H4 @3,1.8 proto=none icon=📺

A1 -- C11
A1 -- C12
A2 -- C21
A2 -- C22
C11 -- C12
C11 -- C21
C11 -- C21
C11 -- C22
C12 -- C21
C12 -- C22
C21 -- C22
A1 -- H1 A1:edge
A1 -- H2 A1:edge
A2 -- H3 A2:edge
A2 -- H4 A2:edge
</code></pre></div>

<p>After <a href="#mstp:5,...">some time</a>, the topology converges to a tree: from the root
C11, there is a path to each bridge<sup id="fnref:bridge"><a class="footnote-ref" href="#fn:bridge">3</a></sup> and no loop. In the upper right
corner, the interface displays a tree icon 🌳 followed by the time it took to
reach this state. <a href="#mstp:9,C11--C12,...">Cut a link</a> and see how the protocol
finds an alternate path to reach C12 in less than a second. You can stop the
simulation, move it forward step by step, reset it to its initial state, or slow
it down with the “snail” mode 🐌. Don’t worry about all the displayed
information: I explain it later.</p>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>While you scroll, the current simulation stays in view so you can
look at it while reading. Uncheck this box to disable this behavior: <input type="checkbox" id="lf-mstp-sticky" checked="checked"/>.</p>
</div>
<p>All examples run in your browser, powered by <a href="https://github.com/mstpd/mstpd" title="Multiple Spanning Tree Protocol Daemon">MSTPD</a>—an open-source
user-space<sup id="fnref:kernel"><a class="footnote-ref" href="#fn:kernel">4</a></sup> implementation of <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>.<sup id="fnref:incomplete"><a class="footnote-ref" href="#fn:incomplete">5</a></sup></p>
<h2 id="historical-interlude">Historical interlude</h2>
<p><a href="https://hiddenheroes.netguru.com/radia-perlman" title="The Magic Box and the Spanning Tree: How Radia Perlman made the Internet work">Radia Perlman</a>, an inductee of the <a href="https://www.internethalloffame.org/inductee/radia-perlman/" title="Radia Perlman profile on the Internet Hall of Fame">Internet Hall of Fame</a> in 2014,
summarized the ancestor of <abbr title="Spanning Tree Protocol">STP</abbr> she invented at <abbr title="Digital Equipment Corporation">DEC</abbr> with this poem, later
included in a <a href="https://patents.google.com/patent/US7339900B2/en" title="Method and apparatus for preventing spanning tree loops during traffic overload conditions">US patent</a>:</p>
<blockquote>
<p>I think that I shall never see<br/>
A graph more lovely than a tree.<br/>
A tree whose crucial property<br/>
Is loop-free connectivity.<br/>
A tree which must be sure to span<br/>
So packets can reach every LAN.<br/>
First, the root must be selected.<br/>
By ID, it is elected.<br/>
Least cost paths from root are traced.<br/>
In the tree, these paths are placed.<br/>
A mesh is made by folks like me,<br/>
Then bridges find a spanning tree.</p>
<p>― <em>Radia Perlman</em>, <a href="https://hiddenheroes.netguru.com/radia-perlman" title="The Magic Box and the Spanning Tree: How Radia Perlman made the Internet work">Algorhyme</a>.</p>
</blockquote>
<h2 id="electing-the-root-bridge">Electing the root bridge</h2>
<p>To build a tree, <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> first elects the bridge with the <strong>lowest bridge
identifier</strong> as the <strong>root bridge</strong>. The bridge identifier combines the priority
and the MAC address: <code>8192.6e:2b:10:a0:5f:29</code>.</p>
<p>In the example below, S1 and S2 have priorities of 4,096 and 8,192: S1 becomes
root. S4 has a priority of 12,288, while S3 keeps the default priority of
32,768:<sup id="fnref:priority"><a class="footnote-ref" href="#fn:priority">6</a></sup> S4 becomes root. S5 and S6 don’t have a specific priority, so
the lowest MAC address wins and S5 becomes root.</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp

S1 @0,0 prio=4096
S2 @0,1 prio=8192
S1 -- S2

S3 @1,0
S4 @1,1 prio=12288
S3 -- S4

S5 @2,0
S6 @2,1
S5 -- S6
</code></pre></div>

<p><a href="#mstp:2,S2-&gt;S1,@">Initially</a>, each bridge advertises itself as
root:<sup id="fnref:wireshark"><a class="footnote-ref" href="#fn:wireshark">7</a></sup></p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="hll"><span class="ss">    Root Identifier</span>: 8192.02:00:00:01:00:01
</span><span class="hll"><span class="ss">    Bridge Identifier</span>: 8192.02:00:00:01:00:01
</span></code></pre></div>

<p>Once a bridge receives a <abbr title="Bridge Protocol Data Unit">BPDU</abbr> advertising a better root bridge, it
<a href="#mstp:3,S2-&gt;S1,@">propagates</a> this new information to its neighbors.</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="hll"><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
</span><span class="hll"><span class="ss">    Bridge Identifier</span>: 8192.02:00:00:00:00:01
</span></code></pre></div>

<h2 id="assigning-roles-to-ports">Assigning roles to ports</h2>
<p>The second step is to assign a role to each port. <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> defines five roles, each
denoted by a letter:</p>
<ul>
<li>root (R),</li>
<li>designated (D),</li>
<li>alternate (A),</li>
<li>disabled (X), or</li>
<li>backup (B).<sup id="fnref:backup"><a class="footnote-ref" href="#fn:backup">8</a></sup></li>
</ul>
<p>Each non-root bridge chooses its <strong>root port</strong>, the one with the lowest-cost
path to the root. Unless you override it, each bridge derives the link cost
from the speed: 20,000 for 1 Gbps. In case of equality, the lowest port
identifier wins.</p>
<p>Each remaining port becomes a <strong>designated port</strong> if the <abbr title="Bridge Protocol Data Unit">BPDU</abbr> it sends is
“better” than the <abbr title="Bridge Protocol Data Unit">BPDU</abbr> it receives. Otherwise, it becomes an <strong>alternate port</strong>.
Later, if the root port goes down, the “best” alternate port becomes the new
root port. The tiebreakers for the best <abbr title="Bridge Protocol Data Unit">BPDU</abbr> are:</p>
<ol>
<li>the lowest root bridge identifier,</li>
<li>the lowest accumulated cost to the root,</li>
<li>the lowest bridge identifier, and</li>
<li>the lowest port identifier.</li>
</ol>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp

S1 @1,0  prio=4096 icon=🌳
S2 @0,1
S3 @2,1

S1 -- S2
S1 -- S3
S1 -- S3
S2 -- S3
</code></pre></div>

<p>In the example above, <a href="#mstp:13">after convergence</a>, S1 is the root bridge
because it has a priority of 4,096, while the other bridges have a priority of
32,768. All its ports are designated ports because the accumulated cost to the
root is 0.</p>
<p>S2’s port facing S1 becomes a root port because it has the lowest accumulated
cost to the root—20,000 vs 40,000. S3 has two ports facing S1, and the one with
the lowest port identifier becomes the root port—<code>0x8000</code> vs <code>0x8001</code>. The other
candidate is an alternate port because the remote port on the link sends a
better <abbr title="Bridge Protocol Data Unit">BPDU</abbr>, with an accumulated cost of 0. On the segment between S2 and S3,
S2’s port wins: while both bridges have the same accumulated cost to the root
(20,000), S2’s bridge identifier is smaller—<code>32768.02:00:00:00:00:01</code> vs
<code>32768.02:00:00:00:00:02</code>.</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="hll"><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
</span><span class="hll"><span class="ss">    Root Path Cost</span>: 20000
</span><span class="hll"><span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:01
</span><span class="ss">    Port identifier</span>: 0x8002
</code></pre></div>

<p>If you <a href="#mstp:13,S1--S3:1">cut the active link between S1 and S3</a>, S3 promotes
the “best” alternate port to root port. If you also <a href="#mstp:13,S1--S3:1,S1--S3:2">disable the second
link</a>, S3 chooses the remaining alternate port as a
root port. But if you <a href="#mstp:13,S1--S2,@,...">disable the link between S1 and
S2</a>, S2 needs a bit more work to elect a new root port
because it does not have an alternate port.</p>
<p>Unless a specific event happens, designated ports send <abbr title="Bridge Protocol Data Units">BPDUs</abbr> <a href="#mstp:13,S1-&gt;S3:2,S2-&gt;S3,S1-&gt;S2,@">every 2
seconds</a>.<sup id="fnref:hello"><a class="footnote-ref" href="#fn:hello">9</a></sup> If a bridge does not
receive <abbr title="Bridge Protocol Data Units">BPDUs</abbr> from its neighbor for 3 consecutive hello periods, it considers
the neighbor dead and removes the port information.</p>
<h2 id="port-state-transition">Port state transition</h2>
<p>Each port can have one of three states. The diagram displays a background color
for each state:</p>
<ul>
<li>discarding (red),</li>
<li>learning (yellow), or</li>
<li>forwarding (green).</li>
</ul>
<p>A <em>root port</em> transitions automatically to the forwarding state. An <em>alternate
port</em> stays in the discarding state. A <em>designated port</em> has two options to
transition from the discarding state to the forwarding state:</p>
<ul>
<li>If the port is an <strong>edge port</strong>, either through configuration or because the
  remote device does not speak any flavor of <abbr title="Spanning Tree Protocol">STP</abbr>, the bridge assumes the device
  won’t participate in the protocol and cannot create a loop. In this case, the
  designated port immediately transitions to the forwarding state.</li>
<li>Otherwise, it sends a <strong>proposal</strong> to its downstream neighbor. If the remote
  bridge agrees that the received <abbr title="Bridge Protocol Data Unit">BPDU</abbr> is “better” than any other <abbr title="Bridge Protocol Data Unit">BPDU</abbr> stored
  for other ports, it elects the receiving port as its root port and starts the
  <strong>synchronization</strong> process: it transitions all non-edge non-synced designated
  ports to the discarding state to avoid a loop. Then, it sends back an
  <strong>agreement</strong>. Upon receiving the agreement, the peer designated port
  transitions to the forwarding state.<sup id="fnref:learning"><a class="footnote-ref" href="#fn:learning">10</a></sup></li>
</ul>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp

S1 @1,0 prio=4096 icon=🌳
S2 @1,1
S3 @0,2
S4 @2,2
S5 @0,3 prio=8192 icon=🪾
S6 @2,3
H1 @0,1.2   proto=none icon=🖨️
H2 @2,1.2   proto=none icon=📠
H3 @2.5,1.3 proto=none icon=📺
H4 @2.5,2.3 proto=none icon=💻

S1 -- S2
S2 -- S3
S2 -- S4
S3 -- S5
S4 -- S6
S4 -- S3
S5 -- S6

S3 -- H1 S3:edge
S4 -- H2 S4:edge
S4 -- H3 S4:edge
S6 -- H4 S6:edge
</code></pre></div>

<p>In the topology above, H1, H2, H3, and H4 are end devices not participating in
the protocol. We configure the ports they connect to as edge ports, so these
ports immediately move to the forwarding state.</p>
<p>Use the “step” button to move the simulation forward. The clock moves to 1
second. <a href="#mstp:2,S1-&gt;S2,S2-&gt;S1,@">Step again</a> and S1 and S2 send a proposal to
each other. Here is the proposal from S2:</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x4e, Agreement, Port Role: Designated, Proposal
        <span class="no">0... .... </span><span class="o">= </span><span class="ss">Topology Change Acknowledgment</span>: No
        <span class="no">.1.. .... </span><span class="o">= </span><span class="ss">Agreement</span>: Yes
        <span class="no">..0. .... </span><span class="o">= </span><span class="ss">Forwarding</span>: No
        <span class="no">...0 .... </span><span class="o">= </span><span class="ss">Learning</span>: No
<span class="hll">        <span class="no">.... 11.. </span><span class="o">= </span><span class="ss">Port Role</span>: Designated <span class="p">(</span><span class="m">3</span><span class="p">)</span>
</span><span class="hll">        <span class="no">.... ..1. </span><span class="o">= </span><span class="ss">Proposal</span>: Yes
</span>        <span class="no">.... ...0 </span><span class="o">= </span><span class="ss">Topology Change</span>: No
<span class="hll"><span class="ss">    Root Identifier</span>: 32768.02:00:00:00:00:01
</span><span class="ss">    Root Path Cost</span>: 0
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:01
<span class="ss">    Port identifier</span>: 0x8001
</code></pre></div>

<p>S1 ignores it: its own root identifier is lower. When S2 receives a similar
proposal from S1, it accepts S1 as its root bridge. It also elects the port to
S1 as the root port and starts the synchronization process. The two designated
ports are already discarding, so no change here. <a href="#mstp:3,S2-&gt;S1#2,@">Step
again</a> and S2 sends two <abbr title="Bridge Protocol Data Units">BPDUs</abbr> to S1. In one of them, the
agreement bit is 1 and the proposal bit is 0. It also shows that S2 accepted S1
as the root bridge and its root port is now in the forwarding state. When
receiving this <abbr title="Bridge Protocol Data Unit">BPDU</abbr>, S1 transitions its own designated port to the forwarding
state. From this point, the link between S1 and S2 forwards user traffic.</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x79, Agreement, Forwarding, Learning, Port Role: Root, Topology Change
        <span class="no">0... .... </span><span class="o">= </span><span class="ss">Topology Change Acknowledgment</span>: No
<span class="hll">        <span class="no">.1.. .... </span><span class="o">= </span><span class="ss">Agreement</span>: Yes
</span><span class="hll">        <span class="no">..1. .... </span><span class="o">= </span><span class="ss">Forwarding</span>: Yes
</span>        <span class="no">...1 .... </span><span class="o">= </span><span class="ss">Learning</span>: Yes
<span class="hll">        <span class="no">.... 10.. </span><span class="o">= </span><span class="ss">Port Role</span>: Root <span class="p">(</span><span class="m">2</span><span class="p">)</span>
</span><span class="hll">        <span class="no">.... ..0. </span><span class="o">= </span><span class="ss">Proposal</span>: No
</span>        <span class="no">.... ...1 </span><span class="o">= </span><span class="ss">Topology Change</span>: Yes
<span class="hll"><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
</span><span class="ss">    Root Path Cost</span>: 20000
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:01
<span class="ss">    Port identifier</span>: 0x8001
</code></pre></div>

<p>Let’s look at what happened to S5. <a href="#mstp:2,S5-&gt;S3,S3-&gt;S5,S5-&gt;S6,S6-&gt;S5,@">Reset the simulation and step
twice</a>. S5 exchanges <abbr title="Bridge Protocol Data Units">BPDUs</abbr> with both S3
and S6. Since S5 has a lower root identifier than S3 and S6, it stays the root
bridge, while S3 and S6 accept the proposal and elect their root ports. S3 and
S6 start the synchronization process. S6’s port to H4 keeps forwarding because
it is an edge port. <a href="#mstp:3,S3-&gt;S5#1,S6-&gt;S5#1,@">Move one step</a>. Both S3 and S6 send
an agreement back to S5, which transitions both designated ports to the
forwarding state. Yet, the link between S5 and S3 keeps discarding user traffic!
If you look carefully, S3’s port toward S5 is now a designated port, not a root
port. During the <a href="#mstp:3,S2-&gt;S3,@">same step</a>, S3 also receives a better <abbr title="Bridge Protocol Data Unit">BPDU</abbr>
from S2 with S1 as the root bridge. It elects its port to S2 as the root port
and downgrades the port to S5 to a designated port, which stays in the
discarding state.</p>
<p>On the <a href="#mstp:4,S3-&gt;S5,@">next step</a>, things get a bit tricky. S3 sends a
proposal to S5:<sup id="fnref:agreement"><a class="footnote-ref" href="#fn:agreement">11</a></sup></p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x4f, Agreement, Port Role: Designated, Proposal, Topology Change
        <span class="no">0... .... </span><span class="o">= </span><span class="ss">Topology Change Acknowledgment</span>: No
        <span class="no">.1.. .... </span><span class="o">= </span><span class="ss">Agreement</span>: Yes
        <span class="no">..0. .... </span><span class="o">= </span><span class="ss">Forwarding</span>: No
        <span class="no">...0 .... </span><span class="o">= </span><span class="ss">Learning</span>: No
        <span class="no">.... 11.. </span><span class="o">= </span><span class="ss">Port Role</span>: Designated <span class="p">(</span><span class="m">3</span><span class="p">)</span>
<span class="hll">        <span class="no">.... ..1. </span><span class="o">= </span><span class="ss">Proposal</span>: Yes
</span>        <span class="no">.... ...1 </span><span class="o">= </span><span class="ss">Topology Change</span>: Yes
<span class="hll"><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
</span><span class="ss">    Root Path Cost</span>: 40000
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:02
<span class="ss">    Port identifier</span>: 0x8002
</code></pre></div>

<p>S5 elects S1 as its root bridge and the port toward S3 as its root port. It
starts its synchronization process, but the designated port to S6 does <em>not</em>
move into the discarding state. Why? That port stays a designated port and its
neighbor S6 has already sent an agreement on the link, so the port keeps its
synced status.</p>
<p>Now, let’s <a href="#mstp:3,@">step back</a> to look at what happens to S6. At this point,
S6 believes S5 is the root bridge. <a href="#mstp:4,S4-&gt;S6#1,@">Step once</a> and S4 sends
a new proposal to S6. S6 accepts the proposal, elects S1 as the root bridge and
the port to S4 as its root port. The role of the port facing S5 changes: from a
root port, it becomes a designated port. Because its peer keeps advertising an
inferior <abbr title="Bridge Protocol Data Unit">BPDU</abbr> on the link, this port becomes disputed and moves to the
discarding state. The root port transitions to the forwarding state and the link
starts forwarding immediately because S4’s designated port is already in the
forwarding state. If we <a href="#mstp:5,S5-&gt;S6,S6-&gt;S5,@">step one more time</a>, S5 and S6
exchange two <abbr title="Bridge Protocol Data Units">BPDUs</abbr>. The one from S5 is better because of its lower bridge
identifier. S5’s port stays a designated port, while S6 downgrades its own port
to an alternate port.</p>
<p>Let’s rewind to the start one last time: cut the link between S1 and S2, <a href="#mstp:S1--S2,9">run
the simulation until the topology is stable</a>, stop the
simulation, and restore the link between S1 and S2. During the <a href="#mstp:S1--S2,9,S1--S2,1,S1-&gt;S2,S2-&gt;S1,@">first
step</a>, S1 and S2 exchange proposals. S2
elects S1 as the root bridge instead of S5 and the port to S1 as the root port.
It downgrades the previous root port to a designated port and moves it into the
discarding state. The other designated port stays synced and keeps its
forwarding state. At the <a href="#mstp:S1--S2,9,S1--S2,2,S2-&gt;S1,@">next step</a>, S2 sends
an agreement to S1 and the link between them starts forwarding user traffic. It
also <a href="#mstp:S1--S2,9,S1--S2,2,S2-&gt;S3,@">sends a proposal to S3</a>, but not to S4.
Instead, <a href="#mstp:S1--S2,9,S1--S2,2,S2-&gt;S4,@">it sends a regular <abbr title="Bridge Protocol Data Unit">BPDU</abbr> to S4</a>. S4
still elects S1 as its root bridge and the port to S2 as its root port. It
demotes its previous root port, the one to S3, to a designated port, which
transitions to the discarding state because of the root port change. The other
alternate port, to S6, also becomes a designated port and stays in the
discarding state. The new root port moves to the forwarding state. On the <a href="#mstp:S1--S2,9,S1--S2,3,S3-&gt;S4#1,@">next
step</a>, S4’s port to S3 settles as an
alternate port after receiving a “better” <abbr title="Bridge Protocol Data Unit">BPDU</abbr> from S3.</p>
<p><abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> is a giant state machine split into smaller ones: bridge detection, port
information, port protocol migration, port role selection, port role
transitions, port receive, port state transitions, port timers, port transmit,
and topology change. Some of them are per bridge, some per port. Each bridge
runs an instance. Time, operational port state changes, and the <abbr title="Bridge Protocol Data Units">BPDUs</abbr> it
receives from other instances drive the transitions. Being event-driven makes
<abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> more efficient but also more difficult to understand.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 900px"><span class="lf-media-inner" style="padding-bottom: 43.333%"><img alt="Western Australian Government Railways class Msa Garratt articulated steam&#10;locomotive: elevation and plan drawing" src="https://media-bernat-ch.b-cdn.net/images/msa-garratt@1x.23f40c8ca9d5b5.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/msa-garratt@1x.23f40c8ca9d5b5.jpg 1131w,https://media-bernat-ch.b-cdn.net/images/msa-garratt@2x.ea938133978ead.jpg 1800w" sizes="auto, (max-width: 900px) 100vw, 900px" width="900" height="390" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAANAQMAAACEvEzUAAAAA1BMVEX///+nxBvIAAAADElEQVR42mNgoBAAAABBAAEodTPOAAAAAElFTkSuQmCC)"/></span></div><figcaption>Placeholder for the <em>Port Information</em> state machine extracted from IEEE 802.1Q-2005, page 182. Pending IEEE authorization for reproduction, this is the blueprint for the Western Australian Government Railways class Msa Garratt articulated steam locomotive.</figcaption></figure>
<h2 id="topology-change-notification">Topology change notification</h2>
<p>A bridge populates a MAC address table: it associates each source MAC address
with the port that last received it. When forwarding an Ethernet frame, it looks
up this table to choose the right port.<sup id="fnref:bum"><a class="footnote-ref" href="#fn:bum">12</a></sup> When a link fails, a connected
fridge reachable through one port may become reachable through another one. The
affected bridges should flush the MAC addresses they learned, because these
entries may now be wrong.</p>
<p>For this purpose, <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> implements <em>topology change notifications</em> using a
flooding mechanism. When a non-edge port transitions to the forwarding state, a
bridge generates <abbr title="Bridge Protocol Data Units">BPDUs</abbr> with the <em>topology change</em> (<abbr title="Topology Change">TC</abbr>) bit set. It sends them to
all the non-edge designated ports and to the root port. It also flushes the MAC
address table on these ports. When a bridge receives such a <abbr title="Bridge Protocol Data Unit">BPDU</abbr>, it propagates
the notification to all non-edge designated ports and the root port, except the
one the notification came from. It also flushes the MAC address table on these
ports. In the examples, the <abbr title="Bridge Protocol Data Units">BPDUs</abbr> with the <abbr title="Topology Change">TC</abbr> bit set to 1 have a red circle.</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp

S1 @1,0 prio=4096 icon=🌳
S2 @0,1
S3 @1,1
S4 @2,1
S5 @1,2
LPT @0.1,2 proto=none icon=🖨️

S1 -- S2
S1 -- S3
S1 -- S4
S2 -- S3
S2 -- S5
S4 -- S5
S5 -- LPT S5:edge
</code></pre></div>

<p>Start the simulation and wait a <a href="#mstp:10">few seconds</a> for the topology to
settle. Stop the simulation and <a href="#mstp:10,S2--S5">disable the link between S2 and
S5</a>. S5 elects the port facing S4 as the root port, which
transitions immediately to the forwarding state. <a href="#mstp:10,S2--S5,1,S5-&gt;S4,@">Step
once</a> and S5 emits a <abbr title="Bridge Protocol Data Unit">BPDU</abbr> with the <abbr title="Topology Change">TC</abbr> bit set to 1:</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x79, Agreement, Forwarding, Learning, Port Role: Root, Topology Change
        <span class="no">0... .... </span><span class="o">= </span><span class="ss">Topology Change Acknowledgment</span>: No
        <span class="no">.1.. .... </span><span class="o">= </span><span class="ss">Agreement</span>: Yes
        <span class="no">..1. .... </span><span class="o">= </span><span class="ss">Forwarding</span>: Yes
        <span class="no">...1 .... </span><span class="o">= </span><span class="ss">Learning</span>: Yes
        <span class="no">.... 10.. </span><span class="o">= </span><span class="ss">Port Role</span>: Root <span class="p">(</span><span class="m">2</span><span class="p">)</span>
        <span class="no">.... ..0. </span><span class="o">= </span><span class="ss">Proposal</span>: No
<span class="hll">        <span class="no">.... ...1 </span><span class="o">= </span><span class="ss">Topology Change</span>: Yes
</span><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
<span class="ss">    Root Path Cost</span>: 40000
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:04
<span class="ss">    Port identifier</span>: 0x8002
</code></pre></div>

<p>S4 receives this <abbr title="Bridge Protocol Data Unit">BPDU</abbr>. It flushes the MAC address table on the port facing S1:
while LPT was previously reachable through this port, it is now reachable
through S5. <a href="#mstp:10,S2--S5,2,S4-&gt;S1,@">Step once</a>. S4 sends S1 a <abbr title="Bridge Protocol Data Unit">BPDU</abbr>
with the <abbr title="Topology Change">TC</abbr> bit set to 1. When S1 receives this <abbr title="Bridge Protocol Data Unit">BPDU</abbr>, it flushes the MAC address
table on the ports facing S2 and S3. <a href="#mstp:10,S2--S5,3,S1-&gt;S2,S1-&gt;S3,@">Step
once</a> and S1 sends a notification to S2 and
S3. <a href="#mstp:10,S2--S5,4,S2-&gt;S3,@">Step once again</a> and S2 sends a notification to
S3, while S3 does nothing because the port toward S2 is an <em>alternate port</em>. S3
does not flush any MAC address table: LPT is still reachable through its port to
S1.</p>
<p>If you <a href="#mstp:10,S2--S5,6,@">step a bit more</a>, you will see that some of the
periodic <abbr title="Bridge Protocol Data Units">BPDUs</abbr> keep the <abbr title="Topology Change">TC</abbr> bit set to 1. Each port runs a timer equal to the
hello timer plus one second.<sup id="fnref:timer"><a class="footnote-ref" href="#fn:timer">13</a></sup> The timer starts when the port emits a
notification. Until it expires, the port sets the <abbr title="Topology Change">TC</abbr> bit to 1 in every <abbr title="Bridge Protocol Data Unit">BPDU</abbr> it
sends. You can also see <a href="#mstp:10,S2--S5,7,S1-&gt;S4,S4-&gt;S5,@">some periodic <abbr title="Bridge Protocol Data Units">BPDUs</abbr></a>
without the <abbr title="Topology Change">TC</abbr> bit: they originate from a port that only received a notification
and therefore did not arm its timer.</p>
<h2 id="security">Security</h2>
<p><abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> is weak against configuration errors and malicious actors. A bridge not
talking <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> can create a loop. An attacker can insert themselves into the
topology to disrupt the service, spy on the traffic, or alter it.</p>
<p>To mitigate such problems, you need to identify the edge ports. An edge port
connects to an end device, like a PC or a printer. Such devices do not generate
<abbr title="Bridge Protocol Data Units">BPDUs</abbr> and cannot create a loop. <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> defines two related flags:</p>
<ul>
<li>When true, <strong>AdminEdge</strong> initializes a port as an edge port. It defaults to
  false.</li>
<li>When true, <strong>AutoEdge</strong> lets a port become an edge port when it does not
  receive <abbr title="Bridge Protocol Data Units">BPDUs</abbr> for 3 seconds. It defaults to true.</li>
</ul>
<p>If an edge port receives a <abbr title="Bridge Protocol Data Unit">BPDU</abbr>, regardless of the values of these two flags, it
reverts to a non-edge port.</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>R0 @1.5,1.5 prio=8192

# AutoEdge=true, AdminEdge=false, bridge
S1 @3,1.58
R0 -- S1

# AutoEdge=true, AdminEdge=false, end device
H1 @2.84,2.18 icon=🖨️ proto=none
R0 -- H1

# AutoEdge=true, AdminEdge=true, bridge
S2 @2.18,2.84
R0 -- S2 R0:edge

# AutoEdge=true, AdminEdge=true, end device
H2 @1.58,3 icon=💻 proto=none
R0 -- H2 R0:edge

# AutoEdge=false, AdminEdge=true, bridge
S3 @0.68,2.76
R0 -- S3 R0:edge R0:no-auto-edge

# AutoEdge=false, AdminEdge=true, end device
H3 @0.24,2.32 icon=📠 proto=none
R0 -- H3 R0:edge R0:no-auto-edge

# AutoEdge=false, AdminEdge=false, bridge
S4 @0,1.42
R0 -- S4 R0:no-auto-edge

# AutoEdge=false, AdminEdge=false, end device
H4 @0.16,0.82 icon=📺 proto=none
R0 -- H4 R0:no-auto-edge

# Network port, bridge
S5 @0.82,0.16
R0 -- S5 R0:network S5:network

# Network port, end device
H5 @1.42,0 icon=☕ proto=none
R0 -- H5 R0:network

# AdminEdge=true, bpdu-guard=true, bridge
S6 @2.32,0.24
R0 -- S6 R0:bpdu-guard R0:edge

# AdminEdge=true, bpdu-guard=true, end device
H6 @2.76,0.68 icon=💡 proto=none
R0 -- H6 R0:bpdu-guard R0:edge
</code></pre></div>

<p>In the topology above, S1, S2, S3, S4, S5, and S6 act as bridges, while H1, H2,
H3, H4, H5, and H6 act as end devices:</p>
<ul>
<li>S1 and H1 are on a port without a specific configuration: <em>AutoEdge</em> is true,
  <em>AdminEdge</em> is false,</li>
<li>S2 and H2 are on a port where <em>AdminEdge</em> is true,</li>
<li>S3 and H3 are on a port where <em>AutoEdge</em> is false and <em>AdminEdge</em> is true,</li>
<li>S4 and H4 are on a port where <em>AutoEdge</em> is false.</li>
</ul>
<p>If you start the simulation and <a href="#mstp:24">wait about 20 seconds</a>, links to S1,
S2, S3, S4, H1, H2, H3, and H4 eventually forward user traffic: none of the
flags matter.</p>
<p>But what about the two remaining pairs? S5 and H5 connect to a <em>network</em> port.
Such a port enables a non-standard feature: <strong>bridge assurance</strong>. The port
transmits <abbr title="Bridge Protocol Data Units">BPDUs</abbr> regardless of its role. If it does not receive <abbr title="Bridge Protocol Data Units">BPDUs</abbr> for 3
consecutive hello periods, it transitions to the discarding state. On the link
between R0 and S5, you can see <a href="#mstp:24,R0-&gt;S5,S5-&gt;R0,@"><abbr title="Bridge Protocol Data Units">BPDUs</abbr> traveling in both
directions</a>, unlike the other links, where only
designated ports send <abbr title="Bridge Protocol Data Units">BPDUs</abbr>.</p>
<p>S6 and H6 connect to a port where <em>AdminEdge</em> is true and <strong><abbr title="Bridge Protocol Data Unit">BPDU</abbr> guard</strong> is
enabled. This is another non-standard feature that shuts down a port if <a href="#mstp:2,S6-&gt;R0,@">it
receives a <abbr title="Bridge Protocol Data Unit">BPDU</abbr></a>.</p>
<p>In summary, if you expect a port to be an edge port, you should set <em>AdminEdge</em>
to true and enable <em><abbr title="Bridge Protocol Data Unit">BPDU</abbr> guard</em>. Otherwise, declare it as a <em>network</em> port.</p>
<h1 id="why-rstp-today">Why <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> today?</h1>
<p>A compelling use case for <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> today is an out-of-band network for a datacenter,
since you can tolerate an outage of a few seconds. The configuration is minimal
and you can use cheap switches, like a Cisco 2960X.<sup id="fnref:price"><a class="footnote-ref" href="#fn:price">14</a></sup> You need two
switches acting as root bridges, and you build several loops to connect <abbr title="Out-of-band">OOB</abbr>
switches in each cabinet. This simple design survives one failure on each
loop.<sup id="fnref:erps"><a class="footnote-ref" href="#fn:erps">15</a></sup></p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp
:tx-hold 10

# Root bridges
R1 @0,1 prio=0
R2 @0,2 prio=4096
R1 -- R2 cost=200 R1:network R2:network
R1 -- R2 cost=200 R1:network R2:network

# First loop
C1  @1,0 icon=🗄️
C4  @2,0 icon=🗄️
C7  @3,0 icon=🗄️
C10 @4,0 icon=🗄️
C12 @5,0 icon=🗄️
C13 @5,3 icon=🗄️
C15 @4,3 icon=🗄️
C18 @3,3 icon=🗄️
C21 @2,3 icon=🗄️
C24 @1,3 icon=🗄️
R1  -- C1  R1:network C1:network
C1  -- C4  C1:network C4:network
C4  -- C7  C4:network C7:network
C7  -- C10 C7:network C10:network
C10 -- C12 C10:network C12:network
C12 -- C13 C12:network C13:network
C13 -- C15 C13:network C15:network
C15 -- C18 C15:network C18:network
C18 -- C21 C18:network C21:network
C21 -- C24 C21:network C24:network
C24 -- R2  C24:network R2:network

# Second loop
C2  @1,0.5 icon=🗄️
C5  @2,0.5 icon=🗄️
C8  @3,0.5 icon=🗄️
C11 @4,0.5 icon=🗄️
C14 @4,2.5 icon=🗄️
C17 @3,2.5 icon=🗄️
C20 @2,2.5 icon=🗄️
C23 @1,2.5 icon=🗄️
R1  -- C2  R1:network C2:network
C2  -- C5  C2:network C5:network
C5  -- C8  C5:network C8:network
C8  -- C11 C8:network C11:network
C11 -- C14 C11:network C14:network
C14 -- C17 C14:network C17:network
C17 -- C20 C17:network C20:network
C20 -- C23 C20:network C23:network
C23 -- R2  C23:network R2:network

# Third loop
C3  @1,1 icon=🗄️
C6  @2,1 icon=🗄️
C9  @3,1 icon=🗄️
C16 @3,2 icon=🗄️
C19 @2,2 icon=🗄️
C22 @1,2 icon=🗄️
R1  -- C3  R1:network C3:network
C3  -- C6  C3:network C6:network
C6  -- C9  C6:network C9:network
C9  -- C16 C9:network C16:network
C16 -- C19 C16:network C19:network
C19 -- C22 C19:network C22:network
C22 -- R2  C22:network R2:network
</code></pre></div>

<p>This topology converges in about <a href="#mstp:6">6 seconds</a>. Each loop should stay
small (around 16 bridges) to reduce the probability of a double failure and to
avoid sharing too much bandwidth. The design can evolve a bit without adding too
much complexity: one VLAN per loop or one bridge domain per loop.</p>
<h2 id="how-large-can-a-network-be">How large can a network be?</h2>
<p>The maximum age, whose default value is 20, governs the maximum distance of a
bridge from the root. The topology below is too big for <abbr title="Bridge Protocol Data Units">BPDUs</abbr> from R1 to reach
beyond S20.<sup id="fnref:root-mac"><a class="footnote-ref" href="#fn:root-mac">16</a></sup></p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp
:tx-hold 10
:max-age 20

R1 @0,0 prio=4096 icon=🌳
R2 @0,5 prio=4096 icon=🪾

S1  @1,0
S2  @2,0
S3  @3,0
S4  @4,0
S5  @5,0
S6  @6,0

S7  @6,1
S8  @5,1
S9  @4,1
S10 @3,1
S11 @2,1
S12 @1,1

S13 @1,2
S14 @2,2
S15 @3,2
S16 @4,2
S17 @5,2
S18 @6,2

S19 @6,3
S20 @5,3
S21 @4,3
S22 @3,3
S23 @2,3
S24 @1,3

S25 @1,4
S26 @2,4
S27 @3,4
S28 @4,4
S29 @5,4
S30 @6,4

S31 @6,5
S32 @5,5
S33 @4,5
S34 @3,5
S35 @2,5
S36 @1,5

R1  -- S1
S1  -- S2
S2  -- S3
S3  -- S4
S4  -- S5
S5  -- S6
S6  -- S7
S7  -- S8
S8  -- S9
S9  -- S10
S10 -- S11
S11 -- S12
S12 -- S13
S13 -- S14
S14 -- S15
S15 -- S16
S16 -- S17
S17 -- S18
S18 -- S19
S19 -- S20
S20 -- S21
S21 -- S22
S22 -- S23
S23 -- S24
S24 -- S25
S25 -- S26
S26 -- S27
S27 -- S28
S28 -- S29
S29 -- S30
S30 -- S31
S31 -- S32
S32 -- S33
S33 -- S34
S34 -- S35
S35 -- S36
S36 -- R2
R1  -- R2 cost=200 down
</code></pre></div>

<p>Once the <a href="#mstp:40">topology settles</a>, part of the network considers R1 the
root, while the other votes for R2. At the boundary, S20 tries to start a
synchronization with S21 to move its <em>designated port</em> to the forwarding state.
The <abbr title="Bridge Protocol Data Unit">BPDU</abbr> looks like this:</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x4e, Agreement, Port Role: Designated, Proposal
<span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
<span class="ss">    Root Path Cost</span>: 400000
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:15
<span class="ss">    Port identifier</span>: 0x8002
<span class="hll"><span class="ss">    Message Age</span>: 20
</span><span class="hll"><span class="ss">    Max Age</span>: 20
</span></code></pre></div>

<p>S21 rejects it because the message age equals the maximum age. On the other
hand, the <abbr title="Bridge Protocol Data Unit">BPDU</abbr> S21 sends to S20 looks like this:</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x7c, Agreement, Forwarding, Learning, Port Role: Designated
<span class="hll"><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:01
</span><span class="ss">    Root Path Cost</span>: 320000
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:16
<span class="ss">    Port identifier</span>: 0x8001
<span class="hll"><span class="ss">    Message Age</span>: 16
</span><span class="hll"><span class="ss">    Max Age</span>: 20
</span></code></pre></div>

<p>This is not enough to change S20’s <em>root port</em> because S20 has a lower <em>root
identifier</em>—<code>4096.02:00:00:00:00:00</code> vs <code>4096.02:00:00:00:00:01</code>.</p>
<p><a href="#mstp:40,R1--R2,...">Fixing the link between R1 and R2</a> resolves the issue. The
maximum message age any packet carries is now 18, below the configured maximum
age. But it only works until another link breaks. A plausible fix is to increase
the maximum age to 40.<sup id="fnref:forward-delay"><a class="footnote-ref" href="#fn:forward-delay">17</a></sup></p>
<h2 id="how-fast-is-rstp">How fast is <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>?</h2>
<p><abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> usually converges in a couple of seconds at startup. It often repairs a
tree in less than a second. Even the <a href="/en/blog/2026-spanning-tree#how-large-can-a-network-be">38-bridge topology</a> takes less than 10
seconds to converge.<sup id="fnref:time"><a class="footnote-ref" href="#fn:time">18</a></sup> Some topologies can take a bit more time to recover
when the root bridge becomes unavailable.<sup id="fnref:slow"><a class="footnote-ref" href="#fn:slow">19</a></sup></p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp

R0 @1,0 prio=0
S1 @1,1 prio=4096
S2 @0,2 prio=8192
S3 @2,2

R0 -- S1
S1 -- S2
S2 -- S3
S3 -- S1
</code></pre></div>

<p>In the topology above, start the simulation, <a href="#mstp:10">wait for convergence</a>,
hit stop, and <a href="#mstp:10,R0--S1">cut the link between R0 and S1</a>. The topology is
already optimal, but <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> has a hard time converging again.</p>
<p>First, S1 loses its root port. It has no more information about R0 and elects
itself as the root bridge. It keeps its ports to S2 and S3 as designated ports
in the forwarding state. <a href="#mstp:10,R0--S1,1,S1-&gt;S2,S1-&gt;S3,@">Step once</a> and it
sends a <abbr title="Bridge Protocol Data Unit">BPDU</abbr> to both S2 and S3 to let them know about the root change. When
receiving it, S2 accepts S1 as its root because it does not have a better root
on another port. It elects the port to S1 as its root port. The other port stays
a designated port. Both ports keep forwarding.</p>
<p>When receiving the <abbr title="Bridge Protocol Data Unit">BPDU</abbr> from S1, S3 behaves differently: it knows R0 as a better
root than S1 through its alternate port to S2. It promotes this port to a root
port and demotes the port facing S1 to a designated port, which requires a new
agreement. <a href="#mstp:10,R0--S1,2,S3-&gt;S1#1,@">Step once</a> and S3 sends a proposal to
S1 with R0 as the root bridge. S1 elects R0 as the root bridge and promotes its
port to S3 as a root port.</p>
<p>During the <a href="#mstp:10,R0--S1,2,S2-&gt;S3,@">same step</a>, S3 also receives a <abbr title="Bridge Protocol Data Unit">BPDU</abbr> from
S2 stating that S1 is the root bridge. Therefore, S3 has no port left with R0 as
the root bridge: it elects S1 as the root bridge and its port to S2 as the root
port. <a href="#mstp:10,R0--S1,3,S3-&gt;S1,@">Step once</a> and its next <abbr title="Bridge Protocol Data Unit">BPDU</abbr> to S1 includes
this information: S1 elects itself again as the root bridge. But during the
<a href="#mstp:10,R0--S1,3,S1-&gt;S2#1,@">same wave</a>, S1 sends a proposal to S2 with R0 as
the root bridge. While S1 and S3 agree that S1 is the root bridge, S2 now
believes this is R0! <a href="#mstp:10,R0--S1,4,@,...">In turn</a>, S2 again convinces S3
that R0 is the root bridge, S3 convinces S1, S1 convinces S2, and S2 convinces
S3.</p>
<p>This could go on forever, but it does not. The <abbr title="Bridge Protocol Data Units">BPDUs</abbr> saying “R0 is root”
eventually age out when the message age goes past the maximum age. In the
example above, at the <a href="#mstp:10,R0--S1,24,S2-&gt;S3,@">eleventh second</a>, S2 sends a
<abbr title="Bridge Protocol Data Unit">BPDU</abbr> to S3 with R0 as root, but S3 drops it because its message age reached the
maximum. With some luck, the topology can also converge faster if a port stops
transmitting new <abbr title="Bridge Protocol Data Units">BPDUs</abbr> after tripping the transmit hold count, whose default
value is 6 per second.</p>
<h1 id="about-mstp">About <abbr title="Multiple Spanning Tree Protocol">MSTP</abbr></h1>
<p><abbr title="Multiple Spanning Tree Protocol">MSTP</abbr> is the “VLAN-aware” version of <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>: it runs several instances of <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> and
lets the administrator map each VLAN to a specific instance. For example, you
can map VLANs 100 to 200 to a first instance, and 300 to 400 to a second
instance. The remaining VLANs map to a special instance named the Internal
Spanning Tree (<abbr title="Internal Spanning Tree">IST</abbr>). <abbr title="Multiple Spanning Tree Protocol">MSTP</abbr> adds its own complexity, but the gist is that you have
several logical topologies acting independently. If you want to dig deeper, have
a look at “<a href="https://ine.com/blog/2008-07-27-mstp-tutorial-part-i-inside-a-region" title="MSTP Tutorial Part I: Inside a Region"><abbr title="Multiple Spanning Tree Protocol">MSTP</abbr> Tutorial Part I: Inside a Region</a>.”</p>
<h1 id="about-the-interactive-examples">About the interactive examples</h1>
<p>The interactive examples run <a href="https://github.com/mstpd/mstpd" title="Multiple Spanning Tree Protocol Daemon">MSTPD</a> directly in your browser, compiled to
<a href="https://developer.mozilla.org/en-US/docs/WebAssembly" title="WebAssembly on MDN">WebAssembly</a> with <a href="https://emscripten.org/" title="Emscripten documentation">emscripten</a>. A C API replaces the code talking to the
Linux kernel: it manages bridges and ports, exports state as JSON, and drives
time deterministically. A JavaScript wrapper makes it more user-friendly:</p>
<div class="language-javascript codehilite"><pre><span/><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">loadMSTPD</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"./dist/mstpd.mjs"</span><span class="p">;</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">mstp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">loadMSTPD</span><span class="p">();</span>

<span class="c1">// Create 3 bridges</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">mstp</span><span class="p">.</span><span class="nx">createBridge</span><span class="p">(</span><span class="s2">"A"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">priority</span><span class="o">:</span><span class="w"> </span><span class="mf">4096</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">mstp</span><span class="p">.</span><span class="nx">createBridge</span><span class="p">(</span><span class="s2">"B"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">priority</span><span class="o">:</span><span class="w"> </span><span class="mf">8192</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">c</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">mstp</span><span class="p">.</span><span class="nx">createBridge</span><span class="p">(</span><span class="s2">"C"</span><span class="p">);</span>

<span class="c1">// Each bridge has two ports</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">a1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">a</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"a-b"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">a2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">a</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"a-c"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">b1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">b</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"b-a"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">b2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">b</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"b-c"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">c1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"c-a"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">c2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"c-b"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span><span class="w"> </span><span class="p">});</span>

<span class="c1">// Build a triangle topology</span>
<span class="nx">mstp</span><span class="p">.</span><span class="nx">link</span><span class="p">(</span><span class="nx">a1</span><span class="p">,</span><span class="w"> </span><span class="nx">b1</span><span class="p">);</span>
<span class="nx">mstp</span><span class="p">.</span><span class="nx">link</span><span class="p">(</span><span class="nx">a2</span><span class="p">,</span><span class="w"> </span><span class="nx">c1</span><span class="p">);</span>
<span class="nx">mstp</span><span class="p">.</span><span class="nx">link</span><span class="p">(</span><span class="nx">b2</span><span class="p">,</span><span class="w"> </span><span class="nx">c2</span><span class="p">);</span>

<span class="c1">// Enable all bridges and ports</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">br</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="p">[</span><span class="nx">a</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="p">,</span><span class="w"> </span><span class="nx">c</span><span class="p">])</span><span class="w"> </span><span class="nx">br</span><span class="p">.</span><span class="nx">enable</span><span class="p">();</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">p</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="p">[</span><span class="nx">a1</span><span class="p">,</span><span class="w"> </span><span class="nx">a2</span><span class="p">,</span><span class="w"> </span><span class="nx">b1</span><span class="p">,</span><span class="w"> </span><span class="nx">b2</span><span class="p">,</span><span class="w"> </span><span class="nx">c1</span><span class="p">,</span><span class="w"> </span><span class="nx">c2</span><span class="p">])</span><span class="w"> </span><span class="nx">p</span><span class="p">.</span><span class="nx">enable</span><span class="p">();</span>

<span class="c1">// Execute 40 seconds' worth of wall clock and display the topology</span>
<span class="nx">mstp</span><span class="p">.</span><span class="nx">step</span><span class="p">(</span><span class="mf">40</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Topology:"</span><span class="p">,</span><span class="w"> </span><span class="nx">mstp</span><span class="p">.</span><span class="nx">topology</span><span class="p">());</span>
</code></pre></div>

<p>Several dozen unit tests explore the features of MSTPD and check that they work
correctly in this environment:</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>node<span class="w"> </span>--test<span class="w"> </span>*.test.mjs
<span class="go">✔ two bridges: lower priority becomes root (41.657342ms)</span>
<span class="go">✔ triangle loop: exactly one port blocks and all agree on the root (5.832ms)</span>
<span class="go">✔ breaking the active link reconverges and restoring recovers (18.730753ms)</span>
<span class="go">[…]</span>
<span class="go">ℹ tests 40</span>
<span class="go">ℹ pass 40</span>
<span class="go">ℹ fail 0</span>
<span class="go">[…]</span>
<span class="go">ℹ duration_ms 396.190897</span>
</code></pre></div>

<p>Additional JavaScript code looks for specific <code>&lt;pre&gt;</code> blocks containing a
topology definition and turns them into interactive widgets. You can inspect
and modify the definition by hitting the “edit” button.</p>
<p>There is also a cool trick to tell whether the topology has converged. After
each step, we save a snapshot of the simulation memory, play 50 seconds’ worth
of simulation to check if the topology is stable, and travel back in time by
restoring that snapshot. 🕰️</p>
<p>The complete code lives on <a href="https://github.com/vincentbernat/mstpd/tree/feature/wasm/wasm">GitHub</a>. I am happy with the result. It can be
difficult to follow everything happening during a single step, but stepping
forward and backward helps. I plan to use the same approach in future blog posts
about networking features.</p>
<div class="admonition">
<p class="admonition-title">Note</p>
<p><a href="https://mtlynch.io/">Michael Lynch</a> reviewed a first draft of this article. He authored
“<a href="https://refactoringenglish.com/" title="Refactoring English: Effective Writing for Software Developers">Refactoring English</a>,” a book to sharpen your writing for blog posts,
documentation, commit messages, and tutorials. Any errors are still mine!</p>
</div>
<div class="admonition">
<p class="admonition-title">Update (2026-10)</p>
<p>This post was briefly discussed on <a href="https://news.ycombinator.com/item?id=49610596">Hacker News</a>.</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:sprites">
<p>The sprites for Stan and Blobby come from <a href="https://craftpix.net/" title="Craftpix: 2D game assets">Craftpix</a>, the coffee
cups from <a href="https://yaninyunus.itch.io/16x16-specialtycoffee" title="Yanin: 16x16 specialty coffee assets">Yanin</a>. <a class="footnote-backref" href="#fnref:sprites" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:history">
<p><abbr title="Spanning Tree Protocol">STP</abbr> was introduced in <a href="https://sci-hub.fr/10.1109/IEEESTD.1991.101050" title="IEEE Standards for Local and Metropolitan Area Networks: Media Access Control (MAC) Bridges">IEEE 802.1D-1990</a>. It is still present in
<a href="https://sci-hub.fr/10.1109/IEEESTD.1998.95619" title="IEEE Standard for Local and Metropolitan Area Networks: Media Access Control (MAC) Bridges">IEEE 802.1D-1998</a> but was withdrawn in <a href="https://sci-hub.fr/10.1109/IEEESTD.2004.94569" title="IEEE Standard for Local and Metropolitan Area Networks: Media Access Control (MAC) Bridges">IEEE 802.1D-2004</a> in favor of
<abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>, introduced in <a href="https://sci-hub.fr/10.1109/IEEESTD.2001.93287" title="IEEE Standard for Local and Metropolitan Area Networks: Rapid Reconfiguration of Spanning Tree">IEEE 802.1w-2001</a>. <abbr title="Multiple Spanning Tree Protocol">MSTP</abbr> was introduced in <a href="https://sci-hub.fr/10.1109/IEEESTD.2002.94223" title="IEEE Standards for Local and Metropolitan Area Networks: Virtual Bridged Local Area Networks — Amendment: Multiple Spanning Trees">IEEE 802.1s-2002</a> and merged into <a href="https://sci-hub.fr/10.1109/IEEESTD.2003.94280" title="IEEE Standards for Local and Metropolitan Area Networks: Virtual Bridged Local Area Networks">IEEE 802.1Q-2003</a>. Both of them are part
of <a href="ahttps://oadoi.org/10.1109/IEEESTD.2022.10004498" title="IEEE Standard for Local and Metropolitan Area Networks: Bridges and Bridged Networks">IEEE 802.1Q-2022</a> along with <a href="https://sci-hub.fr/10.1109/IEEESTD.2012.6231597" title="IEEE Standard for Local and Metropolitan Area Networks: Shortest Path Bridging"><abbr title="Shortest Path Bridging">SPB</abbr></a><del>—a protocol
I had never heard of until writing this article</del>. <a class="footnote-backref" href="#fnref:history" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:bridge">
<p>From here, I use “bridge” instead of the more common word “switch.” <a class="footnote-backref" href="#fnref:bridge" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:kernel">
<p>The Linux kernel only runs <abbr title="Spanning Tree Protocol">STP</abbr>. It delegates the other protocols to
user space. <a class="footnote-backref" href="#fnref:kernel" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:incomplete">
<p>MSTPD implements the state machine from <a href="https://sci-hub.fr/10.1109/IEEESTD.2006.216285" title="IEEE Standard for Local and Metropolitan Area Networks: Virtual Bridged Local Area Networks">IEEE 802.1Q-2005</a>, but
on Linux it runs <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> only. Linux 5.18 added <a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=ec7328b59176227216c461601c6bd0e922232a9b" title="net: bridge: mst: Multiple Spanning Tree (MST) mode">support for forwarding
multiple spanning tree</a>, but MSTPD does not use it yet. See <a href="https://github.com/mstpd/mstpd/pull/150" title="[RFC/RFT] use kernel MST support if available">PR #150</a>
for progress on this front. <a class="footnote-backref" href="#fnref:incomplete" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:priority">
<p>The priority is a multiple of 4,096: with <abbr title="Multiple Spanning Tree Protocol">MSTP</abbr>, the lower 12 bits
of the bridge priority encode the <abbr title="Multiple Spanning Tree">MST</abbr> instance identifier, leaving only the
upper 4 bits for the configured priority. <a class="footnote-backref" href="#fnref:priority" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:wireshark">
<p>To inspect the <abbr title="Bridge Protocol Data Units">BPDUs</abbr> crossing a link, select it, click the
“Download packets” button, and open the file with <a href="https://www.wireshark.org/" title="Wireshark: network protocol analyzer">Wireshark</a>. <a class="footnote-backref" href="#fnref:wireshark" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:backup">
<p>A backup port only exists if the bridge has several ports on the same
collision domain. This should not happen in a switched network. <a class="footnote-backref" href="#fnref:backup" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:hello">
<p>This is the value of the “hello” timer. It used to be configurable,
but <a href="https://sci-hub.fr/10.1109/IEEESTD.2006.216285" title="IEEE Standard for Local and Metropolitan Area Networks: Virtual Bridged Local Area Networks">IEEE 802.1Q-2005</a> pins it to 2. MSTPD does not allow another value. <a class="footnote-backref" href="#fnref:hello" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:learning">
<p>If the peer port does not receive an agreement after the hello
timer elapses—or the maximum age if the port has just come up—it falls back
to the timer-based method for compatibility with <abbr title="Spanning Tree Protocol">STP</abbr>: it transitions to
the learning state, waits again for the hello timer to expire, and
transitions to the forwarding state. <a class="footnote-backref" href="#fnref:learning" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
<li id="fn:agreement">
<p>As in many proposals, S3 also sets the agreement bit to 1. The
proposal bit says “I am the designated port on this link and I want to
transition to the forwarding state.” The agreement bit says “I am already in
sync with the rest of my bridge on this root information.” Both can be true. <a class="footnote-backref" href="#fnref:agreement" title="Jump back to footnote 11 in the text">↩</a></p>
</li>
<li id="fn:bum">
<p>If it finds no entry, the bridge duplicates the Ethernet frame on all
ports, except the incoming one. The same happens if the destination MAC
address is the broadcast one (<code>ff:ff:ff:ff:ff:ff</code>). This behavior bootstraps
the learning process. <a class="footnote-backref" href="#fnref:bum" title="Jump back to footnote 12 in the text">↩</a></p>
</li>
<li id="fn:timer">
<p>This timer makes <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> resistant to packet loss. <a class="footnote-backref" href="#fnref:timer" title="Jump back to footnote 13 in the text">↩</a></p>
</li>
<li id="fn:price">
<p>You can get them for less than US$100 through a broker. All the ports
run <abbr title="Per-VLAN Spanning Tree">PVST</abbr>+ by default and automatically fall back to plain <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>. <a class="footnote-backref" href="#fnref:price" title="Jump back to footnote 14 in the text">↩</a></p>
</li>
<li id="fn:erps">
<p>An alternative would be <a href="https://www.itu.int/rec/T-REC-G.8032" title="ITU-T G.8032">Ethernet Ring Protection Switching</a> (<abbr title="Ethernet Ring Protection Switching">ERPS</abbr>)—another protocol I had never heard of until researching this
article. <a class="footnote-backref" href="#fnref:erps" title="Jump back to footnote 15 in the text">↩</a></p>
</li>
<li id="fn:root-mac">
<p>If you look closely at what happens at t=2s, you can see that R2 is
gaining popularity as root: S17 to S36 believe R2 is the root bridge. S16
does not follow because we hit the maximum age. Later, S17 to S20 reverse
their position. I’ll let you explore the state of the various bridges to
understand the root cause. <a class="footnote-backref" href="#fnref:root-mac" title="Jump back to footnote 16 in the text">↩</a></p>
</li>
<li id="fn:forward-delay">
<p>When increasing the maximum age to 40, you also need to
increase the forward delay to 21 (<code>:forward-delay 21</code>), as the standard
enforces this condition: 2 × (Forward Delay − 1) ≥ Max Age. For this
specific topology, you could also increase the maximum age to 37 and
forward-delay to 20. <a class="footnote-backref" href="#fnref:forward-delay" title="Jump back to footnote 17 in the text">↩</a></p>
</li>
<li id="fn:time">
<p>The simulation may seem slow, but it does not run in real time. Look at
the current timestamp in the upper right corner to know the wall clock, e.g.
“t=8s.” Once the topology stabilizes, the same corner shows the convergence
time, e.g. “🌳 2s.” <a class="footnote-backref" href="#fnref:time" title="Jump back to footnote 18 in the text">↩</a></p>
</li>
<li id="fn:slow">
<p>Khaled Elmeleegy, Alan Cox, and Eugene Ng formalized this phenomenon in
“<a href="https://sci-hub.fr/10.1109/INFOCOM.2006.229" title="On Count-to-Infinity Induced Forwarding Loops in Ethernet Networks">On Count-to-Infinity Induced Forwarding Loops in Ethernet Networks</a>”
and later in “<a href="https://sci-hub.fr/10.1109/TNET.2008.920874" title="Understanding and Mitigating the Effects of Count to Infinity in Ethernet Networks">Understanding and Mitigating the Effects of Count to Infinity
in Ethernet Networks</a>.” They propose a fix that did not find its way into
a standard. <a class="footnote-backref" href="#fnref:slow" title="Jump back to footnote 19 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">A non-interactive tour of the spanning tree protocol</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-spanning-tree-video" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-spanning-tree-video#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-08-24T14:59:00Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-spanning-tree-video.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Imagine you rent office space for a three-day event. You quickly set up a few
Ethernet switches and tape some cables on the floor to get everyone online.
Unfortunately, Stan, your clumsiest coworker, kicks out a cable every time he
gets up for coffee. Spare cables would fix that, but a loop turns into a
broadcast storm: Ethernet packets multiply until nothing else gets through.
That’s where the <em>spanning tree protocol</em> comes in: it blocks just enough of the
spare cables to leave a loop-free tree, and rebuilds it in a second each time
Stan strikes again.<sup id="fnref:credits"><a class="footnote-ref" href="#fn:credits">1</a></sup></p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1920px"><span class="lf-media-inner" style="padding-bottom: 56.250%"><video width="1920" height="1080" controls="" preload="none" crossorigin="anonymous" poster="https://media-bernat-ch.b-cdn.net/images/posters/2026-spanning-tree.f0cfb7dd350fbe.jpg" class="lf-media lf-opaque"><source src="https://media.bernat.ch/videos/2026-spanning-tree.m3u8" type="application/vnd.apple.mpegurl"/><source src="https://media.bernat.ch/videos/2026-spanning-tree/progressive.mp4" type="video/mp4; codecs=&quot;mp4a.40.2,avc1.4d401f&quot;"/><track src="https://media.bernat.ch/videos/2026-spanning-tree.chapters.vtt" kind="chapters" label="Chapters"/><track src="https://media.bernat.ch/videos/2026-spanning-tree.en.vtt" kind="subtitles" srclang="en" default="" label="English"/><track src="https://media.bernat.ch/videos/2026-spanning-tree.fr.vtt" kind="subtitles" srclang="fr" label="français"/></video></span></div></figure>
<p>This content is also available as a <a href="/en/blog/2026-spanning-tree" title="An interactive tour of the spanning tree protocol">text version</a>, with interactive demos
that run a real implementation directly in your browser!</p>
<hr/>
<p>This video is an experiment.<sup id="fnref:time"><a class="footnote-ref" href="#fn:time">2</a></sup> Honestly, except for Radia Perlman <a href="#video:seek-130">reading
her poem</a>,<sup id="fnref:radia"><a class="footnote-ref" href="#fn:radia">3</a></sup> you should read the <a href="/en/blog/2026-spanning-tree" title="An interactive tour of the spanning tree protocol">original article</a> instead. It presents the same content, but you can play with the
interactive examples, which are the main contribution. On the other hand, if you
happen to like the video, be sure to tell me in the comments!</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:credits">
<p>The sprites for Stan and Blobby come from <a href="https://craftpix.net/" title="Craftpix: 2D game assets">Craftpix</a>. The
background music is “<a href="https://www.chosic.com/download-audio/25044/" title="Sonatina No. 2 In G Major – III. Allegro by Aaron Dunn">Sonatina No. 2 in G Major – III. Allegro</a>” by
Aaron Dunn. <a class="footnote-backref" href="#fnref:credits" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:time">
<p>I thought automated tools would produce this video in a couple of
hours. In the end, it was another rabbit hole and it took me more than 12. <a class="footnote-backref" href="#fnref:time" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:radia">
<p>The audio was extracted from a <a href="https://www.youtube.com/watch?v=jOqzLcM2Hbo">Youtube video</a> and cleaned up. <a class="footnote-backref" href="#fnref:radia" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Sharding a routing table for lock-free reads in Go</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-akvorado-rib-sharding" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-akvorado-rib-sharding#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-05-24T19:00:00Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-akvorado-rib-sharding.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>To associate routing information—like AS paths or <abbr title="Border Gateway Protocol">BGP</abbr> communities—to flows,
<a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a> can import routes through the <a href="https://www.rfc-editor.org/rfc/rfc7854" title="RFC 7854: BGP Monitoring Protocol (BMP)"><abbr title="Border Gateway Protocol">BGP</abbr> Monitoring Protocol</a> (<abbr title="BGP Monitoring Protocol">BMP</abbr>). As
the Internet routing table contains more than <a href="https://bgp.potaroo.net" title="BGP Routing Table Analysis Reports">1 million routes</a>, Akvorado
needs to <strong>scale to tens of millions of routes</strong>.<sup id="fnref:optimize"><a class="footnote-ref" href="#fn:optimize">1</a></sup> This has been a
long-standing challenge,<sup id="fnref:past"><a class="footnote-ref" href="#fn:past">2</a></sup> but I expect this issue is now fixed by using
<strong><abbr title="Routing Information Base">RIB</abbr> sharding</strong>, a method that splits the routing database into several parts
to enable concurrent updates.</p>
<div class="toc">
<ul>
<li><a href="#previous-implementation">Previous implementation</a><ul>
<li><a href="#storing-routes-in-a-map">Storing routes in a map</a></li>
<li><a href="#interning-routes">Interning routes</a></li>
<li><a href="#why-does-it-not-scale">Why does it not scale?</a></li>
</ul>
</li>
<li><a href="#rib-sharding">RIB sharding</a><ul>
<li><a href="#first-step-basic-sharding">First step: basic sharding</a></li>
<li><a href="#second-step-lock-free-reads">Second step: lock-free reads</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="previous-implementation">Previous implementation</h1>
<p>Akvorado connects 2 elements to build its <abbr title="Routing Information Base">RIB</abbr>:</p>
<ol>
<li>a <strong>prefix tree</strong>, and</li>
<li>a <strong>list of routes</strong> attached to each prefix.</li>
</ol>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 711px"><span class="lf-media-inner" style="padding-bottom: 105.626%"><img alt="Akvorado BMP RIB implementation before sharding with the memory layout of each&#10;structure and a single lock." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-before.b60946a339a187.svg" width="711" height="751" class="lf-media"/></span></div><figcaption>Akvorado BMP RIB implementation without sharding. One single read/write lock.</figcaption></figure>
<p>In the diagram above, the <abbr title="Routing Information Base">RIB</abbr> stores five IPv4 prefixes and two IPv6 prefixes.
One of them, <code>2001:db8:1::/48</code>, contains three routes:</p>
<ul>
<li>from peer 3, next hop <code>2001:db8::3:1</code>, AS 65402, AS path <code>65402</code>, community
  <code>65402:31</code>,</li>
<li>from peer 4, next hop <code>2001:db8::4:1</code>, same <abbr title="Autonomous System Number">ASN</abbr>, AS path, and community,</li>
<li>from peer 5, next hop <code>2001:db8::5:1</code>, AS 65402, AS path <code>65401 65402</code>,
  community <code>65402:31</code>.</li>
</ul>
<p>The <code>rib</code> structure is defined in Go as follows:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">rib</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">tree</span><span class="w">          </span><span class="o">*</span><span class="nx">bart</span><span class="p">.</span><span class="nx">Table</span><span class="p">[</span><span class="nx">prefixIndex</span><span class="p">]</span>
<span class="w">    </span><span class="nx">routes</span><span class="w">        </span><span class="kt">map</span><span class="p">[</span><span class="nx">routeKey</span><span class="p">]</span><span class="nx">route</span>
<span class="w">    </span><span class="nx">nlris</span><span class="w">         </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">nlri</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextHops</span><span class="w">      </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">nextHop</span><span class="p">]</span>
<span class="w">    </span><span class="nx">rtas</span><span class="w">          </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">routeAttributes</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextPrefixID</span><span class="w">  </span><span class="nx">prefixIndex</span>
<span class="w">    </span><span class="nx">freePrefixIDs</span><span class="w"> </span><span class="p">[]</span><span class="nx">prefixIndex</span>
<span class="p">}</span>
</code></pre></div>

<p>The prefix tree uses the <a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a> package, an adaptation of Donald Knuth’s <a href="https://www.hariguchi.org/art/art.pdf" title="ART: A Fast Free Multibit Trie Based Routing Table">ART
algorithm</a>. The <a href="https://github.com/gaissmai/iprbench">benchmarks</a> demonstrate it outperforms other packages for
lookups, insertions, and memory usage.<sup id="fnref:performance"><a class="footnote-ref" href="#fn:performance">3</a></sup> Plus, the author is quite
helpful.</p>
<h2 id="storing-routes-in-a-map">Storing routes in a map</h2>
<p>The list of routes for each prefix is not stored directly in the prefix tree:
it would put too much pressure on the garbage collector by allocating per-prefix
arrays.</p>
<p>Instead, the <abbr title="Routing Information Base">RIB</abbr> assigns a unique 32-bit prefix identifier for each prefix,
either by picking the last available prefix identifier from the <code>freePrefixIDs</code>
array if any, or using the <code>nextPrefixID</code> value before incrementing it. Then,
the routes are stored in the <code>routes</code> map, leveraging the <a href="https://go.dev/blog/swisstable" title="Faster Go maps with Swiss Tables">optimized Swiss
table</a> in Go. To retrieve routes attached to a prefix, we look them up
one by one in the <code>routes</code> map with a 64-bit key combining the 32-bit prefix
index with a 32-bit route index matching the position of the route in the list.
Akvorado scans routes from the first to the last to find the best one.<sup id="fnref:scan"><a class="footnote-ref" href="#fn:scan">4</a></sup> It
knows there is no more route if the route key returns no result.</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">prefixIndex</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeIndex</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeKey</span><span class="w"> </span><span class="kt">uint64</span>
</code></pre></div>

<h2 id="interning-routes">Interning routes</h2>
<p>A route contains a <abbr title="Border Gateway Protocol">BGP</abbr> peer identifier, a partial <abbr title="Network Layer Reachability Information">NLRI</abbr>,<sup id="fnref:nlri"><a class="footnote-ref" href="#fn:nlri">5</a></sup> the next hop, and the
attributes.</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">route</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">peer</span><span class="w">       </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">nlri</span><span class="w">       </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">nlri</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextHop</span><span class="w">    </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">nextHop</span><span class="p">]</span>
<span class="w">    </span><span class="nx">attributes</span><span class="w"> </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">routeAttributes</span><span class="p">]</span>
<span class="w">    </span><span class="nx">prefixLen</span><span class="w">  </span><span class="kt">uint8</span>
<span class="p">}</span>

<span class="kd">type</span><span class="w"> </span><span class="nx">nlri</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">family</span><span class="w"> </span><span class="nx">bgp</span><span class="p">.</span><span class="nx">Family</span>
<span class="w">    </span><span class="nx">path</span><span class="w">   </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">rd</span><span class="w">     </span><span class="nx">RD</span>
<span class="p">}</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">nextHop</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeAttributes</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">asn</span><span class="w">              </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">asPath</span><span class="w">           </span><span class="p">[]</span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">communities</span><span class="w">      </span><span class="p">[]</span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">largeCommunities</span><span class="w"> </span><span class="p">[]</span><span class="nx">bgp</span><span class="p">.</span><span class="nx">LargeCommunity</span>
<span class="p">}</span>
</code></pre></div>

<p>To save memory and allocations, <abbr title="Network Layer Reachability Information">NLRI</abbr>, next hops, and route attributes are
“interned”: a 32-bit integer replaces the real value. The mechanism predates the
<a href="https://go.dev/blog/unique" title="New unique package"><code>unique</code> package</a> introduced in Go 1.23. We keep it because it has
different trade-offs:</p>
<ul>
<li>It uses <strong>explicit reference counting</strong> instead of relying on weak pointers.</li>
<li>It works with <strong>non-comparable values</strong> implementing <code>Hash()</code> and <code>Equal()</code>
  methods.<sup id="fnref:hash"><a class="footnote-ref" href="#fn:hash">6</a></sup></li>
<li>It uses <strong>explicit pool instances</strong>. This will be useful for sharding.</li>
<li>It has <strong>better performance</strong>. See for example this <a href="https://github.com/akvorado/akvorado/pull/2244/changes/682b6063af50780dcd64e46b39d5e66c5074d9ab" title="outlet/routing: use Go's unique instead of intern">benchmark</a>.</li>
<li>It consumes <strong>half the memory</strong> thanks to unsigned 32-bit references instead
  of pointers.</li>
<li>But it is <strong>not safe for concurrent use</strong>.</li>
</ul>
<h2 id="why-does-it-not-scale">Why does it not scale?</h2>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>At <a href="https://www.free.fr/freebox">AS 12322</a>, we don’t use <abbr title="BGP Monitoring Protocol">BMP</abbr> yet.<sup id="fnref:cisco"><a class="footnote-ref" href="#fn:cisco">7</a></sup> But <a href="https://github.com/bogi788" title="bogi788 on GitHub">Gerhard
Bogner</a> had the patience, availability, and technical skills to help me <a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">debug
this issue</a>.</p>
</div>
<p>The global read/write lock is a bottleneck in this implementation. But how?
There are several users of the <abbr title="Routing Information Base">RIB</abbr>, each with its own set of constraints:</p>
<ul>
<li>
<p>The <strong>Kafka workers</strong> look up the <abbr title="Routing Information Base">RIB</abbr> to enrich flows with routing
  information. They are bound by the number of Kafka partitions.<sup id="fnref:KIP-932"><a class="footnote-ref" href="#fn:KIP-932">8</a></sup>
  Akvorado also adjusts their number to ensure efficient batching to ClickHouse.
  On our setup, the number of workers oscillates between 8 and 16. As we want
  to observe the latest data, we cannot afford for the Kafka workers to lag too
  much.</p>
</li>
<li>
<p>The <strong>monitored routers</strong> send route updates through the <abbr title="BGP Monitoring Protocol">BMP</abbr> protocol. When
  connecting, they can send millions of routes.<sup id="fnref:bmpconfig"><a class="footnote-ref" href="#fn:bmpconfig">9</a></sup> After the initial
  synchronization, updates are sent continuously and may spike from time to
  time. The router detects a stuck <abbr title="BGP Monitoring Protocol">BMP</abbr> station when its TCP window is full and
  resets the session in this case. While Akvorado implements a large incoming
  buffer, it still needs to update the received routes with the write lock held
  fast enough to avoid being detected as stuck.</p>
</li>
<li>
<p>When a <strong>remote <abbr title="Border Gateway Protocol">BGP</abbr> peer goes down</strong>, Akvorado flushes the associated routes by
  walking the <abbr title="Routing Information Base">RIB</abbr> with the write lock held. When a <strong>monitored router goes
  down</strong>, Akvorado waits a bit but eventually flushes all the associated routes.</p>
</li>
</ul>
<p>In short: on a busy setup, lock contention is high for both readers and
writers, and neither can lag too much behind.</p>
<h1 id="rib-sharding"><abbr title="Routing Information Base">RIB</abbr> sharding</h1>
<h2 id="first-step-basic-sharding">First step: basic sharding</h2>
<p>To remove the global lock, the <abbr title="Routing Information Base">RIB</abbr> is split into several “shards,” each one
handling a subset of the prefixes:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 876px"><span class="lf-media-inner" style="padding-bottom: 72.603%"><img alt="Akvorado BMP RIB implementation after sharding with the memory layout of each&#10;structure and one lock per shard." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-step1.71c5ecf8b46b57.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Akvorado BMP RIB implementation with sharding.</figcaption></figure>
<p>The prefix tree stays global and is protected by a single lock. Each shard gets
its read/write lock, its route map, and its intern pools to store NLRIs, next
hops, and route attributes, which would not have been possible with <a href="https://go.dev/blog/unique" title="New unique package">Go’s
<code>unique</code> package</a>. The prefix indexes are also sharded: the 8 most
significant bits are the shard index and the 24 remaining bits are the local
prefix index.</p>
<p>Gerhard <a href="https://github.com/akvorado/akvorado/discussions/2287#discussioncomment-16020731">confirmed</a> that after <a href="https://github.com/akvorado/akvorado/commit/7e6bbf2210fdf7116d2ee168b307b9906cc223c0" title="outlet/routing: implement RIB sharding for BMP">this blind change</a>,
the <abbr title="BGP Monitoring Protocol">BMP</abbr> receiver chugged steadily. 🎉</p>
<p>Later, I wrote a <a href="https://github.com/akvorado/akvorado/blob/0811c40cc2065380e3a6230c2796312838e57850/outlet/routing/provider/bmp/concurrent_test.go">concurrent benchmark</a> over half a million synthetic but
plausible routes<sup id="fnref:plausible"><a class="footnote-ref" href="#fn:plausible">10</a></sup> partitioned over 0 to 8 writers, churning routes as
fast as possible, while 1 to 16 readers continuously look up a set of 10,000
routes. I don’t know if this benchmark is realistic, but it confirms the
improvements for both read and write latencies:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 970px"><span class="lf-media-inner" style="padding-bottom: 40.928%"><img alt="Two heatmaps. One for read latency ratio, the other for write latency ratio.&#10;Both of them comparing the speedup with colored tiles between the code before&#10;sharding and after sharding. Most tiles are&#10;green." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-heatmap.0ed40d587fea78.svg" width="970" height="397" class="lf-media"/></span></div><figcaption>Read and write latency performance improvement after sharding.</figcaption></figure>
<p>It also shows that a high number of writers degrades read latency.</p>
<h2 id="second-step-lock-free-reads">Second step: lock-free reads</h2>
<p>The single read/write lock protecting the prefix tree is the next target. The
<a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a> package provides alternative mutation methods returning an updated tree
using copy-on-write. Readers don’t need the global lock any more, leaving it
only to synchronize writers. The prefix tree is boxed in an atomic pointer.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 876px"><span class="lf-media-inner" style="padding-bottom: 72.603%"><img alt="Akvorado BMP RIB implementation for sharding with lock-free reads. It shows&#10;the memory layout of each structure." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-step2.2996a7f0868135.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Akvorado BMP RIB implementation with sharding and lock-free reads.</figcaption></figure>
<p>Without a lock, readers can now fetch a stale prefix index when walking their
copy of the tree if a concurrent writer removes the last route attached to this
prefix index and recycles it for another prefix. To avoid this issue, we combine
the prefix index with a generation number and store them in the tree:</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">generation</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">prefixRef</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">idx</span><span class="w"> </span><span class="nx">prefixIndex</span>
<span class="w">    </span><span class="nx">gen</span><span class="w"> </span><span class="nx">generation</span>
<span class="p">}</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">rib</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">mu</span><span class="w">     </span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span>
<span class="w">    </span><span class="nx">tree</span><span class="w">   </span><span class="nx">atomic</span><span class="p">.</span><span class="nx">Pointer</span><span class="p">[</span><span class="nx">bart</span><span class="p">.</span><span class="nx">Table</span><span class="p">[</span><span class="nx">prefixRef</span><span class="p">]]</span>
<span class="w">    </span><span class="nx">shards</span><span class="w"> </span><span class="p">[]</span><span class="o">*</span><span class="nx">ribShard</span>
<span class="p">}</span>
</code></pre></div>

<p>Each shard stores the generation number for each local prefix index. The
generation number increases by one if the associated prefix index is freed. When
looking up the routes attached to a prefix index, the reader checks if the
generation number matches. Otherwise, it assumes the index was recycled and the
list of routes is empty.<sup id="fnref:retry"><a class="footnote-ref" href="#fn:retry">11</a></sup> You can see this case in the diagram above for
prefix index 5, stored with a generation index of 3, while the current value in
the <code>[]generations</code> array is 4. The generation number could overflow, but it is
not a problem as lookups are quick.</p>
<p>Running the concurrent benchmark against this new implementation shows the
improvements for the read latency as soon as the cost of the copy-on-write
prefix tree is amortized.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1256px"><span class="lf-media-inner" style="padding-bottom: 46.815%"><img alt="Six heatmaps. Three for read latency ratio, three others for write latency&#10;ratio. They compare the numbers without sharding, with sharding, and with&#10;lock-free reads, pair by pair. For read latency, most tiles are green, showing&#10;an improvement of the second step. For write latency, the speedup is negative&#10;for a low number of readers." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-heatmap2.8bbce411e680da.svg" width="1256" height="588" class="lf-media"/></span></div><figcaption>Read and write latency performance improvement after lock-free reads. The middle column shows the cumulative improvements of both steps.</figcaption></figure>
<hr/>
<p>Among the multiple attempts to optimize the <abbr title="BGP Monitoring Protocol">BMP</abbr> component, <abbr title="Routing Information Base">RIB</abbr> sharding is one
of the more satisfying. <a href="https://github.com/akvorado/akvorado/releases/tag/v2.2.0">Akvorado 2.2</a> implements the first step.
<a href="https://github.com/akvorado/akvorado/pull/2433" title="More efficient RIB tree for BMP">PR #2433</a>, drafted while writing this blog post, implements the second step
and was released with <a href="https://github.com/akvorado/akvorado/releases/tag/v2.4.0">Akvorado 2.4</a>. 🪓</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:optimize">
<p>Each router exporting flows doesn’t need to send its routes. When
Akvorado does not find a route from a specific device, it falls back to a
route sent by another device. It is up to the operator to decide if this
is a good enough approximation. <a class="footnote-backref" href="#fnref:optimize" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:past">
<p>I made many attempts to scale the <abbr title="BGP Monitoring Protocol">BMP</abbr> component. See for example
<a href="https://github.com/akvorado/akvorado/pull/254" title="inlet/bmp: avoid long lock times when flushing peers">PR #254</a>, <a href="https://github.com/akvorado/akvorado/pull/255" title="“Lockless” RIB design">PR #255</a>, <a href="https://github.com/akvorado/akvorado/pull/278" title="inlet/bmp: new lockless design">PR #278</a>, <a href="https://github.com/akvorado/akvorado/pull/2244" title="outlet/routing: read from RIB without any locks">PR #2244</a>, and <a href="https://github.com/akvorado/akvorado/pull/2245" title="outlet/routing: buffer BMP messages to avoid being flagged as “stuck”">PR #2245</a>.
Despite these efforts, this component remained problematic for some users.
See <a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">discussion #2287</a> as the latest example. <a class="footnote-backref" href="#fnref:past" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:performance">
<p>It keeps improving: <a href="https://github.com/gaissmai/bart/releases/tag/v0.28.0" title="bart release v0.28.0: New FastNode implementation">bart 0.28.0</a> features a new
implementation that trades a bit of memory for greater lookup performance. I
did not test it yet, as I have been preparing this blog post for a couple
of months already. <a class="footnote-backref" href="#fnref:performance" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:scan">
<p>Akvorado prefers the route matching the exact next hop. Otherwise, it
falls back to any other route. This is an approximation. An alternative
would be to have one prefix tree for each <abbr title="Border Gateway Protocol">BGP</abbr> peer but it would require
configuring all routers to export their routes. <a href="http://www.pmacct.net/">pmacct</a>’s <abbr title="BGP Monitoring Protocol">BMP</abbr> daemon
implements this approach. <a class="footnote-backref" href="#fnref:scan" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:nlri">
<p>If we consider the <abbr title="Border Gateway Protocol">BGP</abbr> <abbr title="Routing Information Base">RIB</abbr> as a database, the Network Layer
Reachability Information (<abbr title="Network Layer Reachability Information">NLRI</abbr>) is the primary key. Its content depends on
the <abbr title="Border Gateway Protocol">BGP</abbr> family. With IPv4 or IPv6 unicast, this is the prefix. For VPNv4 and
VPNv6 families, it includes the route distinguisher. If you enable the
<a href="https://www.rfc-editor.org/rfc/rfc7911" title="RFC 7911: Advertisement of Multiple Paths in BGP">ADD-PATH</a> extension, the <abbr title="Network Layer Reachability Information">NLRI</abbr> also contains a path identifier.</p>
<p>In our implementation, we don’t store the prefix as we get it from the
looked-up IP address using the prefix length stored separately. <a class="footnote-backref" href="#fnref:nlri" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:hash">
<p>The <code>Hash()</code> methods rely on the <a href="https://pkg.go.dev/hash/maphash" title="hash/maphash on Go Packages"><code>hash/maphash</code></a> package
and on the <a href="https://pkg.go.dev/unsafe" title="unsafe on Go Packages"><code>unsafe</code></a> package to avoid memory copies. See for
example the <a href="https://github.com/akvorado/akvorado/blob/97fced5e855ac220e96148e132f643dc180cd097/outlet/routing/provider/bmp/rib.go#L89-L96"><code>Hash()</code> function for the <code>nlri</code> structure</a>. <a class="footnote-backref" href="#fnref:hash" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:cisco">
<p>Despite being an author or co-author of the first <abbr title="BGP Monitoring Protocol">BMP</abbr>-related RFCs since
2016 (<a href="https://www.rfc-editor.org/rfc/rfc7854" title="RFC 7854: BGP Monitoring Protocol (BMP)">RFC 7854</a>, <a href="https://www.rfc-editor.org/rfc/rfc8671" title="RFC 8671: Support for Adj-RIB-Out in the BGP Monitoring Protocol (BMP)">RFC 8671</a>, <a href="https://www.rfc-editor.org/rfc/rfc9069" title="RFC 9069: Support for Local RIB in the BGP Monitoring Protocol (BMP)">RFC 9069</a>), Cisco did not implement it
in a usable way in IOS XR until version 24.2.1. We still need to upgrade a
few routers to enable this feature. <a class="footnote-backref" href="#fnref:cisco" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:KIP-932">
<p><a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka" title="KIP-932: Queues for Kafka">KIP-932</a> introduces, in Kafka 4.2, the concept of share groups to
enable cooperative consumption on the same partition. This is not supported
in Akvorado yet. <a class="footnote-backref" href="#fnref:KIP-932" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:bmpconfig">
<p>You can configure <abbr title="BGP Monitoring Protocol">BMP</abbr> to send routes for each <abbr title="Border Gateway Protocol">BGP</abbr> peer before or after
applying the incoming policies. In this case, you can get more than one
million routes for each transit peer. You can also tell <abbr title="BGP Monitoring Protocol">BMP</abbr> to send the
local <abbr title="Routing Information Base">RIB</abbr>, which only contains the best path for each prefix. <a class="footnote-backref" href="#fnref:bmpconfig" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:plausible">
<p>The prefixes are random, but the prefix size distribution and the
AS path length distribution follow the <a href="https://bgp.potaroo.net/as2.0/" title="AS65000 BGP Routing Table Analysis Report">data provided by Geoff Huston</a>. <a class="footnote-backref" href="#fnref:plausible" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
<li id="fn:retry">
<p>Alternatively, we could retry the lookup, but it would be pointless:
the <abbr title="Routing Information Base">RIB</abbr> is an <a href="https://en.wikipedia.org/wiki/Eventual_consistency" title="Eventual consistency on Wikipedia">eventually consistent</a> database, and an empty list was a
correct answer at some point in the recent past. <a class="footnote-backref" href="#fnref:retry" title="Jump back to footnote 11 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Automatic Prometheus metrics discovery with Docker labels</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2026-prometheus-metrics-discovery-docker-labels" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2026-prometheus-metrics-discovery-docker-labels#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-03-05T15:40:24Z</updated>
    <id>http://www.luffy.cx/en/blog/2026-prometheus-metrics-discovery-docker-labels.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="/en/blog/2025-akvorado-2.0" title="Akvorado release 2.0">Akvorado</a>, a network flow collector, relies on <a href="https://traefik.io/traefik" title="Traefik: modern HTTP reverse proxy and load-balancer">Traefik</a>, a reverse HTTP
proxy, to expose HTTP endpoints for its <a href="https://docs.docker.com/compose/" title="Docker Compose documentation">Docker Compose</a> services. <a href="https://docs.docker.com/engine/manage-resources/labels/" title="Docker object labels">Docker
labels</a> attached to each service define the routing rules. Traefik picks them
up automatically when a container starts. Instead of maintaining a static
configuration file to collect <a href="https://prometheus.io/docs/concepts/data_model/" title="Prometheus Data Model">Prometheus metrics</a>, we apply the same approach
with <a href="https://grafana.com/docs/alloy/latest/" title="Grafana Alloy documentation">Grafana Alloy</a>.</p>
<div class="toc">
<ul>
<li><a href="#traefik-docker">Traefik &amp; Docker</a></li>
<li><a href="#metrics-discovery-with-alloy">Metrics discovery with Alloy</a><ul>
<li><a href="#discovering-docker-containers">Discovering Docker containers</a></li>
<li><a href="#relabeling-targets">Relabeling targets</a></li>
<li><a href="#scraping-and-forwarding">Scraping and forwarding</a></li>
</ul>
</li>
<li><a href="#built-in-exporters">Built-in exporters</a></li>
</ul>
</div>
<h1 id="traefik-docker">Traefik &amp; Docker</h1>
<p>Traefik <a href="https://doc.traefik.io/traefik/reference/install-configuration/providers/docker/" title="Traefik: Docker provider">listens for events on the Docker socket</a>. Each service advertises its
configuration through labels. For example, here is the Loki service in Akvorado:</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">services</span><span class="p">:</span>
<span class="w">  </span><span class="nt">loki</span><span class="p">:</span>
<span class="w">    </span><span class="c1"># …</span>
<span class="w">    </span><span class="nt">expose</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">3100/tcp</span>
<span class="w">    </span><span class="nt">labels</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.enable=true</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.http.routers.loki.rule=PathPrefix(`/loki`)</span>
</code></pre></div>

<p>Once the container is healthy, Traefik creates a router forwarding requests
matching <code>/loki</code> to its first exposed port. Colocating Traefik configuration
with the service definition is attractive. How do we achieve the same for
Prometheus metrics?</p>
<h1 id="metrics-discovery-with-alloy">Metrics discovery with Alloy</h1>
<p><a href="https://grafana.com/docs/alloy/latest/" title="Grafana Alloy documentation">Grafana Alloy</a>, a metrics collector that scrapes Prometheus endpoints,
includes a <a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.docker/" title="Alloy: discovery.docker"><code>discovery.docker</code></a> component. Just like Traefik,
it connects to the Docker socket.<sup id="fnref:socket"><a class="footnote-ref" href="#fn:socket">1</a></sup> With a few relabeling rules, we teach
it to use Docker labels to locate and scrape metrics.</p>
<p>We define three labels on each service:</p>
<ul>
<li><code>metrics.enable</code> set to <code>true</code> enables metrics collection,</li>
<li><code>metrics.port</code> specifies the port exposing the Prometheus endpoint, and</li>
<li><code>metrics.path</code> specifies the path to the metrics endpoint.</li>
</ul>
<p>If a service exposes more than one port, <code>metrics.port</code> is mandatory. Otherwise,
it defaults to the only exposed port. The default value for <code>metrics.path</code> is
<code>/metrics</code>. The Loki service from earlier becomes:</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">services</span><span class="p">:</span>
<span class="w">  </span><span class="nt">loki</span><span class="p">:</span>
<span class="w">    </span><span class="c1"># …</span>
<span class="w">    </span><span class="nt">expose</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">3100/tcp</span>
<span class="w">    </span><span class="nt">labels</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.enable=true</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.http.routers.loki.rule=PathPrefix(`/loki`)</span>
<span class="hll"><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">metrics.enable=true</span>
</span><span class="hll"><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">metrics.path=/loki/metrics</span>
</span></code></pre></div>

<p>Alloy’s configuration is split into four parts:</p>
<ol>
<li><strong>discover</strong> containers through the Docker socket,</li>
<li><strong>filter and relabel</strong> targets using Docker labels,</li>
<li><strong>scrape</strong> the matching endpoints, and</li>
<li><strong>forward</strong> the metrics to Prometheus.</li>
</ol>
<h2 id="discovering-docker-containers">Discovering Docker containers</h2>
<p>The first building block discovers running containers:</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">discovery.docker</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">host</span><span class="w">             </span><span class="o">=</span><span class="w"> </span><span class="s2">"unix:///var/run/docker.sock"</span>
<span class="w">  </span><span class="na">refresh_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="w">  </span><span class="nb">filter</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">name</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"label"</span>
<span class="w">    </span><span class="na">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"com.docker.compose.project=akvorado"</span><span class="p">]</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p>This connects to the Docker socket and lists containers every 30
seconds.<sup id="fnref:events"><a class="footnote-ref" href="#fn:events">2</a></sup> The <code>filter</code> block restricts discovery to containers belonging
to the <code>akvorado</code> project, avoiding interference with unrelated containers on
the same host. For each discovered container, Alloy produces a target with
labels such as <code>__meta_docker_container_label_metrics_port</code> for the
<code>metrics.port</code> Docker label.</p>
<h2 id="relabeling-targets">Relabeling targets</h2>
<p>The relabeling step filters and transforms raw targets from Docker discovery
into scrape targets. The first stage keeps only targets with <code>metrics.enable</code>
set to <code>true</code>:</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">discovery.relabel</span><span class="w"> </span><span class="s2">"prometheus"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.docker.docker.targets</span>

<span class="c1">  // Keep only targets with metrics.enable=true</span>
<span class="w">  </span><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_metrics_enable"</span><span class="p">]</span>
<span class="w">    </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="no">true</span><span class="err">`</span>
<span class="w">    </span><span class="na">action</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="s2">"keep"</span>
<span class="w">  </span><span class="p">}</span>

<span class="c1">  // …</span>
<span class="p">}</span>
</code></pre></div>

<p>The second stage overrides the discovered port when the service defines
<code>metrics.port</code>:</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="c1">// When metrics.port is set, override __address__.</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__address__", "__meta_docker_container_label_metrics_port"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">):</span><span class="err">\d+;</span><span class="p">(.</span><span class="err">+</span><span class="p">)</span><span class="err">`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__address__"</span>
<span class="w">  </span><span class="na">replacement</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"$1:$2"</span>
<span class="p">}</span>
</code></pre></div>

<p>Next, we handle containers in <code>host</code> network mode. When
<code>__meta_docker_network_name</code> equals <code>host</code>, Alloy rewrites the address to
<code>host.docker.internal</code> instead of <code>localhost</code>:<sup id="fnref:hostdocker"><a class="footnote-ref" href="#fn:hostdocker">3</a></sup></p>
<div class="language-terraform codehilite"><pre><span/><code><span class="c1">// When host networking, override __address__ to host.docker.internal.</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_metrics_port", "__meta_docker_network_name"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">)</span><span class="err">;host`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__address__"</span>
<span class="w">  </span><span class="na">replacement</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"host.docker.internal:$1"</span>
<span class="p">}</span>
</code></pre></div>

<p>The next stage derives the job name from the service name, stripping any
numbered suffix. The instance label is the address without the port:</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_com_docker_compose_service"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">)(?:</span><span class="err">-\d+</span><span class="p">)?</span><span class="err">`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"job"</span>
<span class="p">}</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__address__"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">):</span><span class="err">\d+`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"instance"</span>
<span class="p">}</span>
</code></pre></div>

<p>If a container defines <code>metrics.path</code>, Alloy uses it. Otherwise, it defaults to
<code>/metrics</code>:</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_metrics_path"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">)</span><span class="err">`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__metrics_path__"</span>
<span class="p">}</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__metrics_path__"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="s2">""</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__metrics_path__"</span>
<span class="w">  </span><span class="na">replacement</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"/metrics"</span>
<span class="p">}</span>
</code></pre></div>

<h2 id="scraping-and-forwarding">Scraping and forwarding</h2>
<p>With the targets properly relabeled, scraping and forwarding are
straightforward:</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">prometheus.scrape</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.relabel.prometheus.output</span>
<span class="w">  </span><span class="na">forward_to</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nv">prometheus.remote_write.default.receiver</span><span class="p">]</span>
<span class="w">  </span><span class="na">scrape_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="p">}</span>

<span class="nv">prometheus.remote_write</span><span class="w"> </span><span class="s2">"default"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nb">endpoint</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"http://prometheus:9090/api/v1/write"</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p><code>prometheus.scrape</code> periodically fetches metrics from the discovered targets.
<code>prometheus.remote_write</code> sends them to Prometheus.</p>
<h1 id="built-in-exporters">Built-in exporters</h1>
<p>Some services do not expose a Prometheus endpoint. Redis and Kafka are common
examples. Alloy ships built-in <a href="https://grafana.com/docs/alloy/latest/reference/components/prometheus/" title="Alloy: Prometheus components">Prometheus exporters</a> that
query these services and expose metrics on their behalf.</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">prometheus.exporter.redis</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">redis_addr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"redis:6379"</span>
<span class="p">}</span>
<span class="nv">discovery.relabel</span><span class="w"> </span><span class="s2">"redis"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">prometheus.exporter.redis.docker.targets</span>
<span class="w">  </span><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">target_label</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"job"</span>
<span class="w">    </span><span class="na">replacement</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"redis"</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
<span class="nv">prometheus.scrape</span><span class="w"> </span><span class="s2">"redis"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.relabel.redis.output</span>
<span class="w">  </span><span class="na">forward_to</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nv">prometheus.remote_write.default.receiver</span><span class="p">]</span>
<span class="w">  </span><span class="na">scrape_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="p">}</span>
</code></pre></div>

<p>The same pattern applies to Kafka:</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">prometheus.exporter.kafka</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">kafka_uris</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"kafka:9092"</span><span class="p">]</span>
<span class="p">}</span>
<span class="nv">discovery.relabel</span><span class="w"> </span><span class="s2">"kafka"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">prometheus.exporter.kafka.docker.targets</span>
<span class="w">  </span><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">target_label</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"job"</span>
<span class="w">    </span><span class="na">replacement</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"kafka"</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
<span class="nv">prometheus.scrape</span><span class="w"> </span><span class="s2">"kafka"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.relabel.kafka.output</span>
<span class="w">  </span><span class="na">forward_to</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nv">prometheus.remote_write.default.receiver</span><span class="p">]</span>
<span class="w">  </span><span class="na">scrape_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="p">}</span>
</code></pre></div>

<p>Each exporter is a separate component with its own relabeling and scrape
configuration. We set the <code>job</code> label explicitly since no Docker metadata can
provide it.</p>
<hr/>
<p>With this setup, adding metrics to a new service with a Prometheus endpoint
requires only a few labels in <code>docker-compose.yml</code>, just like adding a Traefik
route. Alloy picks it up automatically. You can apply the same pattern with
another discovery method, like <a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.kubernetes/" title="Alloy: discovery.kubernetes"><code>discovery.kubernetes</code></a>,
<a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.scaleway/" title="Alloy: discovery.scaleway"><code>discovery.scaleway</code></a>, or <a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.http/" title="Alloy: discovery.http"><code>discovery.http</code></a>. 🩺</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:socket">
<p>Both Traefik and Alloy require access to the Docker socket, which
grants root-level access to the host. A <a href="https://github.com/Tecnativa/docker-socket-proxy" title="Docker Socket Proxy: security-enhanced proxy for Docker socket">Docker socket proxy</a> mitigates
this by exposing only the read-only API endpoints needed for discovery. <a class="footnote-backref" href="#fnref:socket" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:events">
<p>Unlike Traefik, which watches for events, Grafana Alloy polls the
container list at regular intervals—a behavior inherited from Prometheus. <a class="footnote-backref" href="#fnref:events" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:hostdocker">
<p>The Alloy service needs <code>extra_hosts:
["host.docker.internal:host-gateway"]</code> in its definition. <a class="footnote-backref" href="#fnref:hostdocker" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Akvorado 2.0</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2025-akvorado-2.0" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2025-akvorado-2.0#isso-thread" rel="replies" type="text/html"/>
    <updated>2025-09-22T06:12:52Z</updated>
    <id>http://www.luffy.cx/en/blog/2025-akvorado-2.0.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="https://github.com/akvorado/akvorado/releases/tag/v2.0.0">Akvorado 2.0</a> was released today! <a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a> collects network flows with
<abbr title="IP Flow Information Export">IPFIX</abbr> and sFlow. It enriches flows and stores them in a ClickHouse database.
Users can browse the data through a web console. This release introduces an
important architectural change and other smaller improvements. Let’s dive in! 🤿</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>git<span class="w"> </span>diff<span class="w"> </span>--shortstat<span class="w"> </span>v1.11.5
<span class="go"> 493 files changed, 25015 insertions(+), 21135 deletions(-)</span>
</code></pre></div>

<div class="toc">
<ul>
<li><a href="#new-outlet-service">New “outlet” service</a></li>
<li><a href="#other-changes">Other changes</a><ul>
<li><a href="#observability">Observability</a></li>
<li><a href="#documentation">Documentation</a></li>
<li><a href="#continuous-integration">Continuous integration</a></li>
<li><a href="#docker">Docker</a></li>
<li><a href="#go">Go</a><ul>
<li><a href="#toolchain">Toolchain</a></li>
<li><a href="#testing">Testing</a></li>
<li><a href="#another-package-for-kafka">Another package for Kafka</a></li>
<li><a href="#improved-routing-table-for-bmp">Improved routing table for BMP</a></li>
<li><a href="#miscellaneous">Miscellaneous</a></li>
</ul>
</li>
<li><a href="#javascript">JavaScript</a></li>
</ul>
</li>
<li><a href="#next-steps">Next steps</a></li>
</ul>
</div>
<h1 id="new-outlet-service">New “outlet” service</h1>
<p>The major change in Akvorado 2.0 is <strong>splitting the inlet service</strong> into two
parts: the <strong>inlet</strong> and the <strong>outlet</strong>. Previously, the inlet handled all flow
processing: receiving, decoding, and enrichment. Flows were then sent to Kafka
for storage in ClickHouse:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 542px"><span class="lf-media-inner" style="padding-bottom: 40.037%"><img alt="Akvorado flow processing before the change: flows are received and processed&#10;by the inlet, sent to Kafka and stored in&#10;ClickHouse" src="https://media-bernat-ch.b-cdn.net/images/akvorado/before-outlet.d3c1c81ca52d28.svg" width="542" height="217" class="lf-media"/></span></div><figcaption>Akvorado flow processing before the introduction of the outlet service</figcaption></figure>
<p>Network flows reach the inlet service using UDP, an unreliable protocol. The
inlet must process them <strong>fast enough to avoid losing packets</strong>. To handle a
high number of flows, the inlet spawns several sets of workers to receive flows,
fetch metadata, and assemble enriched flows for Kafka. Many configuration
options existed for scaling, which increased complexity for users. The code
needed to <strong>avoid blocking at any cost</strong>, making the processing pipeline complex
and sometimes unreliable, particularly the <abbr title="BGP Monitoring Protocol">BMP</abbr> receiver.<sup id="fnref:bmp"><a class="footnote-ref" href="#fn:bmp">1</a></sup> Adding new
features became difficult without making the problem worse.<sup id="fnref:features"><a class="footnote-ref" href="#fn:features">2</a></sup></p>
<p>In Akvorado 2.0, the inlet receives flows and pushes them to Kafka without
decoding them. The new outlet service handles the remaining tasks:</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 732px"><span class="lf-media-inner" style="padding-bottom: 29.645%"><img alt="Akvorado flow processing after the change: flows are received by the inlet,&#10;sent to Kafka, processed by the outlet and inserted in ClickHouse" src="https://media-bernat-ch.b-cdn.net/images/akvorado/after-outlet.9e9a485126fdf3.svg" width="732" height="217" class="lf-media"/></span></div><figcaption>Akvorado flow processing after the introduction of the outlet service</figcaption></figure>
<p>This change goes beyond a simple split:<sup id="fnref:size"><a class="footnote-ref" href="#fn:size">3</a></sup> the outlet now reads flows from
Kafka and pushes them to ClickHouse, two tasks that Akvorado did not handle
before. Flows are heavily <strong>batched to increase efficiency</strong> and reduce the load
on ClickHouse using <a href="https://github.com/ClickHouse/ch-go/" title="Low-level Go Client for ClickHouse">ch-go</a>, a low-level Go client for ClickHouse. When
batches are too small, asynchronous inserts are used (<a href="https://github.com/akvorado/akvorado/commit/e20645c92e38" title="outlet/metadata: synchronous fetching of metadata">e20645</a>). The number of
outlet <strong>workers scales dynamically</strong> (<a href="https://github.com/akvorado/akvorado/commit/e5a625aecf9e" title="outlet: make the number of Kafka workers dynamic">e5a625</a>) based on the target batch
size and latency (50,000 flows and 5 seconds by default).</p>
<p>This new architecture also allows us to <strong>simplify and optimize</strong> the code. The
outlet fetches metadata synchronously (<a href="https://github.com/akvorado/akvorado/commit/e20645c92e38" title="outlet/metadata: synchronous fetching of metadata">e20645</a>). The <abbr title="BGP Monitoring Protocol">BMP</abbr> component becomes
simpler by removing cooperative multitasking (<a href="https://github.com/akvorado/akvorado/commit/3b9486b000c0" title="outlet/routing: remove batching of peer removal">3b9486</a>). Reusing the same
<code>RawFlow</code> object to decode protobuf-encoded flows from Kafka reduces pressure on
the garbage collector (<a href="https://github.com/akvorado/akvorado/commit/8b580fd26bfc" title="outlet/core: reuse the same RawFlow object when decoding">8b580f</a>).</p>
<p>The effect on Akvorado’s overall performance was somewhat uncertain, but a
<a href="https://github.com/akvorado/akvorado/issues/1908#issuecomment-3233143146">user reported</a> <strong>35% lower CPU usage</strong> after migrating from the previous
version, plus resolution of the long-standing <abbr title="BGP Monitoring Protocol">BMP</abbr> component issue. 🥳</p>
<h1 id="other-changes">Other changes</h1>
<p>This new version includes many miscellaneous changes, such as completion for
source and destination ports (<a href="https://github.com/akvorado/akvorado/commit/f92d2ef9e2bd" title="console/filter: add autocomplete for src and dst ports">f92d2e</a>), and automatic restart of the
orchestrator service (<a href="https://github.com/akvorado/akvorado/commit/0f72ffd6761b" title="cmd: automatic restart of orchestrator on configuration change">0f72ff</a>) when configuration changes to avoid a common
pitfall for newcomers.</p>
<p>Let’s focus on some key areas for this release: <a href="#observability">observability</a>,
<a href="#documentation">documentation</a>, <a href="#continuous-integration"><abbr title="Continuous integration">CI</abbr></a>,
<a href="#docker">Docker</a>, <a href="#go">Go</a>, and <a href="#javascript">JavaScript</a>.</p>
<h2 id="observability">Observability</h2>
<p>Akvorado exposes metrics to provide visibility into the processing pipeline and
help troubleshoot issues. These are available through <strong>Prometheus HTTP metrics
endpoints</strong>, such as <a href="https://demo.akvorado.net/api/v0/inlet/metrics"><code>/api/v0/inlet/metrics</code></a>. With the introduction
of the outlet, many metrics moved. Some were also renamed (<a href="https://github.com/akvorado/akvorado/commit/4c0b15e1cd4e" title="inlet/outlet: rename a few metrics">4c0b15</a>) to match
<a href="https://prometheus.io/docs/practices/naming/">Prometheus best practices</a>. Kafka consumer lag was added as a new metric
(<a href="https://github.com/akvorado/akvorado/commit/e3a778552d6e" title="outlet/kafka: expose consumer lag as a prometheus metric">e3a778</a>).</p>
<p>If you do not have your own observability stack, the Docker Compose setup
shipped with Akvorado provides one. You can enable it by activating the profiles
introduced for this purpose (<a href="https://github.com/akvorado/akvorado/commit/529a8f69bebc" title="docker: use profiles to optionally enable Prometheus, Loki and Grafana">529a8f</a>).</p>
<p>The <code>prometheus</code> profile ships <a href="https://prometheus.io/" title="Prometheus: Open source metrics and monitoring">Prometheus</a> to <strong>store metrics</strong> and <a href="https://grafana.com/docs/alloy/latest/" title="Grafana Alloy documentation">Alloy</a>
to <strong>collect them</strong> (<a href="https://github.com/akvorado/akvorado/commit/2b3c46372985" title="docker: switch from prometheus to alloy for scrapping metrics">2b3c46</a>, <a href="https://github.com/akvorado/akvorado/commit/f81299ff2816" title="docker: group services together in alloy configuration">f81299</a>, and <a href="https://github.com/akvorado/akvorado/commit/8eb7cd63b1c0" title="docker: make Alloy configuration use only Docker labels">8eb7cd</a>). Redis and Kafka
metrics are collected through the exporter bundled with Alloy (<a href="https://github.com/akvorado/akvorado/commit/560113e3af24" title="docker: monitor Redis and Kafka using Alloy internal exporter">560113</a>).
Other metrics are exposed using Prometheus metrics endpoints and are
automatically fetched by Alloy with the help of some Docker labels, similar to
what is done to configure Traefik. <a href="https://github.com/google/cadvisor/" title="Analyzes resource usage and performance characteristics of running containers">cAdvisor</a> was also added (<a href="https://github.com/akvorado/akvorado/commit/83d8556d6ddb" title="docker: add cAdvisor for container monitoring">83d855</a>) to
provide some container-related metrics.</p>
<p>The <code>loki</code> profile ships <a href="https://grafana.com/docs/loki/latest/" title="Grafana Loki documentation">Loki</a> to <strong>store logs</strong> (<a href="https://github.com/akvorado/akvorado/commit/45c684dc6737" title="docker: add Loki to the observability stack">45c684</a>). While Alloy
can collect and ship logs to Loki, its parsing abilities are limited: I could
not find a way to preserve all metadata associated with structured logs produced
by many applications, including Akvorado. <a href="https://vector.dev/" title="Vector: lightweight, ultra-fast tool for building observability pipelines">Vector</a> replaces Alloy (<a href="https://github.com/akvorado/akvorado/commit/95e2011d0c67" title="docker: replace Alloy by Vector for parsing logs">95e201</a>)
and features a domain-specific language, <abbr title="Vector Remap Language">VRL</abbr>, to transform logs. Annoyingly,
Vector currently <a href="https://github.com/vectordotdev/vector/issues/7358" title="Support checkpointing and reading old logs with docker_logs source">cannot retrieve Docker logs from before it was
started</a>.</p>
<div class="admonition">
<p class="admonition-title">Update (2025-11)</p>
<p>Alloy can now preserve the fields from the structured
logs into the metadata using a regular expression. See <a href="https://github.com/grafana/alloy/pull/4629" title="feat(loki.process): add regex field to structured_metadata stage">PR #4629</a>.</p>
</div>
<p>Finally, the <code>grafana</code> profile ships <a href="https://grafana.com/docs/grafana/latest/" title="Grafana documentation">Grafana</a>, but the shipped dashboards are
broken. This is planned for a future version.</p>
<h2 id="documentation">Documentation</h2>
<p>The Docker Compose setup provided by Akvorado makes it easy to get the web
interface up and running quickly. However, Akvorado requires a few mandatory
steps to be functional. It ships with <a href="https://demo.akvorado.net/docs">comprehensive documentation</a>, including
a chapter about <a href="https://demo.akvorado.net/docs/troubleshooting">troubleshooting problems</a>. I hoped this documentation would
reduce the support burden. It is difficult to know if it works. Happy users
rarely report their success, while some users open discussions asking for help
without reading much of the documentation.</p>
<p>In this release, the <strong>documentation was significantly improved</strong>.</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>git<span class="w"> </span>diff<span class="w"> </span>--shortstat<span class="w"> </span>v1.11.5<span class="w"> </span>--<span class="w"> </span>console/data/docs
<span class="go"> 10 files changed, 1873 insertions(+), 1203 deletions(-)</span>
</code></pre></div>

<p>The documentation was updated (<a href="https://github.com/akvorado/akvorado/commit/fc1028bfcdc5" title="doc: documentation update for inlet/outlet separation">fc1028</a>) to match Akvorado’s new architecture.
The troubleshooting section was rewritten (<a href="https://github.com/akvorado/akvorado/commit/17a272d0ba18" title="docs: update troubleshooting documentation">17a272</a>). Instructions on how to
improve ClickHouse performance when upgrading from versions earlier than 1.10.0
were added (<a href="https://github.com/akvorado/akvorado/commit/5f1e9a49c7d5" title="docs: document schema update for installations before 1.10.0">5f1e9a</a>). An <abbr title="Large Language Model">LLM</abbr> proofread the entire content (<a href="https://github.com/akvorado/akvorado/commit/06e3f334fdb3" title="docs: proofread the whole documentation">06e3f3</a>).
Developer-focused documentation was also improved (<a href="https://github.com/akvorado/akvorado/commit/548bbbfca161" title="docs: explain more how to develop on the frontend">548bbb</a>, <a href="https://github.com/akvorado/akvorado/commit/e41bae008b68" title="docs: make it a bit simpler to hack Akvorado">e41bae</a>, and
<a href="https://github.com/akvorado/akvorado/commit/871fc5981498" title="docs: make it even easier to hack on the console">871fc5</a>).</p>
<p>From a usability perspective, table of contents sections are now collapsible
(<a href="https://github.com/akvorado/akvorado/commit/c142e58110b3" title="console/frontend: make TOC collapsable in documentation">c142e5</a>). Admonitions help draw user attention to important points
(<a href="https://github.com/akvorado/akvorado/commit/8ac89407fd49" title="console: add admonitions to documentation markdown">8ac894</a>).</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 693px"><span class="lf-media-inner" style="padding-bottom: 67.677%"><img alt="Admonition in Akvorado documentation to ask a user not to open an issue or&#10;start a discussion before reading the documentation" src="https://media-bernat-ch.b-cdn.net/images/akvorado/admonitions@1x.1322ddc0bd4b5b.png" srcset="https://media-bernat-ch.b-cdn.net/images/akvorado/admonitions@1x.1322ddc0bd4b5b.png 871w,https://media-bernat-ch.b-cdn.net/images/akvorado/admonitions@2x.2ad1061da0399d.png 1386w" sizes="auto, (max-width: 693px) 100vw, 693px" width="693" height="469" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGMAAABDAQMAAACyWSngAAAAA1BMVEX5+PvrZTWkAAAAEUlEQVR42mNgGAWjYBQMfgAAA6oAASjPUCUAAAAASUVORK5CYII=)"/></span></div><figcaption>Example of use of admonitions in Akvorado's documentation</figcaption></figure>
<h2 id="continuous-integration">Continuous integration</h2>
<p>This release includes efforts to <strong>speed up</strong> continuous integration on GitHub.
Coverage and race tests run in parallel (<a href="https://github.com/akvorado/akvorado/commit/6af2163066aa" title="build: speed up CI by running coverage and race tests in parallel">6af216</a> and <a href="https://github.com/akvorado/akvorado/commit/fa9e48d16a90" title="build: run a third job for misc tests">fa9e48</a>). The Docker
image builds during the tests but gets tagged only after they succeed
(<a href="https://github.com/akvorado/akvorado/commit/8b0dcee14f60" title="build: build Docker image earlier but tag later">8b0dce</a>).</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1358px"><span class="lf-media-inner" style="padding-bottom: 36.672%"><img alt="GitHub workflow for CI with many jobs, some of them running in parallel, some&#10;not" src="https://media-bernat-ch.b-cdn.net/images/akvorado/github-actions@1x.d3576897765cd8.png" srcset="https://media-bernat-ch.b-cdn.net/images/akvorado/github-actions@1x.d3576897765cd8.png 1707w,https://media-bernat-ch.b-cdn.net/images/akvorado/github-actions@2x.085acee6fbcf28.png 2716w" sizes="auto, (max-width: 972px) 100vw, 900px" width="1358" height="498" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAqcAAAD5AQMAAAAgMA7pAAAAA1BMVEX19/mL6bEjAAAAK0lEQVR42u3BAQEAAACCIP+vbkhAAQAAAAAAAAAAAAAAAAAAAAAAAADAuwFTpgABD31LnAAAAABJRU5ErkJggg==)"/></span></div><figcaption>GitHub workflow to test and build Akvorado</figcaption></figure>
<p>End-to-end tests (<a href="https://github.com/akvorado/akvorado/commit/883e19922eee" title="build: add end-to-end testing">883e19</a>) ensure the shipped Docker Compose setup works as
expected. <a href="https://hurl.dev/">Hurl</a> runs tests on various HTTP endpoints, particularly to verify
metrics (<a href="https://github.com/akvorado/akvorado/commit/42679b1f2db3" title="build: use hurl for end-to-end tests">42679b</a> and <a href="https://github.com/akvorado/akvorado/commit/169fa933af64" title="build: add more e2e tests by testing Prometheus metrics">169fa9</a>). For example:</p>
<div class="language-hurl codehilite"><pre><span/><code><span class="c1">## Test inlet has received NetFlow flows</span>
<span class="kr">GET</span><span class="w"> </span><span class="sx">http://127.0.0.1:8080/prometheus/api/v1/query</span>
<span class="nn">[Query]</span>
<span class="na">query</span><span class="p">:</span><span class="w"> </span>sum(akvorado_inlet_flow_input_udp_packets_total{job="akvorado-inlet",listener=":2055"})
<span class="k">HTTP</span><span class="w"> </span><span class="mi">200</span>
<span class="nn">[Captures]</span>
<span class="na">inlet_receivedflows</span><span class="p">:</span><span class="w"> </span>jsonpath "$.data.result[0].value[1]" toInt
<span class="nn">[Asserts]</span>
<span class="k">variable</span><span class="w"> </span><span class="s2">"inlet_receivedflows"</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">10</span>

<span class="c1">## Test inlet has sent them to Kafka</span>
<span class="kr">GET</span><span class="w"> </span><span class="sx">http://127.0.0.1:8080/prometheus/api/v1/query</span>
<span class="nn">[Query]</span>
<span class="na">query</span><span class="p">:</span><span class="w"> </span>sum(akvorado_inlet_kafka_sent_messages_total{job="akvorado-inlet"})
<span class="k">HTTP</span><span class="w"> </span><span class="mi">200</span>
<span class="nn">[Captures]</span>
<span class="na">inlet_sentflows</span><span class="p">:</span><span class="w"> </span>jsonpath "$.data.result[0].value[1]" toInt
<span class="nn">[Asserts]</span>
<span class="k">variable</span><span class="w"> </span><span class="s2">"inlet_sentflows"</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="p">{{</span><span class="w"> </span><span class="nv">inlet_receivedflows</span><span class="w"> </span><span class="p">}}</span>
</code></pre></div>

<h2 id="docker">Docker</h2>
<p>Akvorado ships with a comprehensive <a href="https://docs.docker.com/compose/" title="Docker Compose documentation">Docker Compose</a> setup to help users <strong>get
started quickly</strong>. It ensures a consistent deployment, eliminating many
configuration-related issues. It also serves as a living documentation of the
complete architecture.</p>
<p>This release brings some small enhancements around Docker:</p>
<ul>
<li>an example to configure Traefik for TLS with <a href="https://letsencrypt.org/">Let’s Encrypt</a> (<a href="https://github.com/akvorado/akvorado/commit/1a27bb1bc2a3" title="docker: add examples to enable authentication and TLS">1a27bb</a>),</li>
<li>HTTP compression (<a href="https://github.com/akvorado/akvorado/commit/bee9a5de6771" title="docker: enable HTTP compression for Traefik">bee9a5</a>), and</li>
<li>support of IPv6 (<a href="https://github.com/akvorado/akvorado/commit/a74a41a6a049" title="docker: enable IPv6 networking, use a specific IPv4 subnet">a74a41</a>).</li>
</ul>
<p>Previously, many Docker images were pulled from the <a href="https://github.com/bitnami/containers/">Bitnami Containers
library</a>. However, VMWare acquired Bitnami in 2019 and Broadcom acquired
VMWare in 2023. As a result, <a href="https://github.com/bitnami/containers/issues/83267" title="Upcoming changes to the Bitnami catalog (effective August 28th, 2025)">Bitnami images were deprecated</a> in less than a
month. This was not really a surprise.<sup id="fnref:broadcom"><a class="footnote-ref" href="#fn:broadcom">4</a></sup> Previous versions of Akvorado
had already started moving away from them. In this release, the Apache project’s
Kafka image replaces the Bitnami one (<a href="https://github.com/akvorado/akvorado/commit/1eb38226fc23" title="docker: switch to Apache Kafka">1eb382</a>). Thanks to the switch to KRaft
mode, Zookeeper is no longer needed (<a href="https://github.com/akvorado/akvorado/commit/0a2ea1b1a189" title="docker: remove zookeeper for ClickHouse cluster as well">0a2ea1</a>, <a href="https://github.com/akvorado/akvorado/commit/8a49ca40d31b" title="docker: remove Zookeeper from default setup">8a49ca</a>, and <a href="https://github.com/akvorado/akvorado/commit/f65d20443622" title="docker: remove use of Zookeeper for Kafka">f65d20</a>).</p>
<p>Akvorado’s Docker images were previously compiled with Nix. However, building
AArch64 images on x86-64 is slow because it relies on QEMU userland emulation.
The updated <code>Dockerfile</code> uses <a href="https://docs.docker.com/build/building/multi-stage/" title="Multi-stage builds">multi-stage</a> and <a href="https://docs.docker.com/build/building/multi-platform/" title="Multi-platform builds">multi-platform</a> builds: one
stage builds the JavaScript part on the host platform, one stage builds the Go
part cross-compiled on the host platform, and the final stage assembles the
image on top of a <a href="https://github.com/GoogleContainerTools/distroless" title="Language focused docker images, minus the operating system">slim distroless image</a> (<a href="https://github.com/akvorado/akvorado/commit/268e95ee1891" title="docker: switch away from Nix to build Docker images">268e95</a> and <a href="https://github.com/akvorado/akvorado/commit/d526ca451ce1" title="build: add a platform independant step into Makefile">d526ca</a>).</p>
<div class="language-docker codehilite"><pre><span/><code><span class="c"># This is a simplified version</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">--platform=$BUILDPLATFORM</span><span class="w"> </span>node:20-alpine<span class="w"> </span>AS<span class="w"> </span>build-js
<span class="k">RUN</span><span class="w"> </span>apk<span class="w"> </span>add<span class="w"> </span>--no-cache<span class="w"> </span>make
<span class="k">WORKDIR</span><span class="w"> </span><span class="s">/build</span>
<span class="k">COPY</span><span class="w"> </span>console/frontend<span class="w"> </span>console/frontend
<span class="k">COPY</span><span class="w"> </span>Makefile<span class="w"> </span>.
<span class="k">RUN</span><span class="w"> </span>make<span class="w"> </span>console/data/frontend

<span class="k">FROM</span><span class="w"> </span><span class="s">--platform=$BUILDPLATFORM</span><span class="w"> </span>golang:alpine<span class="w"> </span>AS<span class="w"> </span>build-go
<span class="k">RUN</span><span class="w"> </span>apk<span class="w"> </span>add<span class="w"> </span>--no-cache<span class="w"> </span>make<span class="w"> </span>curl<span class="w"> </span>zip
<span class="k">WORKDIR</span><span class="w"> </span><span class="s">/build</span>
<span class="k">COPY</span><span class="w"> </span>.<span class="w"> </span>.
<span class="k">COPY</span><span class="w"> </span>--from<span class="o">=</span>build-js<span class="w"> </span>/build/console/data/frontend<span class="w"> </span>console/data/frontend
<span class="k">RUN</span><span class="w"> </span>go<span class="w"> </span>mod<span class="w"> </span>download
<span class="k">RUN</span><span class="w"> </span>make<span class="w"> </span>all-indep
<span class="hll"><span class="k">ARG</span><span class="w"> </span>TARGETOS<span class="w"> </span>TARGETARCH<span class="w"> </span>TARGETVARIANT<span class="w"> </span>VERSION
</span><span class="k">RUN</span><span class="w"> </span>make

<span class="k">FROM</span><span class="w"> </span><span class="s">gcr.io/distroless/static:latest</span>
<span class="k">COPY</span><span class="w"> </span>--from<span class="o">=</span>build-go<span class="w"> </span>/build/bin/akvorado<span class="w"> </span>/usr/local/bin/akvorado
<span class="k">ENTRYPOINT</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"/usr/local/bin/akvorado"</span><span class="w"> </span><span class="p">]</span>
</code></pre></div>

<p>When building for multiple platforms with <code>--platform
linux/amd64,linux/arm64,linux/arm/v7</code>, the build steps until the highlighted
line execute only once for all platforms. This significantly speeds up the
build. 🚅</p>
<p>Akvorado now ships Docker images for these platforms: <code>linux/amd64</code>,
<code>linux/amd64/v3</code>, <code>linux/arm64</code>, and <code>linux/arm/v7</code>. When requesting
<code>ghcr.io/akvorado/akvorado</code>, Docker selects the best image for the current CPU.
On x86-64, there are two choices. If your <a href="https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels" title="X86-64: Microarchitecture levels">CPU is recent enough</a>, Docker
downloads <code>linux/amd64/v3</code>. This version contains additional optimizations and
should run faster than the <code>linux/amd64</code> version. It would be interesting to
ship an image for <code>linux/arm64/v8.2</code>, but Docker does not support the same
mechanism for AArch64 yet (<a href="https://github.com/akvorado/akvorado/commit/7928084fdef0" title="build: add some pointers to understand architecture variants">792808</a>).</p>
<h2 id="go">Go</h2>
<p>This release includes many changes related to Go but not visible to the users.</p>
<h3 id="toolchain">Toolchain</h3>
<p>In the past, Akvorado supported the two latest Go versions, preventing immediate
use of the latest enhancements. The goal was to allow users of stable
distributions to use Go versions shipped with their distribution to compile
Akvorado. However, this became frustrating when interesting features, like <code>go
tool</code>, were released. Akvorado 2.0 requires <strong>Go 1.25</strong> (<a href="https://github.com/akvorado/akvorado/commit/77306ddceee6" title="build: bump go.mod to Go 1.25 to use wg.Go()">77306d</a>) but can be
compiled with older toolchains by automatically downloading a newer one
(<a href="https://github.com/akvorado/akvorado/commit/94fb1c64837d" title="build: accept building with a not up-to-date toolchain">94fb1c</a>).<sup id="fnref:debian"><a class="footnote-ref" href="#fn:debian">5</a></sup> Users can still override <code>GOTOOLCHAIN</code> to revert this
decision. The recommended toolchain updates weekly through <abbr title="Continuous integration">CI</abbr> to ensure we get
the latest minor release (<a href="https://github.com/akvorado/akvorado/commit/5b11ecf29e4c" title="build: try updating Go toolchain weekly">5b11ec</a>). This change also simplifies updates to
newer versions: only <code>go.mod</code> needs updating.</p>
<p>Thanks to this change, Akvorado now uses <code>wg.Go()</code> (<a href="https://github.com/akvorado/akvorado/commit/77306ddceee6" title="build: bump go.mod to Go 1.25 to use wg.Go()">77306d</a>) and I have
started converting some unit tests to the new <code>test/synctest</code> package
(<a href="https://github.com/akvorado/akvorado/commit/bd787e654f93" title="common/daemon: experiment with the test/synctest package">bd787e</a>, <a href="https://github.com/akvorado/akvorado/commit/7016d899690b" title="demoexporter/flows: switch to testing/synctest package for clock">7016d8</a>, and <a href="https://github.com/akvorado/akvorado/commit/159085a0d3ac" title="outlet/metadata: switch to testing/synctest package for clock">159085</a>).</p>
<h3 id="testing">Testing</h3>
<p>When <strong>testing equality</strong>, I use a helper function <code>Diff()</code> to display the
differences when it fails:</p>
<div class="language-go codehilite"><pre><span/><code><span class="nx">got</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">Keys</span><span class="p">()</span>
<span class="nx">expected</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="kt">int</span><span class="p">{</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">}</span>
<span class="k">if</span><span class="w"> </span><span class="nx">diff</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">helpers</span><span class="p">.</span><span class="nx">Diff</span><span class="p">(</span><span class="nx">got</span><span class="p">,</span><span class="w"> </span><span class="nx">expected</span><span class="p">);</span><span class="w"> </span><span class="nx">diff</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s">""</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"Keys() (-got, +want):\n%s"</span><span class="p">,</span><span class="w"> </span><span class="nx">diff</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div>

<p>This function uses <a href="https://github.com/kylelemons/godebug"><code>kylelemons/godebug</code></a>. This package is
no longer maintained and has some shortcomings: for example, by default, it does
not compare struct private fields, which may cause unexpectedly successful
tests. I replaced it with <a href="https://github.com/google/go-cmp"><code>google/go-cmp</code></a>, which is stricter
and has better output (<a href="https://github.com/akvorado/akvorado/commit/e2f1df9add0d" title="tests: replace godebug by go-cmp for structure diffs">e2f1df</a>).</p>
<h3 id="another-package-for-kafka">Another package for Kafka</h3>
<p>Another change is the switch from <a href="https://github.com/IBM/sarama">Sarama</a> to <a href="https://github.com/twmb/franz-go">franz-go</a> to <strong>interact with
Kafka</strong> (<a href="https://github.com/akvorado/akvorado/commit/756e4a8fbd7a" title="/kafka: switch to franz-go">756e4a</a> and <a href="https://github.com/akvorado/akvorado/commit/2d26c5b9cab1" title="inlet/kafka: use kfake for functional tests with Kafka">2d26c5</a>). The main motivation for this change is to
get a better concurrency model. Sarama heavily relies on channels and it is
difficult to understand the lifecycle of an object handed to this package.
franz-go uses a more modern approach with callbacks<sup id="fnref:channels"><a class="footnote-ref" href="#fn:channels">6</a></sup> that is both more
performant and easier to understand. It also ships with a package to spawn fake
Kafka broker clusters, which is more convenient than the mocking functions
provided by Sarama.</p>
<h3 id="improved-routing-table-for-bmp">Improved routing table for <abbr title="BGP Monitoring Protocol">BMP</abbr></h3>
<p>To store its routing table, the <abbr title="BGP Monitoring Protocol">BMP</abbr> component used
<a href="https://github.com/kentik/patricia"><code>kentik/patricia</code></a>, an implementation of a patricia tree
focused on reducing garbage collection pressure.
<a href="https://github.com/gaissmai/bart"><code>gaissmai/bart</code></a> is a more recent alternative using an
adaptation of <a href="https://www.hariguchi.org/art/art.pdf" title="ART: Allotment Routing Table. A Fast Free Multibit Trie Based Routing Table">Donald Knuth’s ART algorithm</a> that promises <a href="https://github.com/gaissmai/iprbench" title="Benchmarks for Golang IP routing table">better
performance</a> and delivers it: <strong>90% faster lookups</strong> and <strong>27% faster
insertions</strong> (<a href="https://github.com/akvorado/akvorado/commit/92ee2e05b7c4" title="outlet/routing: use gaissmai/bart instead of kentik/patricia">92ee2e</a> and <a href="https://github.com/akvorado/akvorado/commit/fdb65c93a5fe" title="outlet/routing: store v4 routes into a v4 tree">fdb65c</a>).</p>
<p>Unlike <code>kentik/patricia</code>, <code>gaissmai/bart</code> does not help efficiently store values
attached to each prefix. I adapted the same approach as <code>kentik/patricia</code> to
store route lists for each prefix: store a 32-bit index for each prefix, and use
it to build a 64-bit index for looking up routes in a map. This leverages Go’s
efficient map structure.</p>
<p><code>gaissmai/bart</code> also supports a lockless routing table version, but this is not
simple because we would need to extend this to the map storing the routes and to
the interning mechanism. I also attempted to use Go’s new <code>unique</code> package to
replace the <code>intern</code> package included in Akvorado, but performance was
worse.<sup id="fnref:worse"><a class="footnote-ref" href="#fn:worse">7</a></sup></p>
<h3 id="miscellaneous">Miscellaneous</h3>
<p>Previous versions of Akvorado were using a <a href="/en/blog/2023-dynamic-protobuf-golang" title="Fast and dynamic encoding of Protocol Buffers in Go">custom Protobuf encoder</a> for
performance and flexibility. With the introduction of the outlet service,
Akvorado only needs a simple static schema, so this code was removed. However,
it is possible to enhance performance with
<a href="https://github.com/planetscale/vtprotobuf"><code>planetscale/vtprotobuf</code></a> (<a href="https://github.com/akvorado/akvorado/commit/e49a744a6d98" title="build: use vtprotobuf to speedup protobuf marshal/unmarshal">e49a74</a>, and <a href="https://github.com/akvorado/akvorado/commit/8b580fd26bfc" title="outlet/core: reuse the same RawFlow object when decoding">8b580f</a>).
Moreover, the dependency on <code>protoc</code>, a C++ program, was somewhat annoying.
Therefore, Akvorado now uses <a href="https://buf.build/">buf</a>, written in Go, to convert a Protobuf
schema into Go code (<a href="https://github.com/akvorado/akvorado/commit/f4c879e525a5" title="build: switch from protoc to buf to build protobuf">f4c879</a>).</p>
<p>Another small optimization to <strong>reduce the size of the Akvorado binary</strong> by
10 MB was to compress the static assets embedded in Akvorado in a ZIP file. It
includes the ASN database, as well as the SVG images for the documentation. A
small layer of code makes this change transparent (<a href="https://github.com/akvorado/akvorado/commit/b1d6382585dd" title="common/embed: replace all go:embed use by an embedded archive">b1d638</a> and <a href="https://github.com/akvorado/akvorado/commit/e69b919d1c78" title="build: remove embedded drawio diagrams from embedded SVG">e69b91</a>).</p>
<h2 id="javascript">JavaScript</h2>
<p>Recently, two large <strong>supply-chain attacks</strong> hit the JavaScript ecosystem: <a href="https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised" title="npm debug and chalk packages compromised">one
affecting the popular packages <code>chalk</code> and <code>debug</code></a> and <a href="https://sigh.dev/posts/ctrl-tinycolor-post-mortem/" title="@ctrl/tinycolor Supply Chain Attack Post-mortem">another
impacting the popular package <code>@ctrl/tinycolor</code></a>. These attacks also
exist in other ecosystems, but JavaScript is a prime target due to heavy use of
small third-party dependencies. The previous version of Akvorado relied on <strong>653
dependencies</strong>.</p>
<p><code>npm-run-all</code> was removed (<a href="https://github.com/akvorado/akvorado/commit/3424e8b8c9f2" title="console/frontend: don't use npm-run-all">3424e8</a>, 132 dependencies). <code>patch-package</code> was
removed (<a href="https://github.com/akvorado/akvorado/commit/6258050251a3" title="console/frontend: remove patch for sugar-date">625805</a> and <a href="https://github.com/akvorado/akvorado/commit/e85ff0ae31fa" title="console/frontend: remove patch for echarts">e85ff0</a>, 69 dependencies) by moving missing
TypeScript definitions to <code>env.d.ts</code>. <code>eslint</code> was replaced with <a href="https://oxc.rs/docs/guide/usage/linter">oxlint</a>, a
linter written in Rust (<a href="https://github.com/akvorado/akvorado/commit/97fd8ca2e479" title="console/frontend: replace eslint by oxlint">97fd8c</a>, 125 dependencies, including the plugins).</p>
<p>I switched from <code>npm</code> to <a href="https://pnpm.io/" title="Fast, disk space efficient package manager">Pnpm</a>, an alternative package manager (<a href="https://github.com/akvorado/akvorado/commit/fce383dbf46b" title="build: switch to pnpm">fce383</a>).
Pnpm does not run install scripts by default<sup id="fnref:install"><a class="footnote-ref" href="#fn:install">8</a></sup> and prevents installing
packages that are too recent. It is also significantly faster.<sup id="fnref:fast"><a class="footnote-ref" href="#fn:fast">9</a></sup> Node.js
does not ship Pnpm but it ships <a href="https://github.com/nodejs/corepack" title="Zero-runtime-dependency package acting as bridge between Node projects and their package managers">Corepack</a>, which allows us to use Pnpm
without installing it. Pnpm can also list licenses used by each dependency,
removing the need for <code>license-compliance</code> (<a href="https://github.com/akvorado/akvorado/commit/a35ca8bc07e0" title="build: replace license-compliance with pnpm">a35ca8</a>, 42 dependencies).</p>
<p>For additional speed improvements, beyond switching to Pnpm and Oxlint, <a href="https://vite.dev/">Vite</a>
was replaced with its faster <a href="https://voidzero.dev/posts/announcing-rolldown-vite" title="Announcing Rolldown-Vite">Rolldown version</a> (<a href="https://github.com/akvorado/akvorado/commit/4638272b7b6d" title="console/frontend: switch to rolldown-vite">463827</a>).</p>
<p>After these changes, Akvorado “only” pulls 225 dependencies. 😱</p>
<h1 id="next-steps">Next steps</h1>
<p>I would like to land three features in the next version of Akvorado:</p>
<ul>
<li>
<p>Add Grafana dashboards to complete the observability stack. See <a href="https://github.com/akvorado/akvorado/issues/1906" title="Ship some Grafana dashboards in docker compose setup">issue
  #1906</a> for details.</p>
</li>
<li>
<p>Integrate <a href="https://github.com/ovh/grafana-akvorado">OVH’s Grafana plugin</a> by providing a stable API for such
  integrations. Akvorado’s web console would still be useful for browsing
  results, but if you want to build and share dashboards, you should switch to
  Grafana. See <a href="https://github.com/akvorado/akvorado/issues/1895" title="Integration of OVH Grafana plugin">issue #1895</a>.</p>
</li>
<li>
<p>Move some work currently done in ClickHouse (custom dictionaries, GeoIP and IP
  enrichment) back into the outlet service. This should give more flexibility
  for adding features like the one requested in <a href="https://github.com/akvorado/akvorado/issues/1030" title="RegExp for custom dimension">issue #1030</a>. See <a href="https://github.com/akvorado/akvorado/issues/2006" title="Move back GeoIP and custom dictionaries into outlet">issue #2006</a>.</p>
</li>
</ul>
<hr/>
<p>I started working on splitting the inlet into two parts more than one year ago.
I found more motivation in recent months, partly thanks to <a href="https://code.claude.com/docs/en/overview" title="Claude Code overview">Claude Code</a>,
which I used as a <a href="https://en.wikipedia.org/wiki/Rubber_duck_debugging" title="https://en.wikipedia.org/wiki/Rubber_duck_debugging">rubber duck</a>. Almost none of the produced code was
kept:<sup id="fnref:exceptions"><a class="footnote-ref" href="#fn:exceptions">10</a></sup> it is like an intern who never learns. 🦆</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:bmp">
<p>Many attempts were made to make the <abbr title="BGP Monitoring Protocol">BMP</abbr> component both performant and
not blocking. See for example <a href="https://github.com/akvorado/akvorado/pull/254" title="inlet/bmp: avoid long lock times when flushing peers">PR #254</a>, <a href="https://github.com/akvorado/akvorado/pull/255" title="“Lockless” RIB design">PR #255</a>, and <a href="https://github.com/akvorado/akvorado/pull/278" title="inlet/bmp: new lockless design">PR #278</a>.
Despite these efforts, this component remained problematic for most users.
See <a href="https://github.com/akvorado/akvorado/issues/1461" title="BMP crash: cannot read BMP body">issue #1461</a> as an example. <a class="footnote-backref" href="#fnref:bmp" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:features">
<p>Some features have been pushed to ClickHouse to avoid the
processing cost in the inlet. See for example <a href="https://github.com/akvorado/akvorado/pull/1059" title="Do geoip enrich in clickhouse instead of inlet">PR #1059</a>. <a class="footnote-backref" href="#fnref:features" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:size">
<p>This is the <a href="https://github.com/akvorado/akvorado/commit/ac68c5970e2c" title="inlet: split inlet into new inlet and outlet">biggest commit</a>:</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>git<span class="w"> </span>show<span class="w"> </span>--shortstat<span class="w"> </span>ac68c5970e2c<span class="w"> </span><span class="p">|</span><span class="w"> </span>tail<span class="w"> </span>-1
<span class="go">231 files changed, 6474 insertions(+), 3877 deletions(-)</span>
</code></pre></div>

<p><a class="footnote-backref" href="#fnref:size" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:broadcom">
<p>Broadcom is known for its user-hostile moves. Look at what happened
with VMWare. <a class="footnote-backref" href="#fnref:broadcom" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:debian">
<p>As a Debian developer, I dislike these mechanisms that circumvent
the distribution package manager. The final straw came when Go 1.25 spent one month in the <a href="https://ftp-master.debian.org/new.html">Debian NEW queue</a>, an arbitrary mechanism I
don’t like at all. <a class="footnote-backref" href="#fnref:debian" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:channels">
<p>In the early years of Go, channels were heavily promoted. Sarama
was designed during this period. A few years later, a more nuanced approach
emerged. See notably “<a href="https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad/" title="Go channels are bad and you should feel bad">Go channels are bad and you should feel bad</a>.” <a class="footnote-backref" href="#fnref:channels" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:worse">
<p>This should be investigated further, but my theory is that the
<code>intern</code> package uses 32-bit integers, while <code>unique</code> uses 64-bit pointers.
See commit <a href="https://github.com/akvorado/akvorado/commit/74e5aca41b71" title="outlet/routing: replace intern pool by unique.Hash">74e5ac</a>. <a class="footnote-backref" href="#fnref:worse" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:install">
<p>This is also possible with <code>npm</code>. See commit <a href="https://github.com/akvorado/akvorado/commit/dab2f7875171" title="build: do not run install scripts with npm">dab2f7</a>. <a class="footnote-backref" href="#fnref:install" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:fast">
<p>An even faster alternative is <a href="https://bun.sh/" title="Fast all-in-one JavaScript runtime">Bun</a>, but it is less available. <a class="footnote-backref" href="#fnref:fast" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:exceptions">
<p>The exceptions are part of the code for the admonition blocks,
the code for collapsing the table of contents, and part of the documentation. <a class="footnote-backref" href="#fnref:exceptions" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Crafting endless AS paths in BGP</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/en/blog/2024-bgp-endless-aspath" rel="alternate"/>
    <link href="https://vincent.bernat.ch/en/blog/2024-bgp-endless-aspath#isso-thread" rel="replies" type="text/html"/>
    <updated>2024-07-28T20:32:36Z</updated>
    <id>http://www.luffy.cx/en/blog/2024-bgp-endless-aspath.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Combining <a href="https://www.rfc-editor.org/rfc/rfc3065" title="RFC 3065: Autonomous System Confederations for BGP"><abbr title="Border Gateway Protocol">BGP</abbr> confederations</a> and <a href="https://networklessons.com/bgp/mpls-layer-3-vpn-bgp-override" title="MPLS Layer 3 VPN BGP AS Override"><abbr title="Autonomous System">AS</abbr> override</a> can potentially
create a <abbr title="Border Gateway Protocol">BGP</abbr> routing loop, resulting in an indefinitely expanding <abbr title="Autonomous System">AS</abbr> path.</p>
<p><strong><abbr title="Border Gateway Protocol">BGP</abbr> confederation</strong> is a technique used to reduce the number of <abbr title="Internal BGP">iBGP</abbr> sessions
and improve scalability in large autonomous systems (<abbr title="Autonomous System">AS</abbr>). It divides an <abbr title="Autonomous System">AS</abbr> into
sub-ASes. Most <abbr title="External BGP">eBGP</abbr> rules apply between sub-ASes, except that next-hop, <abbr title="Multi-Exit Discriminator">MED</abbr>, and
local preferences remain unchanged. The <abbr title="Autonomous System">AS</abbr> path length ignores contributions
from confederation sub-ASes. <abbr title="Border Gateway Protocol">BGP</abbr> confederation is rarely used and <a href="https://www.rfc-editor.org/rfc/rfc4456" title="RFC 4456: BGP Route Reflection: An Alternative to Full Mesh Internal BGP (IBGP)"><abbr title="Border Gateway Protocol">BGP</abbr> route
reflection</a> is typically preferred for scaling.</p>
<p><strong><abbr title="Autonomous System">AS</abbr> override</strong> is a feature that allows a router to replace the <abbr title="Autonomous System Number">ASN</abbr> of a
neighbor in the <abbr title="Autonomous System">AS</abbr> path of outgoing <abbr title="Border Gateway Protocol">BGP</abbr> routes with its own. It’s useful when
two distinct autonomous systems share the same <abbr title="Autonomous System Number">ASN</abbr>. However, it interferes with
<abbr title="Border Gateway Protocol">BGP</abbr>’s loop prevention mechanism and should be used cautiously. A safer
alternative is the <code>allowas-in</code> directive.<sup id="fnref:allowconfedas-in"><a class="footnote-ref" href="#fn:allowconfedas-in">1</a></sup></p>
<p>In the example below, we have four routers in a single confederation, each in
its own sub-<abbr title="Autonomous System">AS</abbr>. <code>R0</code> originates the <code>2001:db8::1/128</code> prefix. <code>R1</code>, <code>R2</code>, and
<code>R3</code> forward this prefix to the next router in the loop.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 371px"><span class="lf-media-inner" style="padding-bottom: 108.086%"><img alt="BGP routing loop involving 4 routers: R0 originates a prefix, R1, R2, R3 make&#10;it loop using next-hop-self and as-override" src="https://media-bernat-ch.b-cdn.net/images/bgp-loop.daef0f4df09813.svg" width="371" height="401" class="lf-media"/></span></div><figcaption>BGP routing loop using a confederation</figcaption></figure>
<p>The router configurations are available in a <a href="https://github.com/vincentbernat/network-lab/tree/master/lab-bgp-infinite-aspath">Git repository</a>. They are
running Cisco IOS XR. <code>R2</code> uses the following configuration for <abbr title="Border Gateway Protocol">BGP</abbr>:</p>
<div class="language-ios codehilite"><pre><span/><code><span class="nf">router</span> bgp <span class="m">64502</span>
 <span class="nf">bgp</span> confederation peers
  <span class="m">64500</span>
  <span class="m">64501</span>
  <span class="m">64503</span>
<span class="c c-Singleline"> !</span>
 <span class="nf">bgp</span> confederation identifier <span class="m">64496</span>
 <span class="nf">bgp</span> router-id <span class="m">1.0.0.2</span>
 <span class="nf">address-family</span> ipv6 unicast
<span class="c c-Singleline"> !</span>
 <span class="nf">neighbor</span> <span class="m">2001:db8::2:0</span>
  <span class="nf">remote-as</span> <span class="m">64501</span>
  <span class="nf">description</span> R1
  <span class="nf">address-family</span> ipv6 unicast
<span class="c c-Singleline">  !</span>
<span class="c c-Singleline"> !</span>
 <span class="nf">neighbor</span> <span class="m">2001:db8::3:1</span>
  <span class="nf">remote-as</span> <span class="m">64503</span>
  <span class="nf">advertisement-interval</span> <span class="m">0</span>
  <span class="nf">description</span> R3
<span class="hll">  <span class="nf">address-family</span> ipv6 unicast
</span><span class="hll">   <span class="nf">next-hop-self</span>
</span><span class="hll">   <span class="nf">as-override</span>
</span><span class="c c-Singleline">  !</span>
<span class="c c-Singleline"> !</span>
<span class="c c-Singleline">!</span>
</code></pre></div>

<p>The session with <code>R3</code> uses both <code>as-override</code> and <code>next-hop-self</code> directives.
The latter is only necessary to make the announced prefix valid, as there is no
<abbr title="Internal Gateway Protocol">IGP</abbr> in this example.<sup id="fnref:igp"><a class="footnote-ref" href="#fn:igp">2</a></sup></p>
<p>Here’s the sequence of events leading to an infinite <abbr title="Autonomous System">AS</abbr> path:</p>
<ol>
<li>
<p><code>R0</code> sends the prefix to <code>R1</code> with <abbr title="Autonomous System">AS</abbr> path <code>(64500)</code>.<sup id="fnref:confed"><a class="footnote-ref" href="#fn:confed">3</a></sup></p>
</li>
<li>
<p><code>R1</code> selects it as the best path, forwarding it to <code>R2</code> with <abbr title="Autonomous System">AS</abbr>
   path <code>(64501 64500)</code>.</p>
</li>
<li>
<p><code>R2</code> selects it as the best path, forwarding it to <code>R3</code> with <abbr title="Autonomous System">AS</abbr>
   path <code>(64500 64501 64502)</code>.</p>
</li>
<li>
<p><code>R3</code> selects it as the best path. It would forward it to <code>R1</code> with <abbr title="Autonomous System">AS</abbr> path
   <code>(64503 64502 64501 64500)</code>, but due to <abbr title="Autonomous System">AS</abbr> override, it substitutes <code>R1</code>’s
   <abbr title="Autonomous System Number">ASN</abbr> with its own, forwarding it with <abbr title="Autonomous System">AS</abbr> path <code>(64503 64502 64503 64500)</code>.</p>
</li>
<li>
<p><code>R1</code> accepts the prefix, as its own <abbr title="Autonomous System Number">ASN</abbr> is not in the <abbr title="Autonomous System">AS</abbr> path. It compares
   this new prefix with the one from <code>R0</code>. Both <code>(64500)</code> and <code>(64503 64502
   64503 64500)</code> have the same length because confederation sub-ASes don’t
   contribute to <abbr title="Autonomous System">AS</abbr> path length. <strong>The first tie-breaker is the router ID</strong>.
   <code>R0</code>’s router ID (<code>1.0.0.4</code>) is higher than <code>R3</code>’s (<code>1.0.0.3</code>). The new
   prefix becomes the best path and is forwarded to <code>R2</code> with <abbr title="Autonomous System">AS</abbr> path <code>(64501
   64503 64501 64503 64500)</code>.</p>
</li>
<li>
<p><code>R2</code> receives the new prefix, replacing the old one. It selects it as the
   best path and forwards it to <code>R3</code> with <abbr title="Autonomous System">AS</abbr> path <code>(64502 64501 64502 64501
   64502 64500)</code>.</p>
</li>
<li>
<p><code>R3</code> receives the new prefix, replacing the old one. It selects it as the
   best path and forwards it to <code>R0</code> with <abbr title="Autonomous System">AS</abbr> path <code>(64503 64502 64503 64502
   64503 64502 64500)</code>.</p>
</li>
<li>
<p><code>R1</code> receives the new prefix, replacing the old one. Again, it competes with
   the prefix from <code>R0</code>, and again the new prefix wins due to the lower router
   ID. The prefix is forwarded to <code>R2</code> with <abbr title="Autonomous System">AS</abbr> path <code>(64501 64503 64501 64503
   64501 64503 64501 64500)</code>.</p>
</li>
</ol>
<p>A few iterations later, <code>R1</code> views the looping prefix as follows:<sup id="fnref:pacing"><a class="footnote-ref" href="#fn:pacing">4</a></sup></p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">RP/0/RP0/CPU0:R1#</span>show<span class="w"> </span>bgp<span class="w"> </span>ipv6<span class="w"> </span>u<span class="w"> </span><span class="m">2001</span>:db8::1/128<span class="w"> </span>bestpath-compare
<span class="go">BGP routing table entry for 2001:db8::1/128</span>
<span class="go">Last Modified: Jul 28 10:23:05.560 for 00:00:00</span>
<span class="go">Paths: (2 available, best #2)</span>
<span class="go">  Path #1: Received by speaker 0</span>
<span class="go">  Not advertised to any peer</span>
<span class="go">  (64500)</span>
<span class="go">    2001:db8::1:0 from 2001:db8::1:0 (1.0.0.4), if-handle 0x00000000</span>
<span class="go">      Origin IGP, metric 0, localpref 100, valid, confed-external</span>
<span class="go">      Received Path ID 0, Local Path ID 0, version 0</span>
<span class="hll"><span class="go">      Higher router ID than best path (path #2)</span>
</span><span class="go">  Path #2: Received by speaker 0</span>
<span class="go">  Advertised IPv6 Unicast paths to peers (in unique update groups):</span>
<span class="go">    2001:db8::2:1</span>
<span class="go">  (64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64500)</span>
<span class="go">    2001:db8::4:0 from 2001:db8::4:0 (1.0.0.3), if-handle 0x00000000</span>
<span class="go">      Origin IGP, metric 0, localpref 100, valid, confed-external, best, group-best</span>
<span class="go">      Received Path ID 0, Local Path ID 1, version 37</span>
<span class="go">      best of AS 64503, Overall best</span>
</code></pre></div>

<p>There’s no upper bound for an <abbr title="Autonomous System">AS</abbr> path, but <abbr title="Border Gateway Protocol">BGP</abbr> messages have size limits (4096
bytes per <a href="https://www.rfc-editor.org/rfc/rfc4271" title="RFC 4271: A Border Gateway Protocol 4 (BGP-4)">RFC 4271</a> or 65535 bytes per <a href="https://www.rfc-editor.org/rfc/rfc8654" title="RFC 8654: Extended Message Support for BGP">RFC 8654</a>). At some
point, <abbr title="Border Gateway Protocol">BGP</abbr> updates can’t be generated. On Cisco IOS XR, the <abbr title="Border Gateway Protocol">BGP</abbr> process crashes
well before reaching this limit.<sup id="fnref:ddts"><a class="footnote-ref" href="#fn:ddts">5</a></sup></p>
<hr/>
<p>The main lessons from this tale are:</p>
<ul>
<li>never use <abbr title="Border Gateway Protocol">BGP</abbr> confederations under any circumstances, and</li>
<li>be cautious of features that weaken <abbr title="Border Gateway Protocol">BGP</abbr> routing loop detection. 🌀</li>
</ul>
<div class="admonition">
<p class="admonition-title">Update (2026-10)</p>
<p>This post was briefly discussed on <a href="https://news.ycombinator.com/item?id=41101700">Hacker News</a>.</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:allowconfedas-in">
<p>When using <abbr title="Border Gateway Protocol">BGP</abbr> confederations with Cisco IOS XR, use
<code>allowconfedas-in</code> instead. It’s available since <a href="https://www.cisco.com/c/en/us/td/docs/iosxr/ncs5500/bgp/711x/configuration/guide/b-bgp-cg-ncs5500-711x/implementing-bgp.html#feat-3439-overview-bgp-allowas-in-9k">IOS XR 7.11</a>. <a class="footnote-backref" href="#fnref:allowconfedas-in" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:igp">
<p>Using <abbr title="Border Gateway Protocol">BGP</abbr> confederations is already inadvisable. If you don’t use the
same <abbr title="Internal Gateway Protocol">IGP</abbr> for all sub-ASes, you’re inviting trouble! However, the scenario
described here is also possible with an <abbr title="Internal Gateway Protocol">IGP</abbr>. <a class="footnote-backref" href="#fnref:igp" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:confed">
<p>When an <abbr title="Autonomous System">AS</abbr> path segment is composed of <abbr title="Autonomous System Numbers">ASNs</abbr> from a confederation, it
is displayed between parentheses. <a class="footnote-backref" href="#fnref:confed" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:pacing">
<p>By default, IOS XR paces <abbr title="External BGP">eBGP</abbr> updates. This is controlled by the
<code>advertisement-interval</code> directive. Its default value is 30 seconds for <abbr title="External BGP">eBGP</abbr>
peers (even in the same confederation). <code>R1</code> and <code>R2</code> set this value to 0,
while <code>R3</code> sets it to 2 seconds. This gives some time to watch the <abbr title="Autonomous System">AS</abbr> path
grow. <a class="footnote-backref" href="#fnref:pacing" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:ddts">
<p>This is <a href="https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-iosxr-bgp-dos-O7stePhX" title="Cisco IOS XR Software Border Gateway Protocol Confederation Denial of Service Vulnerability">CVE-2025-20115</a>. It only happens when using <code>as-override</code> on
an <abbr title="Autonomous System">AS</abbr> path with a too long <code>AS_CONFED_SEQUENCE</code>. This is fixed in 24.2.21
and 24.3.1. <a class="footnote-backref" href="#fnref:ddts" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
</feed>