<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: pathvector-dev</title>
    <description>The latest articles on DEV Community by pathvector-dev (@pathvector-dev).</description>
    <link>https://dev.to/pathvector-dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4024552%2Ff09668fa-231d-4239-8826-bba1552bc241.png</url>
      <title>DEV Community: pathvector-dev</title>
      <link>https://dev.to/pathvector-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pathvector-dev"/>
    <language>en</language>
    <item>
      <title>Which router wins the segment? Read the election, not the docs</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Thu, 06 Aug 2026 00:00:09 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/which-router-wins-the-segment-read-the-election-not-the-docs-44ik</link>
      <guid>https://dev.to/pathvector-dev/which-router-wins-the-segment-read-the-election-not-the-docs-44ik</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-ospf-03/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-ospf-03/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic — inputs, state, and branches — rather than as configuration examples. Every module points at one real Python file and walks its control flow. The source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;pathvector-studio/protocol-in-code&lt;/a&gt;. If you're earlier in the journey and want to &lt;em&gt;touch&lt;/em&gt; the protocols first — capture packets, poke at daemons, break things on purpose — start with the companion &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series instead, then come back here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If several routers share one broadcast segment, how does the code pick DR and BDR?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hold onto that as you read. Not "what is a DR" — you can get that from any certification slide deck. The question is how a deterministic function turns a bag of interface states into exactly one designated router and at most one backup, with no coordination beyond what every router can already see.&lt;/p&gt;

&lt;p&gt;The file we're reading is &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/ospf/dr_election.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/ospf/dr_election.py&lt;/code&gt;&lt;/a&gt;. It is under sixty lines. Almost all of the interesting behavior lives in two of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this comes from
&lt;/h2&gt;

&lt;p&gt;Session 02 left us with a loose end. On a broadcast segment, some neighbors reach &lt;code&gt;2-Way&lt;/code&gt; and then simply stop — they never progress to full adjacency. That looks like a bug until you know what comes next. Those routers stopped because they aren't the DR or the BDR, and on a shared segment you only form full adjacencies with those two. So the "stuck at 2-Way" observation from last session is really a pointer to this session's branch: something has to decide who the DR and BDR are, and everyone else settles into &lt;code&gt;2-Way&lt;/code&gt; on purpose.&lt;/p&gt;

&lt;p&gt;That's the shape worth carrying with you: &lt;strong&gt;an election exists to collapse an N×N mesh of adjacencies into a hub.&lt;/strong&gt; Without it, ten routers on one Ethernet segment would form 45 adjacencies and flood 45 copies of everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read it like code
&lt;/h2&gt;

&lt;p&gt;The module gives a read order, and it's the right one: the candidate shape first, then the picker, then the election that uses both.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. What a candidate is
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InterfaceCandidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;router_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;declared_dr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;declared_bdr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four fields, and each one is doing a distinct job.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;priority&lt;/code&gt; is the operator's thumb on the scale. &lt;code&gt;router_id&lt;/code&gt; is the tie-break of last resort — it's guaranteed unique on the segment, so it's guaranteed to produce a total order. And then there's the interesting pair: &lt;code&gt;declared_dr&lt;/code&gt; and &lt;code&gt;declared_bdr&lt;/code&gt;. These are not "should this router be DR." They're &lt;strong&gt;"this router is currently claiming to be DR"&lt;/strong&gt; — a self-report that arrives in the neighbor's Hello packet. That distinction is the whole reason OSPF elections are sticky rather than churning every time a better router shows up.&lt;/p&gt;

&lt;p&gt;Note that the dataclass is &lt;code&gt;frozen=True&lt;/code&gt;. Candidates are inputs to a pure function, not mutable state being edited in place. The election reads; it does not write.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The comparison
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_router_id_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;router_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;router_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_pick_highest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;InterfaceCandidate&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;InterfaceCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;_router_id_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_id&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The entire ranking rule of this election is one tuple: &lt;code&gt;(priority, router_id_key)&lt;/code&gt;. Python's tuple comparison does the rest — compare priority first, and only if priority ties does &lt;code&gt;router_id&lt;/code&gt; get consulted. This is the "why does router ID still matter after priority" answer, sitting right there in the key function.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_router_id_key&lt;/code&gt; deserves a second look, because it's the kind of thing that's easy to get wrong and never notice. A router ID looks like an IPv4 address, but it's a string. Compare &lt;code&gt;"10.0.0.2"&lt;/code&gt; and &lt;code&gt;"10.0.0.10"&lt;/code&gt; as strings and &lt;code&gt;"10.0.0.2"&lt;/code&gt; wins, because &lt;code&gt;"2" &amp;gt; "1"&lt;/code&gt; lexicographically. Splitting on &lt;code&gt;.&lt;/code&gt; and mapping to &lt;code&gt;int&lt;/code&gt; gives you &lt;code&gt;(10, 0, 0, 2)&lt;/code&gt; vs &lt;code&gt;(10, 0, 0, 10)&lt;/code&gt;, and now the numeric order is correct. The election is deterministic either way — every router would agree on the wrong answer consistently — but it wouldn't match what a real OSPF implementation picks.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;if not candidates: return None&lt;/code&gt; guard is not decoration. &lt;code&gt;max()&lt;/code&gt; on an empty sequence raises &lt;code&gt;ValueError&lt;/code&gt;, and an empty candidate list is a completely legitimate state — a segment where every router is configured with priority &lt;code&gt;0&lt;/code&gt;. The &lt;code&gt;None&lt;/code&gt; return is what lets the caller express "no DR on this segment" as a value rather than an exception.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. The election
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;elect_dr_bdr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;InterfaceCandidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ElectionResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;eligible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;declared_dr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eligible&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;declared_dr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;designated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_pick_highest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;declared_dr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;_pick_highest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eligible&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;designated&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;designated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;declared_bdr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;declared_bdr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;backup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_pick_highest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;declared_bdr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;_pick_highest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as three filters and two picks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The eligibility filter.&lt;/strong&gt; &lt;code&gt;priority &amp;gt; 0&lt;/code&gt; is the first line, and it's absolute. A router with priority &lt;code&gt;0&lt;/code&gt; doesn't lose the election — it never enters it. It's gone from &lt;code&gt;eligible&lt;/code&gt;, which means it's gone from &lt;code&gt;declared_dr&lt;/code&gt;, gone from &lt;code&gt;remaining&lt;/code&gt;, gone from &lt;code&gt;declared_bdr&lt;/code&gt;, and gone from &lt;code&gt;eligible_routers&lt;/code&gt; in the result. Priority &lt;code&gt;0&lt;/code&gt; is not "lowest priority," it's opt-out. If you've ever wondered why the OSPF docs describe &lt;code&gt;priority 0&lt;/code&gt; as "ineligible" rather than "last in line," this list comprehension is the reason: there is no code path that can reach a zero-priority router after line one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The declared-role preference.&lt;/strong&gt; This is the line to slow down on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;designated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_pick_highest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;declared_dr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;_pick_highest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;or&lt;/code&gt; short-circuits on truthiness. If anyone on the segment is already claiming to be DR, &lt;code&gt;declared_dr&lt;/code&gt; is non-empty, &lt;code&gt;_pick_highest&lt;/code&gt; returns a candidate, and the second &lt;code&gt;_pick_highest(eligible)&lt;/code&gt; never runs. &lt;strong&gt;The general field is only consulted when nobody is already claiming the role.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That single &lt;code&gt;or&lt;/code&gt; is the seed of non-preemption. A brand-new router with priority 255 boots onto a segment where a priority-1 router is already declaring itself DR. It does not win. It isn't in &lt;code&gt;declared_dr&lt;/code&gt;, so it's never compared against the incumbent at all. Priority only decides &lt;em&gt;who becomes&lt;/em&gt; DR, not &lt;em&gt;who stays&lt;/em&gt; DR — and the code makes that structural rather than conditional. There's no &lt;code&gt;if incumbent_exists&lt;/code&gt; branch; the preference falls out of which list gets passed to &lt;code&gt;max()&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exclusion, then the same pattern again.&lt;/strong&gt; BDR selection is the DR logic run a second time over a smaller set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;eligible&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;designated&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;designated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The filter excludes the winner &lt;em&gt;by router ID&lt;/em&gt;, not by object identity. Since &lt;code&gt;router_id&lt;/code&gt; is unique on a segment, that's the correct key — and it's more robust than &lt;code&gt;is&lt;/code&gt;, which would break the moment a candidate was reconstructed rather than passed through. The &lt;code&gt;designated is None&lt;/code&gt; clause keeps &lt;code&gt;remaining&lt;/code&gt; equal to &lt;code&gt;eligible&lt;/code&gt; when there was no DR to exclude, though in practice if &lt;code&gt;_pick_highest(eligible)&lt;/code&gt; returned &lt;code&gt;None&lt;/code&gt; then &lt;code&gt;eligible&lt;/code&gt; was empty and &lt;code&gt;remaining&lt;/code&gt; will be too.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;declared_bdr&lt;/code&gt; filtered from &lt;code&gt;remaining&lt;/code&gt;, &lt;code&gt;_pick_highest(...) or _pick_highest(...)&lt;/code&gt; again. Same shape, one level down. Note what this &lt;em&gt;doesn't&lt;/em&gt; do: the current DR is excluded from BDR consideration, but there's no promotion logic — if the DR disappears, this function just re-runs and the old BDR, still carrying &lt;code&gt;declared_bdr=True&lt;/code&gt;, wins the &lt;code&gt;declared_dr&lt;/code&gt; check... except it doesn't, because it declared &lt;em&gt;BDR&lt;/em&gt;, not &lt;em&gt;DR&lt;/em&gt;. Sit with that one; it's a real gap between this model and the RFC, and we'll come back to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The result
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ElectionResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;designated_router&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;designated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_id&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;designated&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;backup_designated_router&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;backup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_id&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;backup&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;eligible_routers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_id&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_router_id_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;router_id&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two &lt;code&gt;Optional[str]&lt;/code&gt; slots and a sorted tuple. &lt;code&gt;None&lt;/code&gt; for &lt;code&gt;designated_router&lt;/code&gt; is a first-class outcome, not an error — an all-priority-&lt;code&gt;0&lt;/code&gt; segment genuinely has no DR.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eligible_routers&lt;/code&gt; is sorted by the same &lt;code&gt;_router_id_key&lt;/code&gt;, which makes the result stable and comparable across runs regardless of the input ordering. That's a small thing that matters a lot: an election whose &lt;em&gt;output representation&lt;/em&gt; depends on input order is an election you can't diff, snapshot, or test reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same shape, different protocol
&lt;/h2&gt;

&lt;p&gt;Once you've read this election as &lt;code&gt;filter → prefer-incumbent → max → tie-break&lt;/code&gt;, you start seeing it everywhere.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;_pick_highest(declared) or _pick_highest(all)&lt;/code&gt; pattern — &lt;em&gt;prefer whoever is already holding the role, fall back to the best available&lt;/em&gt; — is structurally the same as a &lt;strong&gt;BGP route-selection&lt;/strong&gt; tie-break chain, where the oldest established path wins among otherwise-equal candidates specifically to stop routes from flapping. Different protocol, different fields, identical motivation: stability is worth more than optimality once you're already converged.&lt;/p&gt;

&lt;p&gt;And the &lt;code&gt;priority == 0&lt;/code&gt; filter is the same move as a &lt;strong&gt;DNS&lt;/strong&gt; nameserver marked unreachable, or a &lt;strong&gt;TCP&lt;/strong&gt; connection with &lt;code&gt;cwnd&lt;/code&gt; collapsed to the point of exclusion — a participant removing itself from consideration entirely rather than competing badly. Filtering before ranking is cheaper and safer than ranking with a special-case loser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The walkthrough is executable. From the repo root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/ospf/session_03_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the script at &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/examples/ospf/session_03_walkthrough.py" rel="noopener noreferrer"&gt;&lt;code&gt;examples/ospf/session_03_walkthrough.py&lt;/code&gt;&lt;/a&gt;, then change the inputs. The instructive experiments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build a candidate set where the highest-priority router has &lt;code&gt;declared_dr=False&lt;/code&gt; and a low-priority router has &lt;code&gt;declared_dr=True&lt;/code&gt;. Confirm the low-priority router wins.&lt;/li&gt;
&lt;li&gt;Set every priority to &lt;code&gt;0&lt;/code&gt; and check what comes back in all three fields of &lt;code&gt;ElectionResult&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Give two routers the same priority and different router IDs, one of them &lt;code&gt;10.0.0.9&lt;/code&gt; and the other &lt;code&gt;10.0.0.10&lt;/code&gt;. Verify which wins, and convince yourself it's &lt;code&gt;_router_id_key&lt;/code&gt; doing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This is a teaching model, and being clear about the gap is the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's a snapshot, not a state machine.&lt;/strong&gt; The real election in RFC 2328 §9.4 runs as an eight-step algorithm inside the interface state machine, and it &lt;em&gt;runs twice&lt;/em&gt; — the second pass exists precisely so that a router that just made itself DR can then correctly select a BDR from the updated set. &lt;code&gt;elect_dr_bdr()&lt;/code&gt; is a single pure pass over immutable inputs. There is no re-invocation, no convergence loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-preemption is only half-modeled.&lt;/strong&gt; The &lt;code&gt;declared_dr&lt;/code&gt; preference captures the &lt;em&gt;spirit&lt;/em&gt; of non-preemption, but the real rules are more specific: a router that is already DR does not give up the role, and a router that becomes BDR is promoted to DR when the DR fails. This model has no promotion path — a former BDR carries &lt;code&gt;declared_bdr=True&lt;/code&gt;, which does nothing for it in the &lt;code&gt;declared_dr&lt;/code&gt; filter. Re-running the election after a DR failure gives you a plausible answer, but not the RFC's answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No Wait Timer.&lt;/strong&gt; Real OSPF interfaces sit in &lt;code&gt;Waiting&lt;/code&gt; state for &lt;code&gt;RouterDeadInterval&lt;/code&gt; before electing anything, specifically to avoid a router electing itself DR just because it booted first and hasn't heard anyone else yet. There's no time in this model at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No &lt;code&gt;Hello&lt;/code&gt; packet, no priority advertisement.&lt;/strong&gt; &lt;code&gt;InterfaceCandidate&lt;/code&gt; is handed to us pre-assembled. In reality every field here is learned from &lt;code&gt;Hello&lt;/code&gt; packets on the segment, and the &lt;code&gt;declared_dr&lt;/code&gt;/&lt;code&gt;declared_bdr&lt;/code&gt; values are literally the &lt;code&gt;Designated Router&lt;/code&gt; and &lt;code&gt;Backup Designated Router&lt;/code&gt; fields of those packets — which means they can be stale, inconsistent between routers, or briefly contradictory during convergence. This model assumes every router sees the same candidate tuple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No interface types.&lt;/strong&gt; DR/BDR election applies to broadcast and NBMA networks. Point-to-point links don't elect anything. The model has no concept of network type, so it will happily elect a DR for a segment that shouldn't have one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Don't answer these from memory — answer them by pointing at a line in &lt;code&gt;dr_election.py&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A router configured with &lt;code&gt;priority 0&lt;/code&gt; sends a Hello with &lt;code&gt;declared_dr=True&lt;/code&gt;. Trace it through &lt;code&gt;elect_dr_bdr()&lt;/code&gt;: at which exact line does it stop mattering, and what does &lt;code&gt;eligible_routers&lt;/code&gt; contain?&lt;/li&gt;
&lt;li&gt;Two routers on the segment both have &lt;code&gt;declared_dr=True&lt;/code&gt; — a genuinely possible state during convergence. Which one becomes DR, and which comparison in &lt;code&gt;_pick_highest&lt;/code&gt; decides it? Can the loser end up as BDR?&lt;/li&gt;
&lt;li&gt;The current DR fails and drops off the segment. You re-run &lt;code&gt;elect_dr_bdr()&lt;/code&gt; with the surviving candidates, one of which has &lt;code&gt;declared_bdr=True&lt;/code&gt;. Does that router become the new DR? Read the filters and follow the &lt;code&gt;or&lt;/code&gt;, then say what the RFC would have done instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Done when&lt;/strong&gt; you can explain, without looking, why priority &lt;code&gt;0&lt;/code&gt; disappears from the candidate set entirely rather than losing, and why &lt;code&gt;router_id&lt;/code&gt; still decides the outcome after priority has had its say.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc2328" rel="noopener noreferrer"&gt;RFC 2328&lt;/a&gt; — OSPF Version 2. §9.4 is the DR election algorithm; §7.3 explains why the Designated Router exists at all.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc2328#section-9.5" rel="noopener noreferrer"&gt;RFC 2328 §9.5&lt;/a&gt; — Hello packet contents, including the &lt;code&gt;Designated Router&lt;/code&gt; and &lt;code&gt;Backup Designated Router&lt;/code&gt; fields this model receives as &lt;code&gt;declared_dr&lt;/code&gt; / &lt;code&gt;declared_bdr&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/ospf/dr_election.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/ospf/dr_election.py&lt;/code&gt;&lt;/a&gt; — the file this article reads.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>ospf</category>
      <category>protocol</category>
    </item>
    <item>
      <title>A Hello does not make a neighbor Full — the code says so in two stages</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Wed, 05 Aug 2026 00:00:11 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/a-hello-does-not-make-a-neighbor-full-the-code-says-so-in-two-stages-4bn3</link>
      <guid>https://dev.to/pathvector-dev/a-hello-does-not-make-a-neighbor-full-the-code-says-so-in-two-stages-4bn3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-ospf-02/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-ospf-02/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic — inputs, state, and branches — rather than as configuration examples. The source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;github.com/pathvector-studio/protocol-in-code&lt;/a&gt;, and every article quotes directly from a small Python module you can read end to end in one sitting. If you're earlier on the path and want to &lt;em&gt;do&lt;/em&gt; things with protocols before dissecting them, start with the hands-on companion series, &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How does a received Hello turn into &lt;code&gt;Init&lt;/code&gt;, &lt;code&gt;2-Way&lt;/code&gt;, or &lt;code&gt;Full&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question contains a trap, and the trap is the whole point of this session. Read enough vendor documentation and you absorb a mental model where a Hello packet arrives, some negotiation happens, and the neighbor "comes up" — one continuous process with &lt;code&gt;Full&lt;/code&gt; at the end of it. Then you hit a network where two routers sit at &lt;code&gt;2-Way&lt;/code&gt; forever and it looks like a failure, or a pair stuck in &lt;code&gt;ExStart&lt;/code&gt; where Hellos are clearly flowing fine.&lt;/p&gt;

&lt;p&gt;The code we're reading this session is &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/ospf/neighbor.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/ospf/neighbor.py&lt;/code&gt;&lt;/a&gt;. It answers the question by refusing to answer it as asked: a Hello can produce &lt;code&gt;Init&lt;/code&gt;, &lt;code&gt;2-Way&lt;/code&gt;, or &lt;code&gt;ExStart&lt;/code&gt;, and that's the end of what a Hello can do. &lt;code&gt;Full&lt;/code&gt; comes from somewhere else entirely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/docs/ospf/session_01.md" rel="noopener noreferrer"&gt;Session 01&lt;/a&gt; left us with two booleans: whether a Hello is acceptable, and whether the peer already lists us in its neighbor set. This session picks up exactly there, turns those booleans into a state, and then — separately — brings in the database-exchange inputs.&lt;/p&gt;

&lt;p&gt;Read the file in this order: &lt;code&gt;NeighborState&lt;/code&gt;, then &lt;code&gt;AdjacencyInputs&lt;/code&gt;, then &lt;code&gt;advance_neighbor_state()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state space, and what it's hiding
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;DOWN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Down&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;INIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Init&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;TWO_WAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2-Way&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;EXSTART&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ExStart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;EXCHANGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exchange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;LOADING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loading&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;FULL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Full&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven states, in the order OSPF traverses them. Nothing surprising yet — but notice there's a seam in this list even though the enum doesn't mark it. &lt;code&gt;DOWN&lt;/code&gt;, &lt;code&gt;INIT&lt;/code&gt;, and &lt;code&gt;TWO_WAY&lt;/code&gt; are outcomes of looking at Hello packets. &lt;code&gt;EXCHANGE&lt;/code&gt;, &lt;code&gt;LOADING&lt;/code&gt;, and &lt;code&gt;FULL&lt;/code&gt; are outcomes of exchanging link-state databases. &lt;code&gt;EXSTART&lt;/code&gt; is the hinge: it's where the Hello phase hands off to the database phase.&lt;/p&gt;

&lt;p&gt;The input struct makes that seam explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AdjacencyInputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;hello_accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;saw_self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;should_form_full_adjacency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;database_description_ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;request_list_empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;retransmissions_cleared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six booleans, and they split cleanly into two groups. The first three come from Hello processing. The last three come from the database exchange, and they all default to &lt;code&gt;True&lt;/code&gt; — which is a small design decision worth pausing on, because it means an &lt;code&gt;AdjacencyInputs&lt;/code&gt; constructed with only the Hello fields describes a neighbor whose database exchange has already gone perfectly. That's a convenience for tests, not a claim about reality; the helper that builds inputs from a real Hello result flips those defaults the other way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;inputs_from_hello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HelloCheckResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;should_form_full_adjacency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;database_description_ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;request_list_empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;retransmissions_cleared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AdjacencyInputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same three fields, defaults inverted. When all you have is a Hello, you have no evidence about the database at all, so the honest default is "not yet."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;should_form_full_adjacency&lt;/code&gt; is an &lt;em&gt;input&lt;/em&gt;, not a computed result. In real OSPF it's the output of DR/BDR election on a broadcast segment, plus network-type rules — a router on an Ethernet segment forms full adjacencies with the DR and BDR and stops at &lt;code&gt;2-Way&lt;/code&gt; with everyone else. This model takes that decision as given so the state machine stays readable. Where that boolean comes from is its own problem; what it &lt;em&gt;does&lt;/em&gt; is what we're reading here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Stage one: what a Hello can decide
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;advance_on_hello()&lt;/code&gt; is a chain of guard clauses, and reading it top to bottom is reading a priority order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;advance_on_hello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hello_accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;saw_self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;should_form_full_adjacency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hello_accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOWN&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;saw_self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INIT&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;should_form_full_adjacency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TWO_WAY&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INIT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TWO_WAY&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXSTART&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four branches, and each one answers a different question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;hello_accepted&lt;/code&gt; is a hard floor.&lt;/strong&gt; A Hello that fails the Session 01 checks — mismatched area, hello/dead interval disagreement, authentication failure, wrong netmask — doesn't leave the neighbor where it was. It drops it to &lt;code&gt;DOWN&lt;/code&gt;. There is no partial credit and no memory of how far the relationship had progressed. This is worth internalizing because it explains a class of production symptom: a neighbor that was &lt;code&gt;Full&lt;/code&gt; for months collapsing all the way to &lt;code&gt;DOWN&lt;/code&gt; after someone changes a timer on one side. The code has no branch for "was Full, so be lenient."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;saw_self&lt;/code&gt; is the one-way/two-way test.&lt;/strong&gt; We received a valid Hello, so we know they're transmitting and we can hear them. What we don't know is whether they can hear &lt;em&gt;us&lt;/em&gt;. The only evidence for that is our own Router ID appearing in the neighbor list of the Hello they sent. Without it, &lt;code&gt;INIT&lt;/code&gt; — the state that means "I hear you, unconfirmed whether you hear me." This is the classic unidirectional-link signature, and it's why &lt;code&gt;INIT&lt;/code&gt; exists as a distinct state rather than being folded into &lt;code&gt;DOWN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;should_form_full_adjacency&lt;/code&gt; is the terminal branch that isn't a failure.&lt;/strong&gt; Bidirectional communication is confirmed, and if the answer here is "no," the function returns &lt;code&gt;TWO_WAY&lt;/code&gt; — and that's the final answer. Not a stop on the way somewhere; the destination. Two routers on a broadcast segment that are both DROTHERs will sit at &lt;code&gt;2-Way&lt;/code&gt; indefinitely, exchanging Hellos, never exchanging databases, and that is the protocol working correctly. They don't need a full adjacency with each other because they'll both get the same LSAs through the DR.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The last two lines are the interesting ones.&lt;/strong&gt; Once we've decided a full adjacency is wanted, the function checks where we currently are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INIT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TWO_WAY&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXSTART&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the neighbor is still in a Hello-phase state, we promote it to &lt;code&gt;EXSTART&lt;/code&gt; — the handoff point. But if it's already past that (&lt;code&gt;EXCHANGE&lt;/code&gt;, &lt;code&gt;LOADING&lt;/code&gt;, &lt;code&gt;FULL&lt;/code&gt;), we return &lt;code&gt;current&lt;/code&gt; unchanged. A valid Hello arriving at an established neighbor does not restart the database exchange. That bare &lt;code&gt;return current&lt;/code&gt; is the entire mechanism that makes OSPF adjacencies stable under ordinary Hello traffic: every 10 seconds a Hello comes in, gets validated, and lands on a line that says "change nothing."&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage two: what the database exchange decides
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;advance_neighbor_state()&lt;/code&gt; composes the two phases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;advance_neighbor_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjacencyInputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;advance_on_hello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hello_accepted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hello_accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;saw_self&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;saw_self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;should_form_full_adjacency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;should_form_full_adjacency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOWN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INIT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TWO_WAY&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;
    &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hello phase runs first, unconditionally. Then a gate: if the Hello phase landed on &lt;code&gt;DOWN&lt;/code&gt;, &lt;code&gt;INIT&lt;/code&gt;, or &lt;code&gt;TWO_WAY&lt;/code&gt;, we return immediately and the database-exchange inputs are never consulted. They're present in the struct, they may be fully populated, and the function ignores them completely.&lt;/p&gt;

&lt;p&gt;This is the structural answer to the question we opened with. &lt;code&gt;hello_accepted=True, saw_self=True, should_form_full_adjacency=False&lt;/code&gt; returns &lt;code&gt;TWO_WAY&lt;/code&gt; no matter what the other three booleans say. There is no path from a Hello to &lt;code&gt;Full&lt;/code&gt; that skips this gate.&lt;/p&gt;

&lt;p&gt;Past the gate, we're in database territory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;database_description_ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXSTART&lt;/span&gt;

    &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EXCHANGE&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_list_empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOADING&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retransmissions_cleared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOADING&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NeighborState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FULL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;database_description_ok&lt;/code&gt; covers master/slave negotiation and DD packet agreement — MTU match, sequence number sync, options compatibility. Fail it and you stay at &lt;code&gt;EXSTART&lt;/code&gt;. Note that this branch &lt;em&gt;pins&lt;/em&gt; you there rather than dropping you to &lt;code&gt;DOWN&lt;/code&gt;: the Hellos are fine, bidirectional communication is fine, and the peer is still very much a neighbor. What's broken is one layer up. A pair of routers stuck at &lt;code&gt;ExStart&lt;/code&gt; with healthy Hello traffic is exactly this branch firing repeatedly, and the classic cause — an MTU mismatch — is one you'll never find by looking at Hello packets.&lt;/p&gt;

&lt;p&gt;Past that, &lt;code&gt;current&lt;/code&gt; is assigned &lt;code&gt;EXCHANGE&lt;/code&gt; and then two conditions can knock it down to &lt;code&gt;LOADING&lt;/code&gt;. &lt;code&gt;request_list_empty&lt;/code&gt; asks whether we still have LSAs we've requested but not received. &lt;code&gt;retransmissions_cleared&lt;/code&gt; asks whether we have LSAs we sent that haven't been acknowledged. Either one non-empty means the databases aren't synchronized yet, in either direction, and &lt;code&gt;LOADING&lt;/code&gt; is the state for "still reconciling."&lt;/p&gt;

&lt;p&gt;Only when all six booleans line up does the function return &lt;code&gt;FULL&lt;/code&gt;. Read the branches as a conjunction and &lt;code&gt;FULL&lt;/code&gt; is the &lt;em&gt;absence&lt;/em&gt; of every objection — not a positive achievement, but the state you fall through to when nothing is left to complain about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The walkthrough is executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/ospf/session_02_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it, then start changing inputs. The most useful experiment is constructing an &lt;code&gt;AdjacencyInputs&lt;/code&gt; with every database field &lt;code&gt;True&lt;/code&gt; and &lt;code&gt;should_form_full_adjacency=False&lt;/code&gt;, and confirming that the result is &lt;code&gt;TWO_WAY&lt;/code&gt;. Then set &lt;code&gt;should_form_full_adjacency=True&lt;/code&gt; with &lt;code&gt;database_description_ok=False&lt;/code&gt; and watch it pin at &lt;code&gt;EXSTART&lt;/code&gt;. Two calls, and the two-stage structure stops being an abstraction.&lt;/p&gt;

&lt;p&gt;The other experiment worth doing: call &lt;code&gt;advance_neighbor_state()&lt;/code&gt; with &lt;code&gt;current=NeighborState.FULL&lt;/code&gt; and a perfectly good set of inputs, then with &lt;code&gt;current=NeighborState.FULL&lt;/code&gt; and &lt;code&gt;hello_accepted=False&lt;/code&gt;. The second one collapses to &lt;code&gt;DOWN&lt;/code&gt; in a single call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This model is deliberately smaller than the real state machine, and the gaps matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There are no events.&lt;/strong&gt; RFC 2328 defines the neighbor state machine in terms of events — &lt;code&gt;HelloReceived&lt;/code&gt;, &lt;code&gt;Start&lt;/code&gt;, &lt;code&gt;2-WayReceived&lt;/code&gt;, &lt;code&gt;NegotiationDone&lt;/code&gt;, &lt;code&gt;ExchangeDone&lt;/code&gt;, &lt;code&gt;LoadingDone&lt;/code&gt;, &lt;code&gt;AdjOK?&lt;/code&gt;, &lt;code&gt;SeqNumberMismatch&lt;/code&gt;, &lt;code&gt;BadLSReq&lt;/code&gt;, &lt;code&gt;KillNbr&lt;/code&gt;, &lt;code&gt;InactivityTimer&lt;/code&gt;, &lt;code&gt;1-WayReceived&lt;/code&gt; — each with a defined action and a defined resulting state. This code has no event type at all. It takes a snapshot of six booleans and computes a state from scratch. That means it can express &lt;em&gt;where a neighbor should be given current conditions&lt;/em&gt;, but it cannot express &lt;em&gt;what happens on a specific trigger&lt;/em&gt;, and it has no place to hang the side effects the real machine specifies (start the inactivity timer, clear the LSA lists, send a DD packet).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timers are absent.&lt;/strong&gt; No dead interval, no inactivity timer, no retransmit interval, no RouterDeadInterval expiry path. In real OSPF, a neighbor goes &lt;code&gt;DOWN&lt;/code&gt; because Hellos &lt;em&gt;stopped arriving&lt;/em&gt; for dead-interval seconds — an absence of input over time. This model can only express &lt;code&gt;hello_accepted=False&lt;/code&gt;, a Hello that arrived and failed. The most common cause of a neighbor going down in production is the one the model structurally cannot represent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;EXCHANGE&lt;/code&gt; is a pass-through, not a real state.&lt;/strong&gt; Look closely: &lt;code&gt;current = NeighborState.EXCHANGE&lt;/code&gt; is assigned and then immediately either overwritten by &lt;code&gt;LOADING&lt;/code&gt; or fallen through to &lt;code&gt;FULL&lt;/code&gt;. The function can never &lt;em&gt;return&lt;/em&gt; &lt;code&gt;EXCHANGE&lt;/code&gt;. In real OSPF, &lt;code&gt;Exchange&lt;/code&gt; is where DD packets are actually traded and it can persist for a meaningful stretch. Here it's a line of code that exists to name the phase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The DR/BDR election is a boolean.&lt;/strong&gt; &lt;code&gt;should_form_full_adjacency&lt;/code&gt; compresses network type, priority comparison, DR/BDR election, and the wait timer into one input. That's a substantial subsystem reduced to &lt;code&gt;True&lt;/code&gt;/&lt;code&gt;False&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing is per-interface or per-LSA.&lt;/strong&gt; No interface state machine (&lt;code&gt;Waiting&lt;/code&gt;, &lt;code&gt;DR&lt;/code&gt;, &lt;code&gt;BDR&lt;/code&gt;, &lt;code&gt;DROther&lt;/code&gt;), no LSA-level detail behind &lt;code&gt;request_list_empty&lt;/code&gt; and &lt;code&gt;retransmissions_cleared&lt;/code&gt;, no graceful restart, no &lt;code&gt;AdjOK?&lt;/code&gt; re-evaluation when a DR changes under an established adjacency.&lt;/p&gt;

&lt;p&gt;What the model &lt;em&gt;does&lt;/em&gt; preserve, and preserves precisely, is the two-phase structure and the guard ordering. That's the thing worth carrying to real routers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same shape, different protocol
&lt;/h2&gt;

&lt;p&gt;Once you see the shape here — &lt;em&gt;reachability confirmed, then capability negotiated, then state synchronized, with separate gates and separate failure modes at each&lt;/em&gt; — you start finding it everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BGP&lt;/strong&gt; is the closest match. TCP connects (&lt;code&gt;Connect&lt;/code&gt;), OPEN messages are exchanged and validated (&lt;code&gt;OpenSent&lt;/code&gt; → &lt;code&gt;OpenConfirm&lt;/code&gt;), and only then does the session reach &lt;code&gt;Established&lt;/code&gt; and begin exchanging routes. A BGP session stuck in &lt;code&gt;OpenSent&lt;/code&gt; is the same class of problem as OSPF stuck in &lt;code&gt;ExStart&lt;/code&gt;: the transport is fine, the parameter negotiation isn't. And just as a bad Hello drops OSPF to &lt;code&gt;DOWN&lt;/code&gt; regardless of prior state, a NOTIFICATION drops BGP to &lt;code&gt;Idle&lt;/code&gt; from anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TCP's own handshake&lt;/strong&gt; is &lt;code&gt;advance_on_hello()&lt;/code&gt; in miniature. &lt;code&gt;SYN&lt;/code&gt; received means "I hear you" — the &lt;code&gt;INIT&lt;/code&gt; equivalent. &lt;code&gt;SYN-ACK&lt;/code&gt; carrying an acknowledgment of &lt;em&gt;your&lt;/em&gt; sequence number is the &lt;code&gt;saw_self&lt;/code&gt; check: proof the peer heard you, not just that you heard the peer. A half-open connection is &lt;code&gt;INIT&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS&lt;/strong&gt; splits the same way at a different layer. The handshake establishes that both sides can talk and agree on parameters; only after &lt;code&gt;Finished&lt;/code&gt; verifies both transcripts does application data flow. A handshake failure at parameter negotiation — no shared cipher suite — is &lt;code&gt;database_description_ok=False&lt;/code&gt; in a different costume: connectivity proven, compatibility not.&lt;/p&gt;

&lt;p&gt;The recurring lesson is that "connected" and "usable" are always separate states, and the states in between exist to name &lt;em&gt;which&lt;/em&gt; of them failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you answer these from the code alone?
&lt;/h2&gt;

&lt;p&gt;Don't look anything up. Go back to &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/ospf/neighbor.py" rel="noopener noreferrer"&gt;&lt;code&gt;neighbor.py&lt;/code&gt;&lt;/a&gt; and trace the branches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Two routers exchange valid Hellos, each sees the other's Router ID in the neighbor list, and both stay at &lt;code&gt;2-Way&lt;/code&gt; indefinitely. Which single line in &lt;code&gt;advance_on_hello()&lt;/code&gt; produced that, and which input would you have to change to move them forward?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A neighbor has been &lt;code&gt;FULL&lt;/code&gt; for a week. One Hello arrives with a mismatched hello interval. What does &lt;code&gt;advance_neighbor_state()&lt;/code&gt; return, and how many lines of the function does it execute before returning?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;advance_neighbor_state()&lt;/code&gt; is called with &lt;code&gt;should_form_full_adjacency=True&lt;/code&gt;, &lt;code&gt;database_description_ok=True&lt;/code&gt;, &lt;code&gt;request_list_empty=True&lt;/code&gt;, and &lt;code&gt;retransmissions_cleared=False&lt;/code&gt;. What comes back, and what does that result tell you about which direction of the database exchange is incomplete?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You're done with this session when you can explain why some neighbors stop at &lt;code&gt;2-Way&lt;/code&gt; — and why that's not a bug — and what still blocks &lt;code&gt;Full&lt;/code&gt; after a neighbor reaches &lt;code&gt;ExStart&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc2328" rel="noopener noreferrer"&gt;RFC 2328&lt;/a&gt; — OSPF Version 2. Section 10 covers the neighbor data structure and the full neighbor state machine; section 10.3 is the state-change table this module compresses.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc5340" rel="noopener noreferrer"&gt;RFC 5340&lt;/a&gt; — OSPF for IPv6, for the same machine with a different addressing model.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/docs/ospf/session_01.md" rel="noopener noreferrer"&gt;Session 01: Hello Acceptance&lt;/a&gt; — where &lt;code&gt;hello_accepted&lt;/code&gt; and &lt;code&gt;saw_self&lt;/code&gt; come from.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>ospf</category>
      <category>protocol</category>
    </item>
    <item>
      <title>An OSPF Hello has to match before it can mean anything</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Tue, 04 Aug 2026 00:00:13 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/an-ospf-hello-has-to-match-before-it-can-mean-anything-2jgl</link>
      <guid>https://dev.to/pathvector-dev/an-ospf-hello-has-to-match-before-it-can-mean-anything-2jgl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-ospf-01/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-ospf-01/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic — inputs, state, branches — rather than as configuration examples. The full source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;github.com/pathvector-studio/protocol-in-code&lt;/a&gt;, and every module is backed by a small Python file you can read end to end in a sitting. If you're earlier in the journey and want hands-on packet-level exercises first, start with the companion &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series and come back here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What has to match before an OSPF Hello can even start a neighbor relationship?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the whole module. Not "how does SPF work," not "what's in an LSA" — just the gate that runs before any of that is on the table. Most OSPF material introduces the Hello packet as a keepalive: routers shout on the wire every 10 seconds, and if you stop hearing one for 40 seconds you tear the neighbor down. That's true, and it's also the least interesting thing about Hello.&lt;/p&gt;

&lt;p&gt;The interesting thing is that Hello is a &lt;strong&gt;filter&lt;/strong&gt;. Before OSPF will consider two routers to be on speaking terms, a set of fields in the received packet has to agree with the local interface's configuration. If they don't, nothing happens — no adjacency, no database exchange, no error to the sender, usually no log line you'll notice. The relationship simply never begins.&lt;/p&gt;

&lt;p&gt;If you've ever stared at two routers that are unmistakably on the same broadcast segment, both configured for OSPF, both sending Hellos, and neither one forming an adjacency — you've been on the wrong side of this filter. The code below is where that lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The source
&lt;/h2&gt;

&lt;p&gt;The file this module reads is &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/ospf/hello.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/ospf/hello.py&lt;/code&gt;&lt;/a&gt;. It's short enough that you can hold all of it in your head, which is the point. Read it in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;InterfaceHelloConfig&lt;/code&gt; — what the local interface believes about itself&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OSPFHelloPacket&lt;/code&gt; — what arrived on the wire&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;evaluate_hello()&lt;/code&gt; — top to bottom, no skipping&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Two sides of a comparison
&lt;/h2&gt;

&lt;p&gt;Start with the two dataclasses, because the shape of the check is already visible in their fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InterfaceHelloConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;local_router_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;area_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;netmask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;hello_interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;dead_interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the local side: five values, all of them things you configured (directly or by inheritance from an interface or process). Note what is &lt;em&gt;not&lt;/em&gt; here — no neighbor list, no state, no timers. This is a config snapshot, frozen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OSPFHelloPacket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;source_router_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;area_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;netmask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;hello_interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;dead_interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;designated_router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;backup_designated_router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;neighbors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The packet side is a superset. Four fields — &lt;code&gt;area_id&lt;/code&gt;, &lt;code&gt;netmask&lt;/code&gt;, &lt;code&gt;hello_interval&lt;/code&gt;, &lt;code&gt;dead_interval&lt;/code&gt; — line up name-for-name with the config. That alignment is not cosmetic; it's the check, written out in the type layout before any function runs. When two structs share a field name in a protocol implementation, ask what compares them.&lt;/p&gt;

&lt;p&gt;The remaining fields (&lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;designated_router&lt;/code&gt;, &lt;code&gt;backup_designated_router&lt;/code&gt;, &lt;code&gt;neighbors&lt;/code&gt;) are the ones OSPF uses &lt;em&gt;after&lt;/em&gt; the gate opens — DR election and neighbor-state tracking. This module deliberately doesn't own those. It only touches &lt;code&gt;neighbors&lt;/code&gt;, and only for one specific reason, which we'll get to.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate, one branch at a time
&lt;/h2&gt;

&lt;p&gt;Here's &lt;code&gt;evaluate_hello()&lt;/code&gt; in full. It is worth reading as a single continuous piece of logic rather than five separate rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate_hello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InterfaceHelloConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OSPFHelloPacket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;HelloCheckResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_router_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_router_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;router_id_loop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;area_id&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;area_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;area_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netmask&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netmask&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;netmask_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hello_interval&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hello_interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello_interval_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dead_interval&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dead_interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dead_interval_mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HelloCheckResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;saw_self&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;saw_self&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_router_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neighbors&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HelloCheckResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;saw_self&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;saw_self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three structural facts jump out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First: the checks accumulate rather than short-circuit.&lt;/strong&gt; Each &lt;code&gt;if&lt;/code&gt; appends to &lt;code&gt;reasons&lt;/code&gt; and falls through to the next. A packet with both a wrong area and a wrong netmask comes back with both strings, not just the first one. This is a deliberate choice about diagnostics — when you're debugging a non-forming adjacency, "area_mismatch" alone might send you chasing one problem while a second one waits behind it. Real implementations vary here; many bail on the first failure and log a single reason. The accumulating form is more honest about the fact that misconfiguration is rarely singular.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second: the first check isn't a mismatch at all.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_router_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_router_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;router_id_loop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every other branch is &lt;code&gt;!=&lt;/code&gt;. This one is &lt;code&gt;==&lt;/code&gt;. If the Hello claims to come from &lt;em&gt;your own&lt;/em&gt; router ID, something is wrong — either you're seeing your own packet reflected back (a bridging loop, a misconfigured switch, a hairpin), or someone else on the segment is configured with the same router ID. Both are pathological, and neither should produce a neighbor. The reason string, &lt;code&gt;router_id_loop&lt;/code&gt;, names the more common cause.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; OSPF router IDs are dotted-quad-shaped but are not addresses. They're identifiers, and nothing on the wire enforces uniqueness. Duplicate router IDs are a configuration error that the protocol can only detect by symptom — and this branch is one of the symptoms.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Third: &lt;code&gt;saw_self&lt;/code&gt; is computed only after acceptance.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;HelloCheckResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;saw_self&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;saw_self&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_router_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neighbors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The early return hardcodes &lt;code&gt;saw_self=False&lt;/code&gt;. Not "unknown," not &lt;code&gt;None&lt;/code&gt; — &lt;code&gt;False&lt;/code&gt;. That's a statement about meaning: if the packet failed the gate, the question "did they list me as a neighbor?" isn't merely unanswered, it's &lt;em&gt;not askable&lt;/em&gt;. A packet from the wrong area doesn't get to tell you anything about your adjacency state, even if your router ID happens to appear in its neighbor list.&lt;/p&gt;

&lt;p&gt;This is the sort of thing that reads as a small implementation detail and is actually a security and correctness boundary. Untrusted input doesn't get to influence state until it's been validated as belonging on this link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;saw_self&lt;/code&gt; is the interesting output
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;accepted&lt;/code&gt; is the obvious result. &lt;code&gt;saw_self&lt;/code&gt; is the one that matters more.&lt;/p&gt;

&lt;p&gt;OSPF's adjacency state machine walks through Down → Init → 2-Way → ExStart → Exchange → Loading → Full. The transition from &lt;strong&gt;Init&lt;/strong&gt; to &lt;strong&gt;2-Way&lt;/strong&gt; is precisely this: &lt;em&gt;I have received a Hello from a neighbor, and that Hello contains my own router ID in its neighbor list.&lt;/em&gt; That's the moment bidirectional reachability is confirmed. Until then, you know they can reach you (you got their packet), but you don't know that you can reach them.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;evaluate_hello()&lt;/code&gt; produces two facts and stops:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;accepted&lt;/code&gt; — this packet belongs on this link&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;saw_self&lt;/code&gt; — bidirectional communication is confirmed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything downstream (DR election, database description exchange, LSA flooding) is fed by those two bits and doesn't appear in this module at all. The function's job is to turn a packet into an input for a state machine it doesn't own.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Same shape, different protocol:&lt;/strong&gt; the "I only trust this relationship once I've seen my own identity reflected back" pattern is not OSPF-specific. It's the same shape as the TCP three-way handshake — SYN proves they can reach you, SYN-ACK carrying your sequence number proves you can reach them. BGP does it too, with the OPEN/KEEPALIVE exchange before the session is Established. Once you've seen the shape in one protocol, the others stop looking like arbitrary ceremony.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The module ships a walkthrough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/ospf/session_01_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're looking for two things in the output: one accepted Hello and one rejected Hello. Read the &lt;code&gt;reasons&lt;/code&gt; tuple on the rejected one and trace it back to the specific &lt;code&gt;if&lt;/code&gt; that produced it.&lt;/p&gt;

&lt;p&gt;The genuinely useful exercise is to edit the walkthrough's inputs. Flip &lt;code&gt;dead_interval&lt;/code&gt; and confirm you get &lt;code&gt;dead_interval_mismatch&lt;/code&gt;. Then flip &lt;code&gt;area_id&lt;/code&gt; &lt;em&gt;as well&lt;/em&gt; and confirm you get both strings in the tuple, in source order — that accumulate-don't-short-circuit behavior is easy to nod along to and easy to get wrong when you predict it. Then set &lt;code&gt;source_router_id&lt;/code&gt; equal to &lt;code&gt;local_router_id&lt;/code&gt; while everything else matches, and watch a packet that agrees on every configured field still get rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this toy model leaves out
&lt;/h2&gt;

&lt;p&gt;This file models the first teaching gate and nothing else. Being specific about the gap is the point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No packet parsing.&lt;/strong&gt; &lt;code&gt;OSPFHelloPacket&lt;/code&gt; arrives as a Python dataclass with typed fields. Real OSPF Hellos are bytes inside an OSPF header inside an IP packet with protocol number 89. Header version, packet length, checksum, and authentication are all validated &lt;em&gt;before&lt;/em&gt; anything in this file would run, and each of those is its own rejection path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No timers.&lt;/strong&gt; &lt;code&gt;hello_interval&lt;/code&gt; and &lt;code&gt;dead_interval&lt;/code&gt; are compared as integers and never used as durations. The actual protocol behavior — send a Hello every &lt;code&gt;hello_interval&lt;/code&gt;, declare the neighbor down after &lt;code&gt;dead_interval&lt;/code&gt; of silence — requires a clock and a scheduler, neither of which exists here. &lt;code&gt;dead_interval&lt;/code&gt; in this model is purely a value that must agree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No adjacency state machine.&lt;/strong&gt; The module stops at &lt;code&gt;accepted&lt;/code&gt; and &lt;code&gt;saw_self&lt;/code&gt;. There is no &lt;code&gt;Down&lt;/code&gt;, no &lt;code&gt;Init&lt;/code&gt;, no &lt;code&gt;2-Way&lt;/code&gt;. The function computes an input; something else consumes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No DR/BDR election.&lt;/strong&gt; &lt;code&gt;priority&lt;/code&gt;, &lt;code&gt;designated_router&lt;/code&gt;, and &lt;code&gt;backup_designated_router&lt;/code&gt; are carried on the packet and never read by &lt;code&gt;evaluate_hello()&lt;/code&gt;. They're present so the dataclass is honest about what a Hello contains, not because this module uses them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An incomplete match list.&lt;/strong&gt; Real OSPF also requires agreement on the area type (stub/NSSA options bits in the E-bit and N-bit), authentication type and credentials, and MTU during the subsequent Database Description exchange. The netmask check itself doesn't apply on point-to-point links. Five fields is the teaching set, not the specification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No wire behavior on rejection.&lt;/strong&gt; In this model a rejected Hello returns a result object. On a real link it is silently discarded, sometimes with a counter increment. The sender learns nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these omissions change the answer to the core question. They change how much of the real failure surface you've seen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Close this page and answer these from the source file alone:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A packet arrives with a matching area, netmask, and both intervals — and &lt;code&gt;source_router_id&lt;/code&gt; equal to your &lt;code&gt;local_router_id&lt;/code&gt;. Does the function ever look at &lt;code&gt;packet.neighbors&lt;/code&gt;? Trace the exact path through &lt;code&gt;evaluate_hello()&lt;/code&gt; that decides this.&lt;/li&gt;
&lt;li&gt;Two routers on the same segment disagree only on &lt;code&gt;hello_interval&lt;/code&gt;. Which one detects the problem, and what does the other one learn about it? What does the code say about who gets told?&lt;/li&gt;
&lt;li&gt;Why is &lt;code&gt;saw_self&lt;/code&gt; hardcoded to &lt;code&gt;False&lt;/code&gt; in the rejection branch rather than computed and returned alongside the reasons? Construct the concrete case where computing it anyway would be actively wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't take my word for any of it — the file is under 60 lines.&lt;/p&gt;

&lt;p&gt;You'll know you've got this module when you can name, without looking, the fields that must match before adjacency can continue, and explain why seeing your own router ID in a neighbor's list changes the next input to the adjacency state machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc2328" rel="noopener noreferrer"&gt;RFC 2328&lt;/a&gt; — OSPF Version 2. Section 9.5 covers sending Hello packets; Section 10.5 covers receiving them and is the direct analogue of &lt;code&gt;evaluate_hello()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc5340" rel="noopener noreferrer"&gt;RFC 5340&lt;/a&gt; — OSPF for IPv6, for how the same gate is expressed when the address family changes.&lt;/li&gt;
&lt;li&gt;Source file: &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/ospf/hello.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/ospf/hello.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>ospf</category>
      <category>protocol</category>
    </item>
    <item>
      <title>A BGP speaker is just state plus three event handlers</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Mon, 03 Aug 2026 00:00:09 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/a-bgp-speaker-is-just-state-plus-three-event-handlers-1a78</link>
      <guid>https://dev.to/pathvector-dev/a-bgp-speaker-is-just-state-plus-three-event-handlers-1a78</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-bgp-15/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-bgp-15/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic — inputs, state, and branches — rather than as configuration examples. The source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;pathvector-studio/protocol-in-code&lt;/a&gt;, and every article quotes from a Python file you can open and run yourself. If you're earlier in your networking journey and want to type commands and watch packets before you read code, start with the hands-on companion series &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does the smallest readable BGP speaker look like when all the previous sessions are connected?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the question this module turns over. Not "how do I configure a BGP daemon," and not "what does a production speaker do" — but something narrower and, I think, more useful: if you've already written the session gate, the export refresh, the per-event branches, and the policy-aware best-path selection as separate functions, what's actually left to write?&lt;/p&gt;

&lt;p&gt;The answer is smaller than most people expect. The file we're reading, &lt;code&gt;src/protocol_in_code/bgp/speaker.py&lt;/code&gt;, is about 120 lines, and roughly half of them are argument lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything that came before
&lt;/h2&gt;

&lt;p&gt;The speaker isn't inventing anything. It's assembling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session 11&lt;/strong&gt; gave us the session gate — the check that decides whether a peer is even in a state where its announcements count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session 12&lt;/strong&gt; gave us export refresh — recomputing what we advertise outward when the local RIB changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session 13&lt;/strong&gt; gave us event-specific branches — &lt;code&gt;process_announce_event&lt;/code&gt;, &lt;code&gt;process_withdraw_event&lt;/code&gt;, &lt;code&gt;process_peer_down_event&lt;/code&gt;, each with its own shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session 14&lt;/strong&gt; gave us prefix-wide, policy-aware best-path selection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This session wraps those event branches as methods on a single object. That's the whole move.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is the same pattern you'll see in the TCP track when the congestion-control functions get wrapped into a connection object, and in the DNS track when the cache and the resolver loop get joined. A protocol implementation is almost always &lt;em&gt;pure decision functions&lt;/em&gt; plus &lt;em&gt;a mutable bag of state that calls them in order&lt;/em&gt;. Once you see that shape, the "which daemon should I read next" question gets a lot less intimidating.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Read the speaker as a bag of state
&lt;/h2&gt;

&lt;p&gt;Start at the dataclass. Don't read it as a class definition — read it as an inventory of everything a BGP speaker has to remember:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ToyBGPSpeaker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VRP&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PipelinePolicies&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBIn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AdjRIBIn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocRIB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;LocRIB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBOut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AdjRIBOut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PeerSession&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;export_targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ExportTarget&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven fields, and every one of them maps to something you'd point at on a whiteboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;adj_rib_in&lt;/code&gt; — what peers told us, before we judged it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;loc_rib&lt;/code&gt; — what we decided to believe.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;adj_rib_out&lt;/code&gt; — what we're telling other peers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;peers&lt;/code&gt; — session state per neighbor (this is where the Session 11 gate reads from).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;export_targets&lt;/code&gt; — who we advertise to, and under what outbound policy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vrps&lt;/code&gt; and &lt;code&gt;policies&lt;/code&gt; — the validation and policy inputs that the pipeline consults.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The three-RIB split is the part worth sitting with. It's not an implementation detail; it's the reason BGP can be policy-driven at all. If there were one table, "what I heard," "what I chose," and "what I say" would be the same thing, and there'd be nowhere to put a policy decision. The &lt;code&gt;Adj-RIB-In&lt;/code&gt; → &lt;code&gt;Loc-RIB&lt;/code&gt; → &lt;code&gt;Adj-RIB-Out&lt;/code&gt; chain exists precisely so that each arrow can have a filter on it.&lt;/p&gt;

&lt;p&gt;Note also what has &lt;em&gt;no&lt;/em&gt; default: &lt;code&gt;vrps&lt;/code&gt; and &lt;code&gt;policies&lt;/code&gt; are required constructor arguments. You cannot build this speaker without deciding what you validate against and what your policies are. That's a deliberate bit of API design — in the toy model, an unpolicied speaker isn't a thing you can accidentally create.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three-line rhythm
&lt;/h2&gt;

&lt;p&gt;Now read one of the event handlers. Here's &lt;code&gt;receive_announce()&lt;/code&gt; in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_announce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathAttributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SpeakerStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;process_announce_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;AnnounceEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_all_export_targets&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_step_from_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;announce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strip the argument list and it's two statements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;process_announce_event&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_step_from_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;announce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build an event object. Hand it to the pure function from Session 13 along with all the state. Turn the result into a report. That rhythm — &lt;strong&gt;event in, decision function, step out&lt;/strong&gt; — is the entire course in miniature, and it repeats verbatim in the other two handlers.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;receive_withdraw()&lt;/code&gt; is the same shape with one fewer argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_withdraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SpeakerStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;process_withdraw_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;WithdrawEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_all_export_targets&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_step_from_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;withdraw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spot the difference before you read on: &lt;code&gt;self.peers&lt;/code&gt; isn't passed. Announce and peer-down both take it; withdraw doesn't. That asymmetry is not an oversight, and it's the single best thing to reason about in this file. A withdraw doesn't need the session gate, because a withdraw is a &lt;em&gt;removal&lt;/em&gt; — there's nothing to validate, no attributes to run through policy, no decision about whether the peer is allowed to say this. Announce needs the gate because accepting a path is a judgment call. Peer-down needs the peers dict because it &lt;em&gt;mutates&lt;/em&gt; it — the session state gets downgraded as part of the event.&lt;/p&gt;

&lt;p&gt;Read the argument lists as a statement about what each event is allowed to touch. That's a habit worth carrying into real codebases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result type
&lt;/h2&gt;

&lt;p&gt;Every handler returns the same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpeakerStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;prefixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
    &lt;span class="n"&gt;installed_paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;export_changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ExportChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the conversion is mechanical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_step_from_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EventResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SpeakerStep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SpeakerStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;prefixes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;touched_prefixes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;installed_paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_paths&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;export_changes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;export_changes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks like pointless indirection until you ask what it's for. &lt;code&gt;EventResult&lt;/code&gt; is the internal vocabulary of the event layer; &lt;code&gt;SpeakerStep&lt;/code&gt; is the speaker's public answer to "what just happened?" Keeping them separate means the event functions can grow new internal fields without changing what callers see, and it gives you one uniform shape to log or assert against regardless of which event fired.&lt;/p&gt;

&lt;p&gt;The field types carry information too. &lt;code&gt;installed_paths&lt;/code&gt; is &lt;code&gt;dict[str, PathCandidate | None]&lt;/code&gt; — the &lt;code&gt;None&lt;/code&gt; is load-bearing. A prefix mapping to &lt;code&gt;None&lt;/code&gt; means "this prefix was touched and now has no best path," which is a genuinely different outcome from the prefix not appearing in the dict at all. &lt;code&gt;accepted&lt;/code&gt; being a single bool, meanwhile, tells you these events are all-or-nothing at the speaker level: an announce either got through the gate and the pipeline or it didn't.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;export_changes&lt;/code&gt; is where the Session 12 work surfaces. A single announce can ripple into multiple outbound advertisements — one per export target whose view of the prefix changed — which is why it's a tuple rather than a single value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring and running it
&lt;/h2&gt;

&lt;p&gt;Two small setup methods complete the object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_neighbor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BGPSessionConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open_peer_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_export_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExportTarget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;export_targets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_all_export_targets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ExportTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;export_targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are dict inserts keyed by &lt;code&gt;peer_id&lt;/code&gt;, and &lt;code&gt;_all_export_targets()&lt;/code&gt; freezes the values into a tuple on every call — the event functions receive a snapshot they can't mutate, which keeps the "pure function" contract honest even though the speaker itself is mutable.&lt;/p&gt;

&lt;p&gt;The walkthrough is runnable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/bgp/session_15_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch for five things as it steps through:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;first announce&lt;/strong&gt; installing one path — &lt;code&gt;installed_paths&lt;/code&gt; goes from nothing to a &lt;code&gt;PathCandidate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;second announce&lt;/strong&gt; either keeping or changing the best path — this is Session 14's comparison logic firing.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;withdraw&lt;/strong&gt; moving the prefix to the remaining peer — the &lt;code&gt;Loc-RIB&lt;/code&gt; re-selects from what's left in &lt;code&gt;Adj-RIB-In&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peer-down&lt;/strong&gt; removing the last remaining path and downgrading the peer's session state — one event mutating two kinds of state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound advertisements&lt;/strong&gt; changing as each of the above happens — &lt;code&gt;export_changes&lt;/code&gt; should be non-empty exactly when the &lt;code&gt;Loc-RIB&lt;/code&gt; decision actually changed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last one is the interesting audit. If you see a &lt;code&gt;Loc-RIB&lt;/code&gt; change with no corresponding export change, either a policy filtered it or you've found a bug. Both are worth chasing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This is a teaching model, and being precise about what it &lt;em&gt;isn't&lt;/em&gt; matters more here than in earlier sessions — because "a BGP speaker" sounds like a complete thing, and this isn't one.&lt;/p&gt;

&lt;p&gt;What's absent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No timers.&lt;/strong&gt; No hold timer, no keepalive, no MRAI (Minimum Route Advertisement Interval). Real speakers damp their advertisement rate; this one advertises the instant a decision changes. A significant fraction of real BGP behavior — including a good chunk of convergence dynamics — lives in those timers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No packet parsing.&lt;/strong&gt; &lt;code&gt;receive_announce()&lt;/code&gt; takes a &lt;code&gt;PathAttributes&lt;/code&gt; object, not bytes. There is no wire format here, no TLV walking, no length validation, none of the malformed-attribute error handling that RFC 4271 spends real pages on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No capability negotiation.&lt;/strong&gt; No OPEN message, no capability exchange, no AFI/SAFI negotiation. &lt;code&gt;add_neighbor()&lt;/code&gt; just constructs a session and puts it in a dict.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No UPDATE encoding.&lt;/strong&gt; &lt;code&gt;export_changes&lt;/code&gt; describes decisions, not messages. Nothing here would go on a socket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No FSM.&lt;/strong&gt; Real BGP has a six-state finite state machine (Idle, Connect, Active, OpenSent, OpenConfirm, Established). The toy model has a session gate and a &lt;code&gt;peer_down()&lt;/code&gt; that downgrades state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No transport.&lt;/strong&gt; No TCP connection, no port 179, no MD5 or TCP-AO.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this model &lt;em&gt;does&lt;/em&gt; show is the control-plane loop shape: an event arrives, state is consulted, a decision function runs, RIBs move, and outbound state is recomputed. That shape is real, and it's the part that stays the same across implementations. Everything in the list above is a layer wrapped around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same shape, different protocol
&lt;/h2&gt;

&lt;p&gt;Once you can see &lt;code&gt;ToyBGPSpeaker&lt;/code&gt; as &lt;em&gt;mutable state + pure event functions + a uniform result type&lt;/em&gt;, you'll find the pattern everywhere. A TCP connection object is a bag of state (&lt;code&gt;cwnd&lt;/code&gt;, &lt;code&gt;ssthresh&lt;/code&gt;, sequence numbers, retransmission queue) with event handlers for ACK-received, timeout-fired, and data-queued — each one consulting state, running a decision function, and returning what changed. A DNS resolver is a cache plus handlers for query-received and response-received.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Adj-RIB-In&lt;/code&gt; / &lt;code&gt;Loc-RIB&lt;/code&gt; / &lt;code&gt;Adj-RIB-Out&lt;/code&gt; split has its own echoes: it's the same "raw input, decided truth, published view" separation you see in a TLS session cache holding tickets separately from the live parameters actually negotiated, or in conntrack keeping the observed tuple separate from the translated one it emits.&lt;/p&gt;

&lt;p&gt;Different protocol, different field names, same three moving parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Before you close the file, see whether you can answer these from the source alone — no running required:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;receive_withdraw()&lt;/code&gt; doesn't receive &lt;code&gt;self.peers&lt;/code&gt;, but &lt;code&gt;receive_announce()&lt;/code&gt; and &lt;code&gt;peer_down()&lt;/code&gt; do. What breaks — and what &lt;em&gt;correctly&lt;/em&gt; doesn't break — if a withdraw arrives from a peer whose session has gone down?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_all_export_targets()&lt;/code&gt; builds a fresh tuple on every event. If an export target were added mid-event by some other code path, would the in-flight event see it? Does the answer differ for &lt;code&gt;peers&lt;/code&gt;, which is passed as the live dict?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;installed_paths&lt;/code&gt; is &lt;code&gt;dict[str, PathCandidate | None]&lt;/code&gt;. Trace which event can produce a &lt;code&gt;None&lt;/code&gt; value, and which can produce a prefix in &lt;code&gt;prefixes&lt;/code&gt; that has no entry in &lt;code&gt;installed_paths&lt;/code&gt; at all. Are those the same situation?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't take my word for any of it — the file is short enough to hold in your head, which is the entire point of a toy model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Done when
&lt;/h2&gt;

&lt;p&gt;You can explain the speaker as one object holding state plus three event handlers, and you can see how Sessions 01–14 collapse into a single readable control-plane loop. If someone asks you "what does a BGP speaker do?", the answer you reach for should now be structural rather than a list of features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4271" rel="noopener noreferrer"&gt;RFC 4271 — A Border Gateway Protocol 4 (BGP-4)&lt;/a&gt; — especially §9 (UPDATE Message Handling) for the decision process this model compresses, and §8 for the FSM the toy model omits.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4632" rel="noopener noreferrer"&gt;RFC 4632 — CIDR&lt;/a&gt; — the addressing model underneath every prefix in the RIBs.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc6811" rel="noopener noreferrer"&gt;RFC 6811 — BGP Prefix Origin Validation&lt;/a&gt; — where the &lt;code&gt;VRP&lt;/code&gt; type comes from.&lt;/li&gt;
&lt;li&gt;Source for this session: &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/speaker.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/speaker.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>bgp</category>
      <category>protocol</category>
    </item>
    <item>
      <title>One prefix stops being one route: reading BGP's decision set as code</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Sun, 02 Aug 2026 00:00:11 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/one-prefix-stops-being-one-route-reading-bgps-decision-set-as-code-5943</link>
      <guid>https://dev.to/pathvector-dev/one-prefix-stops-being-one-route-reading-bgps-decision-set-as-code-5943</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-bgp-14/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-bgp-14/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic — inputs, state, and branches — rather than as configuration examples. The code we read here lives in &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;pathvector-studio/protocol-in-code&lt;/a&gt;. If you're earlier in your journey and want to &lt;em&gt;do&lt;/em&gt; things with protocols before dissecting them, start with the hands-on &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series instead — it builds the muscle memory this series assumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How do multiple received paths for one prefix become a policy-aware set of installable candidates?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question sounds small until you notice what it costs. Session 03 of this track taught pure best-path selection: hand it a list of candidates, get back the winner. It's a clean, total function. But best-path selection is only correct when &lt;em&gt;every&lt;/em&gt; candidate handed to it is a legitimate thing to install. The moment you introduce RPKI validation and routing policy, that assumption evaporates. Some candidates shouldn't be installed at all. Others should be installed but with rewritten attributes that change who wins.&lt;/p&gt;

&lt;p&gt;So the shape of the problem changes. One prefix is no longer one route. One prefix is a decision &lt;em&gt;per peer&lt;/em&gt;, and the decisions have to be made before comparison can mean anything.&lt;/p&gt;

&lt;p&gt;The file that does this is &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/decision_process.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/decision_process.py&lt;/code&gt;&lt;/a&gt;, with &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/pipeline.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/pipeline.py&lt;/code&gt;&lt;/a&gt; as its companion. It's short enough to read in one sitting, which is the point — the entire argument fits in two functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the record before you read the loop
&lt;/h2&gt;

&lt;p&gt;Start at the dataclass, not the function. What a piece of code chooses to &lt;em&gt;keep&lt;/em&gt; tells you what it thinks a decision is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CandidateDecision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ValidationState&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PolicyAction&lt;/span&gt;
    &lt;span class="n"&gt;installed_candidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four fields, and every one of them is load-bearing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;validation_state&lt;/code&gt; and &lt;code&gt;action&lt;/code&gt; are deliberately separate. Sessions 04 and 05 spent their time on exactly this split: what RPKI &lt;em&gt;says&lt;/em&gt; about a route (&lt;code&gt;ValidationState&lt;/code&gt;) is not the same thing as what your router &lt;em&gt;does&lt;/em&gt; about it (&lt;code&gt;PolicyAction&lt;/code&gt;). An &lt;code&gt;INVALID&lt;/code&gt; state doesn't mechanically mean "drop." It means an input to a policy that you wrote, and that policy might drop, might deprioritize, might log and accept. Collapsing the two into a single "verdict" field is the most common modelling mistake in this area, and it's the one this dataclass refuses to make. The state and the action ride together, both visible, neither derived from the other after the fact.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;installed_candidate&lt;/code&gt; is &lt;code&gt;PathCandidate | None&lt;/code&gt;, and the &lt;code&gt;None&lt;/code&gt; is the whole story. A decision that produced no installable candidate is still a decision — it's a record that this peer offered this prefix and policy said no. You keep the row. You don't silently drop it from the list.&lt;/p&gt;

&lt;p&gt;And then &lt;code&gt;peer_id&lt;/code&gt;, which is the sneaky one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;PathCandidate&lt;/code&gt; intentionally &lt;em&gt;doesn't&lt;/em&gt; carry &lt;code&gt;peer_id&lt;/code&gt;. Pure best-path comparison doesn't need to know where a route came from — it compares LOCAL_PREF, AS_PATH length, origin, and so on, and none of those depend on peer identity. So earlier sessions dropped it. Here the course puts it back, because policy-aware decision making is inherently about &lt;em&gt;who told you&lt;/em&gt;. This is a real design tension in protocol modelling: the minimal type for one operation is the wrong type for the next one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The loop that turns a prefix into a set
&lt;/h2&gt;

&lt;p&gt;Here's the function the whole session points at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate_prefix_candidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VRP&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PipelinePolicies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CandidateDecision&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CandidateDecision&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;received_attributes_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_candidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;CandidateDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;installed_candidate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the signature first. It takes one prefix and the &lt;em&gt;entire&lt;/em&gt; Adj-RIB-In — the table of what every neighbor sent you, before any local processing. It returns a &lt;code&gt;list&lt;/code&gt;. That type signature is the session's thesis: one prefix in, many decisions out.&lt;/p&gt;

&lt;p&gt;The loop body is a single call. &lt;code&gt;received_attributes_for_prefix(adj_rib_in, prefix)&lt;/code&gt; yields &lt;code&gt;(peer_id, attributes)&lt;/code&gt; pairs — every peer that currently advertises this prefix. Then &lt;code&gt;evaluate_candidate()&lt;/code&gt; — the per-route pipeline that Session 10 assembled — runs unchanged for each one. Session 10 did validation-then-policy for &lt;em&gt;one&lt;/em&gt; incoming route. This session does nothing new to that logic; it just runs it across the fan-in.&lt;/p&gt;

&lt;p&gt;That's worth sitting with. The interesting code here isn't clever. It's a &lt;code&gt;for&lt;/code&gt; loop and an &lt;code&gt;append&lt;/code&gt;. The intelligence is in the &lt;em&gt;shape&lt;/em&gt; — in deciding that "evaluate one route" and "evaluate a prefix" are different operations with different return types, and that the second is a fan-out over the first rather than a bigger version of it.&lt;/p&gt;

&lt;p&gt;Note also what the loop preserves: decisions where &lt;code&gt;installed&lt;/code&gt; came back &lt;code&gt;None&lt;/code&gt; still get appended. &lt;code&gt;evaluate_prefix_candidates()&lt;/code&gt; is a reporting function. It tells you what happened to every path, including the ones that died. If you want to know why traffic for a prefix is going somewhere surprising, this list is the answer, and it would be useless if it only contained survivors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selection happens last, and only on survivors
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;select_best_installable_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VRP&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PipelinePolicies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;decisions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_prefix_candidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;installable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;installed_candidate&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;installed_candidate&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;installable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;select_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four statements, and the ordering of them is the answer to the session's question.&lt;/p&gt;

&lt;p&gt;Evaluate everything. Filter to what survived. Bail if nothing did. &lt;em&gt;Then&lt;/em&gt; compare.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;select_best_path()&lt;/code&gt; — the pure function from Session 03 — is called exactly once, on the last line, against a list that has already been filtered and, crucially, already been &lt;em&gt;rewritten&lt;/em&gt;. The candidates in &lt;code&gt;installable&lt;/code&gt; are not the attributes that arrived on the wire. They're whatever &lt;code&gt;evaluate_candidate()&lt;/code&gt; produced after policy ran. If a policy knocked LOCAL_PREF down on a route from an &lt;code&gt;INVALID&lt;/code&gt;-validating peer, the candidate in this list carries the lowered value, and best-path compares against that.&lt;/p&gt;

&lt;p&gt;This is why "just run best-path" is not enough once validation and policy exist. Best-path is a comparison over a set; it has no opinion about set membership and no opinion about the values it's comparing. Both of those are decided upstream. Run best-path on raw received attributes and you'll faithfully select a route that policy would have rejected.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;if not installable: return None&lt;/code&gt; line deserves a beat too. Every path for this prefix can be filtered out, and that's a legitimate outcome — the prefix simply has no route. It's not an error and it's not an empty-list crash in &lt;code&gt;select_best_path()&lt;/code&gt;. The &lt;code&gt;None&lt;/code&gt; return type makes "no route for this prefix" a first-class value that callers have to handle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same shape, different protocol
&lt;/h2&gt;

&lt;p&gt;Once you've seen this ordering, you start recognizing it elsewhere in the series.&lt;/p&gt;

&lt;p&gt;Within BGP itself the lineage is explicit: Session 03 gave pure comparison, Sessions 04 and 05 split validation state from policy action, Session 10 wired them together for a single route, and this session fans that out prefix-wide. Nothing was rewritten along the way — each layer wraps the previous one and adds exactly one concern.&lt;/p&gt;

&lt;p&gt;The broader pattern is &lt;strong&gt;validate → transform → filter → select&lt;/strong&gt;, and it isn't BGP-specific. A DNS resolver validating RRSIGs before it will serve an answer from cache is doing the same thing: cryptographic verification produces a &lt;em&gt;state&lt;/em&gt;, local policy decides whether that state is fatal, and only then does the resolver pick which record set to hand back. A TLS stack evaluating a certificate chain does it too — the chain either validates or it doesn't, and separately your trust policy decides whether a validation failure aborts the handshake or gets pinned around. Same shape, different protocol. The mistake in all three is the same one: fusing the verification result and the local decision into one boolean, and then having nowhere to put the case where you want to accept something you know is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The walkthrough is executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/bgp/session_14_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to watch for in the output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One peer whose route is &lt;strong&gt;deprioritized by policy&lt;/strong&gt; — same prefix, still installable, but its attributes came out of the pipeline different from how they went in.&lt;/li&gt;
&lt;li&gt;One peer whose route &lt;strong&gt;stays installable without change&lt;/strong&gt; — the untouched control case.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;final best path being chosen after that rewrite&lt;/strong&gt;, not before it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can trace the third bullet back to the four-line body of &lt;code&gt;select_best_installable_for_prefix()&lt;/code&gt;, you've got the session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This is a teaching model, and it simplifies aggressively. Being specific about where:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best path here is one function call over a flat list.&lt;/strong&gt; Real BGP best-path selection is the tie-breaker cascade from RFC 4271 §9.1 — weight, LOCAL_PREF, locally originated, AS_PATH length, origin type, MED (with all its comparison-scope subtleties), eBGP over iBGP, IGP metric to next hop, and router-ID as the final coin flip. It also requires next-hop reachability, which this model never checks at all. A candidate here is installable if policy says so; a real router additionally demands a resolvable next hop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything is recomputed from scratch.&lt;/strong&gt; &lt;code&gt;select_best_installable_for_prefix()&lt;/code&gt; reads the whole Adj-RIB-In for a prefix and re-evaluates every peer's path on every call. Real implementations are incremental and event-driven: an UPDATE arrives, and only the affected prefixes are re-run, usually behind a scheduler with route-refresh handling, timers, and damping. There's no notion of &lt;em&gt;time&lt;/em&gt; in this model — no MRAI, no convergence behavior, no churn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is exactly one best path.&lt;/strong&gt; No ECMP, no multipath, no add-path. Real deployments routinely install several next hops for one prefix, and the function's &lt;code&gt;PathCandidate | None&lt;/code&gt; return type structurally forbids that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RPKI is a list of VRPs passed in as an argument.&lt;/strong&gt; No RTR session, no cache staleness, no partial-coverage semantics beyond whatever &lt;code&gt;ValidationState&lt;/code&gt; models. Real ROV has to reason about a validator that may be down, stale, or disagreeing with a peer's.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adj-RIB-Out and advertisement are absent.&lt;/strong&gt; This model stops at "which path do I install." What you re-advertise to which neighbors, with what attribute modifications, is a whole separate policy stage that isn't here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No withdrawals, no state transitions.&lt;/strong&gt; The Adj-RIB-In is a static snapshot. Nothing in this file handles a peer going down, a path being withdrawn, or the difference between "no route" and "route just disappeared" — which is precisely the distinction that makes real routing hard to debug.&lt;/p&gt;

&lt;p&gt;None of these omissions change the argument the session is making. They're the reason it fits in forty lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Close the walkthrough output and answer these from the source alone:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A peer's route validates as &lt;code&gt;INVALID&lt;/code&gt; but the resulting &lt;code&gt;CandidateDecision&lt;/code&gt; still has a non-&lt;code&gt;None&lt;/code&gt; &lt;code&gt;installed_candidate&lt;/code&gt;. Reading &lt;code&gt;evaluate_prefix_candidates()&lt;/code&gt; and &lt;code&gt;CandidateDecision&lt;/code&gt;, is that a bug — and which field would you inspect to find out what actually happened?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;select_best_installable_for_prefix()&lt;/code&gt; returns a &lt;code&gt;PathCandidate&lt;/code&gt; whose attributes don't match anything you can find in the Adj-RIB-In for that prefix. Where in the four-statement body did the values change, and why is that the correct place for it?&lt;/li&gt;
&lt;li&gt;Every peer advertises the prefix, and the function returns &lt;code&gt;None&lt;/code&gt;. Which line produced that, and what does the returned value fail to tell you that &lt;code&gt;evaluate_prefix_candidates()&lt;/code&gt; would have?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't take my word for any of it — the file is short, and the answers are all in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4271" rel="noopener noreferrer"&gt;RFC 4271&lt;/a&gt; — A Border Gateway Protocol 4 (BGP-4), especially §9.1 on the decision process&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc6811" rel="noopener noreferrer"&gt;RFC 6811&lt;/a&gt; — BGP Prefix Origin Validation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc8481" rel="noopener noreferrer"&gt;RFC 8481&lt;/a&gt; — Clarifications to BGP Origin Validation Based on RPKI&lt;/li&gt;
&lt;li&gt;Source for this session: &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/decision_process.py" rel="noopener noreferrer"&gt;&lt;code&gt;decision_process.py&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/pipeline.py" rel="noopener noreferrer"&gt;&lt;code&gt;pipeline.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>bgp</category>
      <category>routing</category>
    </item>
    <item>
      <title>Announce, withdraw, and peer down are three different code paths</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Sat, 01 Aug 2026 00:00:13 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/announce-withdraw-and-peer-down-are-three-different-code-paths-26d4</link>
      <guid>https://dev.to/pathvector-dev/announce-withdraw-and-peer-down-are-three-different-code-paths-26d4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-bgp-13/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-bgp-13/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic — inputs, state, and branches — rather than as configuration examples. Every module points at a real Python file you can open, read, and run; the source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;github.com/pathvector-studio/protocol-in-code&lt;/a&gt;. If you're newer to this and want to build up hands-on first, start with the companion &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series, which is the practical, run-it-yourself track.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;When a BGP speaker receives news from the outside world, that news arrives in a few distinct flavors. A peer announces a prefix. A peer withdraws a prefix. A peer's session drops entirely. It's tempting to model all three as "an update happened, go recompute" — one generic entry point with a flag or two.&lt;/p&gt;

&lt;p&gt;So here's the question to hold onto while you read:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Core question:&lt;/strong&gt; What function decides which control-plane path to run for announce, withdraw, and peer-down events?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And its sharper follow-up: why are these three separate functions instead of one? The answer is not stylistic. Each event enters the control plane at a different point, and one of them can touch an unbounded number of prefixes.&lt;/p&gt;

&lt;p&gt;The file is &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/events.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/events.py&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three triggers, not one
&lt;/h2&gt;

&lt;p&gt;Start at the top of the file. The events are declared as three separate frozen dataclasses, and the shape of each one already tells you something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AnnounceEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathAttributes&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WithdrawEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PeerDownEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read them as a narrowing sequence. An announce carries the full payload: who, what prefix, and what path attributes came with it. A withdraw carries who and what prefix — there are no attributes, because you're removing a path, not describing one. A peer-down carries only who. It doesn't name a prefix at all, and that omission is the whole reason the third branch has to be written differently: the set of affected prefixes isn't in the event, it has to be discovered from state.&lt;/p&gt;

&lt;p&gt;All three converge on one return type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EventResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;touched_prefixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
    &lt;span class="n"&gt;best_paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;export_changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ExportChange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;touched_prefixes&lt;/code&gt; is a tuple and &lt;code&gt;best_paths&lt;/code&gt; is a dict keyed by prefix. The return type was designed for the many-prefix case even though two of the three branches will only ever fill it with one entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the dispatch
&lt;/h2&gt;

&lt;p&gt;Before walking through the real code, here's the shape in pseudocode. This is the lesson; the actual functions are longer, but they don't deviate from it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;announce&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;gate&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt;
    &lt;span class="n"&gt;recompute&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;
    &lt;span class="n"&gt;refresh&lt;/span&gt; &lt;span class="n"&gt;exports&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;withdraw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;remove&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="n"&gt;recompute&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;
    &lt;span class="n"&gt;refresh&lt;/span&gt; &lt;span class="n"&gt;exports&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;peer_down&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;remove&lt;/span&gt; &lt;span class="n"&gt;whole&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;
    &lt;span class="n"&gt;recompute&lt;/span&gt; &lt;span class="n"&gt;affected&lt;/span&gt; &lt;span class="n"&gt;prefixes&lt;/span&gt;
    &lt;span class="n"&gt;refresh&lt;/span&gt; &lt;span class="n"&gt;exports&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things stand out. Only the announce branch has a gate. Every branch ends in the same two steps — recompute, then refresh exports. And only the last branch has a loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Announce: gate first, then recompute
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;process_announce_event()&lt;/code&gt; starts with the session gate that Session 11 made explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;peer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;receive_update_if_established&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;EventResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;touched_prefixes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
            &lt;span class="n"&gt;best_paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
            &lt;span class="n"&gt;export_changes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An announce from a peer that isn't in &lt;code&gt;ESTABLISHED&lt;/code&gt; doesn't get to write into the Adj-RIB-In at all. Look carefully at the early return: it doesn't just bail with a flag. It reports &lt;code&gt;touched_prefixes&lt;/code&gt; containing the prefix, and it reports the &lt;em&gt;current&lt;/em&gt; best path from &lt;code&gt;loc_rib.best_paths.get(event.prefix)&lt;/code&gt; — the one that was already there. &lt;code&gt;export_changes&lt;/code&gt; is empty. The rejected announce is visible in the result, but it changed nothing.&lt;/p&gt;

&lt;p&gt;That's a deliberate distinction worth internalizing: "we looked at this prefix" and "this prefix changed" are different claims, and the result type keeps them separate.&lt;/p&gt;

&lt;p&gt;If the gate passes, the rest is two calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_recompute_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;refresh_exports_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;export_targets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_recompute_prefix()&lt;/code&gt; is where the event layer hands off all the prefix-level decision work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_recompute_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocRIB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VRP&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PipelinePolicies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;select_best_installable_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;remove_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="nf"&gt;install_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helper is the reason the three branches stay short. Every hard question — which candidate wins the decision process, whether RPKI validation rejects it, what the policies say — lives behind &lt;code&gt;select_best_installable_for_prefix()&lt;/code&gt;. The event layer only cares about the binary outcome: there's a best path (install it) or there isn't (remove whatever was installed). Session 14 goes deeper into that decision logic, but the dispatch shape you're reading here doesn't change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Withdraw: no gate, and it starts by deleting
&lt;/h2&gt;

&lt;p&gt;Now compare &lt;code&gt;process_withdraw_event()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_withdraw_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WithdrawEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocRIB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;export_targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ExportTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...],&lt;/span&gt;
    &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VRP&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PipelinePolicies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EventResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;withdraw_received_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_recompute_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;refresh_exports_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;export_targets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two differences from announce, and both are in the signature and the first line.&lt;/p&gt;

&lt;p&gt;First, look at the parameter list: there's no &lt;code&gt;peers&lt;/code&gt; argument. The withdraw path doesn't have access to the session table, which means it structurally &lt;em&gt;cannot&lt;/em&gt; gate on session state. That's not an omission — it's the design being enforced by the type signature.&lt;/p&gt;

&lt;p&gt;Second, the first statement is a removal, not a validation. There is no &lt;code&gt;accepted&lt;/code&gt; check and the function always returns &lt;code&gt;accepted=True&lt;/code&gt;. Withdrawing state you may or may not have is safe; adding state you shouldn't have is not. Removal is idempotent in a way that installation isn't, so the asymmetry is justified rather than sloppy.&lt;/p&gt;

&lt;p&gt;After that first line, the two branches are identical: recompute, refresh. The withdrawal might not even change the best path — if the withdrawing peer wasn't the winner, &lt;code&gt;select_best_installable_for_prefix()&lt;/code&gt; will return the same candidate it did before, and &lt;code&gt;refresh_exports_for_prefix()&lt;/code&gt; will produce no changes. The code doesn't special-case that. It recomputes unconditionally and lets the export refresh decide whether anything is actually different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Peer down: one event, many prefixes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;process_peer_down_event()&lt;/code&gt; is where the shape breaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;lost_prefixes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paths_by_peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paths_by_peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;peer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;peers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PeerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SessionState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACTIVE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line is the crux of this whole module. Because &lt;code&gt;PeerDownEvent&lt;/code&gt; carries no prefix, the affected set has to be &lt;em&gt;read out of state&lt;/em&gt; before that state is destroyed. Order matters: snapshot the keys, then pop the peer's table. Reverse those two lines and you lose the list of prefixes you were about to fix up.&lt;/p&gt;

&lt;p&gt;Note also &lt;code&gt;.get(event.peer_id, {})&lt;/code&gt; and &lt;code&gt;.pop(event.peer_id, None)&lt;/code&gt; — a peer-down for a peer with no received paths, or no entry at all, is a no-op rather than a &lt;code&gt;KeyError&lt;/code&gt;. Session teardown races are exactly the situation where you get duplicate or spurious down events.&lt;/p&gt;

&lt;p&gt;The session state moves to &lt;code&gt;ACTIVE&lt;/code&gt;, not &lt;code&gt;IDLE&lt;/code&gt;. That's the FSM's "trying to connect" state, which is where a peer that just dropped should sit. And because &lt;code&gt;PeerSession&lt;/code&gt; is frozen, the update is a replacement of the dict entry, not a mutation.&lt;/p&gt;

&lt;p&gt;Then comes the loop that no other branch has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;lost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ExportChange&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lost_prefixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lost&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_recompute_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;refresh_exports_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;export_targets&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;EventResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;touched_prefixes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="n"&gt;best_paths&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;export_changes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same two-step body as announce and withdraw — &lt;code&gt;_recompute_prefix()&lt;/code&gt; then &lt;code&gt;refresh_exports_for_prefix()&lt;/code&gt; — wrapped in a &lt;code&gt;for&lt;/code&gt;. And note what it does &lt;em&gt;not&lt;/em&gt; do: it doesn't blanket-withdraw every prefix the peer had announced. For each lost prefix it re-runs the decision process against what's left in the Adj-RIB-In. If another peer was also announcing that prefix, &lt;code&gt;_recompute_prefix()&lt;/code&gt; finds it and installs it, and the export refresh emits a change to the new next hop rather than a withdrawal. Only prefixes where the downed peer was the sole source collapse to &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the practical answer to the "which branch touches many prefixes" question, and it's why a single generic update function would have been awkward: two of the three branches know their prefix up front, and one has to go find it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Recomputing per prefix in a loop is also where real BGP implementations spend serious engineering effort. A peer down on a full-table session means several hundred thousand recomputations. Real implementations batch, defer, and prioritize; this toy model just loops.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Reading lens: the same shape shows up elsewhere
&lt;/h2&gt;

&lt;p&gt;The pattern here — &lt;em&gt;the event doesn't name its own blast radius, so you snapshot state before destroying it&lt;/em&gt; — isn't BGP-specific. It's the same shape as conntrack entry expiry, where a single timeout has to enumerate the flows it invalidates before dropping the table entry. It's the same shape as a DNS resolver flushing everything under a zone when the zone's delegation changes. In each case the trigger is small and singular, and the cleanup has to derive its own scope from state that's about to go away.&lt;/p&gt;

&lt;p&gt;Once you've seen the announce/withdraw/peer-down split, you'll notice that most protocol event loops have exactly this trio: a gated additive event, an ungated removal, and a teardown that fans out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The walkthrough for this session is runnable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/bgp/session_13_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch for four things in the output, in order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one announce producing a best path&lt;/li&gt;
&lt;li&gt;one withdraw removing that best path&lt;/li&gt;
&lt;li&gt;a second announce restoring it&lt;/li&gt;
&lt;li&gt;one peer-down withdrawing all affected exports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one is the payoff. Compare the length of &lt;code&gt;touched_prefixes&lt;/code&gt; in that final result against the three before it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This is a teaching model, and it simplifies in ways that matter if you carry the mental model into production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Events are synchronous and serialized.&lt;/strong&gt; Each &lt;code&gt;process_*_event()&lt;/code&gt; call runs to completion before the next event exists. Real BGP speakers process a stream of UPDATE messages off a TCP socket, with input queues, read batching, and no guarantee that a peer-down is observed before or after the updates that were in flight when the session dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real UPDATE message carries both announcements and withdrawals.&lt;/strong&gt; RFC 4271's UPDATE has a Withdrawn Routes field &lt;em&gt;and&lt;/em&gt; an NLRI field in the same message. Splitting them into &lt;code&gt;AnnounceEvent&lt;/code&gt; and &lt;code&gt;WithdrawEvent&lt;/code&gt; is a modeling choice that makes the branches legible; a real parser has to handle both in one message and get the ordering right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No timers.&lt;/strong&gt; There's no MRAI (Minimum Route Advertisement Interval), no route flap damping, no graceful restart. In this model, peer-down means the paths are gone immediately. Real BGP with graceful restart keeps stale routes marked and installed while it waits for the session to come back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The recompute is exhaustive, not incremental.&lt;/strong&gt; &lt;code&gt;_recompute_prefix()&lt;/code&gt; re-runs the full selection for a prefix every time. Production implementations track which candidate is currently best and short-circuit most updates without a full re-scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;export_changes&lt;/code&gt; is a return value, not I/O.&lt;/strong&gt; Nothing is transmitted. There are no OPEN/KEEPALIVE/NOTIFICATION messages, no hold timer, no actual peers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;peers[event.peer_id]&lt;/code&gt; in the announce path will &lt;code&gt;KeyError&lt;/code&gt;&lt;/strong&gt; on an unknown peer. That's fine for a model where you control the inputs; a real speaker never trusts the peer identifier that far.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Try to answer these by reading &lt;code&gt;events.py&lt;/code&gt; alone, without running anything. If you can't, that's the signal for where to look:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Why is &lt;code&gt;process_withdraw_event()&lt;/code&gt; missing the &lt;code&gt;peers&lt;/code&gt; parameter that the other two branches take, and what would change if you added a session gate to it?&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;process_peer_down_event()&lt;/code&gt;, what breaks if you swap the order of the &lt;code&gt;lost_prefixes&lt;/code&gt; assignment and the &lt;code&gt;paths_by_peer.pop()&lt;/code&gt; call?&lt;/li&gt;
&lt;li&gt;An announce arrives from a peer in &lt;code&gt;OPEN_CONFIRM&lt;/code&gt;. Trace the return value: what is &lt;code&gt;accepted&lt;/code&gt;, what is in &lt;code&gt;touched_prefixes&lt;/code&gt;, and what is in &lt;code&gt;best_paths&lt;/code&gt;?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you have answers, go verify them against the source rather than against your memory of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Done when
&lt;/h2&gt;

&lt;p&gt;You can explain why &lt;code&gt;announce&lt;/code&gt;, &lt;code&gt;withdraw&lt;/code&gt;, and &lt;code&gt;peer down&lt;/code&gt; are three branches instead of one generic update function — and you can say, without looking, which branch touches exactly one prefix and which one can touch many.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4271" rel="noopener noreferrer"&gt;RFC 4271 — A Border Gateway Protocol 4 (BGP-4)&lt;/a&gt; — §4.3 for the UPDATE message format, §9.1 for the decision process&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4724" rel="noopener noreferrer"&gt;RFC 4724 — Graceful Restart Mechanism for BGP&lt;/a&gt; — what a real speaker does on peer down instead of dropping paths&lt;/li&gt;
&lt;li&gt;Previous in this track: Session 11 (the session gate) and Session 12 (export refresh) — both are the pieces this dispatcher chooses between. Session 14 goes into the prefix-level decision logic that &lt;code&gt;_recompute_prefix()&lt;/code&gt; delegates to.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>bgp</category>
      <category>protocol</category>
    </item>
    <item>
      <title>When Loc-RIB changes, every peer needs a different answer</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Fri, 31 Jul 2026 00:00:09 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/when-loc-rib-changes-every-peer-needs-a-different-answer-5ae3</link>
      <guid>https://dev.to/pathvector-dev/when-loc-rib-changes-every-peer-needs-a-different-answer-5ae3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-bgp-12/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-bgp-12/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic — inputs, state, and branches — rather than as configuration examples. The source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;pathvector-studio/protocol-in-code&lt;/a&gt;, and every module points at a real Python file you can open, read, and run. If you're earlier in the journey and want hands-on packet-level exercises first, start with the companion &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series and come back here.&lt;/p&gt;

&lt;p&gt;Today's module is BGP Session 12: &lt;strong&gt;export refresh after recompute&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When &lt;code&gt;Loc-RIB&lt;/code&gt; changes, how do outbound advertisements get refreshed per peer?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hold that question while you read. It sounds like plumbing, but it's the question that separates two things people often collapse into one: &lt;em&gt;what this router believes is the best path&lt;/em&gt;, and &lt;em&gt;what this router tells each neighbor&lt;/em&gt;. Those are different pieces of state, updated at different times, by different code.&lt;/p&gt;

&lt;p&gt;Two earlier sessions set this up. Session 08 showed how a single installed route becomes a single exported route — the policy pass that turns "I use this path" into "here is what I'll say about it." Session 09 showed how &lt;code&gt;Loc-RIB&lt;/code&gt; can change after a session goes down and best-path selection reruns. Session 12 is the seam between them: recompute already happened, &lt;code&gt;Loc-RIB&lt;/code&gt; already moved, and now something has to walk the peers and fix up &lt;code&gt;Adj-RIB-Out&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The file to open is &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/export_refresh.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/export_refresh.py&lt;/code&gt;&lt;/a&gt;, with &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/export_policy.py" rel="noopener noreferrer"&gt;&lt;code&gt;export_policy.py&lt;/code&gt;&lt;/a&gt; as the companion it leans on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export is peer-specific, and the types say so
&lt;/h2&gt;

&lt;p&gt;Start with the data, not the function. The first thing worth noticing is that there is no such thing as "the export" of a prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExportTarget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;peer_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PeerType&lt;/span&gt;
    &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExportPolicy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;code&gt;ExportTarget&lt;/code&gt; bundles three things: &lt;em&gt;who&lt;/em&gt; the peer is, &lt;em&gt;what kind&lt;/em&gt; of peer it is (customer, upstream, and so on), and &lt;em&gt;which policy&lt;/em&gt; applies. Nothing about the route is in here. That asymmetry is the design statement: the route is global to the router, the export decision is local to the peer.&lt;/p&gt;

&lt;p&gt;The output type mirrors it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExportChangeKind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ADVERTISE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;advertise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;WITHDRAW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;withdraw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExportChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExportChangeKind&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two kinds of change, and &lt;code&gt;path&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt; for one of them. A withdrawal carries no path — you're not telling the peer about a route, you're telling it to forget one. The type makes that impossible to get wrong.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;ExportChangeKind&lt;/code&gt; subclasses &lt;code&gt;str&lt;/code&gt; as well as &lt;code&gt;Enum&lt;/code&gt;. That's a small ergonomic choice — it means the value compares equal to &lt;code&gt;"advertise"&lt;/code&gt; and serializes cleanly — not a protocol fact. Don't read anything into it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Three values, one peer
&lt;/h2&gt;

&lt;p&gt;Now the function. The whole module is one loop, and the loop body juggles exactly three values per peer. Get those three straight and you've got the session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;refresh_exports_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocRIB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBOut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ExportTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ExportChange&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ExportChange&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;installed&lt;/code&gt; is read &lt;strong&gt;once&lt;/strong&gt;, before the loop. That's the "what this router believes" value, and it's the same for every peer — that's the whole point of &lt;code&gt;Loc-RIB&lt;/code&gt;. Note the &lt;code&gt;.get()&lt;/code&gt;: if recompute removed the prefix entirely, &lt;code&gt;installed&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt;, and the loop below still runs. A prefix disappearing is not a special case here; it's the same code path with a different input.&lt;/p&gt;

&lt;p&gt;Then, per peer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;current_by_prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;advertisements_by_peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_by_prefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;desired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;desired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prepare_export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;installed&lt;/code&gt; — what &lt;code&gt;Loc-RIB&lt;/code&gt; says the router uses. One value, shared.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;current&lt;/code&gt; — what this peer has &lt;em&gt;already been told&lt;/em&gt;. Read out of &lt;code&gt;Adj-RIB-Out&lt;/code&gt;, per peer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;desired&lt;/code&gt; — what this peer &lt;em&gt;should&lt;/em&gt; be told. &lt;code&gt;installed&lt;/code&gt; run through &lt;code&gt;prepare_export()&lt;/code&gt; with this peer's type and policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;desired&lt;/code&gt; starts at &lt;code&gt;None&lt;/code&gt; and only becomes a path if &lt;code&gt;installed&lt;/code&gt; exists. And even when &lt;code&gt;installed&lt;/code&gt; exists, &lt;code&gt;prepare_export()&lt;/code&gt; may return &lt;code&gt;None&lt;/code&gt; — that's policy denying the export. Both roads lead to the same place, which is why the next branch doesn't care which one you took.&lt;/p&gt;

&lt;h2&gt;
  
  
  The branch that matters
&lt;/h2&gt;

&lt;p&gt;Here's the reconciliation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;desired&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;current_by_prefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="nc"&gt;ExportChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ExportChangeKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WITHDRAW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;desired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;current_by_prefix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;desired&lt;/span&gt;
            &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;ExportChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ExportChangeKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADVERTISE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;desired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as a two-by-two table of &lt;code&gt;current&lt;/code&gt; against &lt;code&gt;desired&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;current&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;desired&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;nothing — peer never knew, still doesn't&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;withdraw&lt;/strong&gt;, and drop the entry from &lt;code&gt;Adj-RIB-Out&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;advertise&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;set, equal&lt;/td&gt;
&lt;td&gt;nothing — no wire churn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;set, different&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;advertise&lt;/strong&gt; the new value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two "nothing" rows are the ones worth pausing on. The inner &lt;code&gt;if current is not None&lt;/code&gt; guard means you don't emit a withdrawal for a route the peer was never told about — BGP has no way to un-say something you never said. And &lt;code&gt;if current != desired&lt;/code&gt; means an unchanged export produces no &lt;code&gt;ExportChange&lt;/code&gt; at all. That second one is why this is called a &lt;em&gt;refresh&lt;/em&gt; and not a &lt;em&gt;resend&lt;/em&gt;: recompute might touch a prefix and, after policy, produce byte-identical output for a peer. That peer hears nothing.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;desired is None&lt;/code&gt; branch is where withdrawal is &lt;em&gt;born&lt;/em&gt;. Nothing upstream decided "send a withdraw" — it's an emergent consequence of &lt;code&gt;Adj-RIB-Out&lt;/code&gt; holding a value that policy no longer justifies. Notice too that both the state mutation and the change record happen together in each branch: &lt;code&gt;Adj-RIB-Out&lt;/code&gt; is the durable state, the returned &lt;code&gt;list[ExportChange]&lt;/code&gt; is the delta. They never diverge because they're written in the same two lines.&lt;/p&gt;

&lt;p&gt;That structure also answers the "why a separate pass?" part of the session. Best-path selection produces one answer per prefix. Export produces N answers per prefix, one per peer, and each one depends on peer-local state (&lt;code&gt;current&lt;/code&gt;) that best-path selection has no business knowing about. Fusing them would mean best-path selection carrying a peer loop inside it. Splitting them means recompute can run, settle, and &lt;em&gt;then&lt;/em&gt; hand a stable &lt;code&gt;Loc-RIB&lt;/code&gt; to a pass whose only job is per-peer reconciliation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The module ships a runnable walkthrough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/bgp/session_12_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to look for in the output:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;An advertisement to the customer peer.&lt;/strong&gt; &lt;code&gt;installed&lt;/code&gt; exists, policy permits, &lt;code&gt;current&lt;/code&gt; was empty → &lt;code&gt;ADVERTISE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No advertisement to the denied upstream peer.&lt;/strong&gt; &lt;code&gt;installed&lt;/code&gt; exists, but &lt;code&gt;prepare_export()&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;. &lt;code&gt;desired is None&lt;/code&gt;, &lt;code&gt;current is None&lt;/code&gt;, so the &lt;code&gt;continue&lt;/code&gt; fires and nothing is emitted. Silence, not a withdraw.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A withdrawal when the installed path disappears.&lt;/strong&gt; &lt;code&gt;loc_rib.best_paths.get(prefix)&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;, so &lt;code&gt;desired&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt; for every peer — but only the peers with a non-&lt;code&gt;None&lt;/code&gt; &lt;code&gt;current&lt;/code&gt; get a &lt;code&gt;WITHDRAW&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Case 2 and case 3 both hit &lt;code&gt;desired is None&lt;/code&gt;. Only one of them produces output. That difference is entirely &lt;code&gt;current&lt;/code&gt;, and it's the single most useful thing to internalize from this file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This is a toy model, and being precise about what it isn't is the point of this section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It refreshes one prefix at a time.&lt;/strong&gt; &lt;code&gt;refresh_exports_for_prefix()&lt;/code&gt; takes a single &lt;code&gt;prefix&lt;/code&gt;. A real implementation after a session teardown has thousands to tens of thousands of prefixes to reconsider, and the interesting engineering is entirely in how you batch, prioritize, and pace that work — not in the per-prefix decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It does not build UPDATE messages.&lt;/strong&gt; There is no packet here. No path attribute encoding, no NLRI packing, no grouping of prefixes that share an attribute set into one UPDATE — which is the main reason real BGP UPDATEs are efficient at all. The function returns a &lt;code&gt;list[ExportChange]&lt;/code&gt;, a description of &lt;em&gt;what should be true&lt;/em&gt;, and stops. Turning that list into wire bytes is a separate problem this module deliberately doesn't touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No timing.&lt;/strong&gt; Real BGP spaces out advertisements with MRAI (Minimum Route Advertisement Interval) timers, precisely so that a flapping route doesn't turn into a flood of UPDATEs. There is no clock in this file. Every change is emitted immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No withdrawal suppression or route damping.&lt;/strong&gt; Nothing here notices that a prefix has advertised and withdrawn ten times in a minute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparison by &lt;code&gt;!=&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;if current != desired&lt;/code&gt; relies on &lt;code&gt;PathCandidate&lt;/code&gt; equality. Real implementations compare over attribute sets with rules about which attributes are transitive, which are optional, and which get rewritten on export.&lt;/p&gt;

&lt;p&gt;What the module &lt;em&gt;does&lt;/em&gt; model faithfully is the decision itself: given what &lt;code&gt;Loc-RIB&lt;/code&gt; holds, what &lt;code&gt;Adj-RIB-Out&lt;/code&gt; holds, and this peer's policy, should this peer see an advertise, a withdraw, or nothing? That decision is real. Everything around it is stripped.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape shows up elsewhere
&lt;/h2&gt;

&lt;p&gt;Once you've seen &lt;code&gt;current&lt;/code&gt; vs. &lt;code&gt;desired&lt;/code&gt; reconciled against per-consumer state, you start seeing it in tracks that have nothing to do with BGP.&lt;/p&gt;

&lt;p&gt;It's a &lt;strong&gt;reconciliation loop&lt;/strong&gt;: read the intended state, read the observed state, emit the minimal set of changes to close the gap — and emit &lt;em&gt;nothing&lt;/em&gt; when they already agree. That last property is what makes the operation safe to run repeatedly, which is the same property Kubernetes controllers are built on.&lt;/p&gt;

&lt;p&gt;It also rhymes with cache invalidation across the series. A DNS resolver holding a record that the authoritative zone has since changed is in the same position as a peer holding a stale entry in &lt;code&gt;Adj-RIB-Out&lt;/code&gt;: some pass has to notice the divergence and push a correction. The difference is direction — DNS pulls on TTL expiry, BGP pushes on recompute — but the state comparison at the center is identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Close the article and open the file. Can you answer these from the code alone?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A peer's export policy is edited so a prefix that was permitted is now denied, but &lt;code&gt;Loc-RIB&lt;/code&gt; is unchanged. Walk &lt;code&gt;refresh_exports_for_prefix()&lt;/code&gt; for that peer — which branch fires, and what ends up in the returned list?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;refresh_exports_for_prefix()&lt;/code&gt; is called twice in a row with identical inputs. What does the second call return, and what in the code guarantees it?&lt;/li&gt;
&lt;li&gt;A peer has never been advertised a prefix, and best-path selection removes that prefix from &lt;code&gt;Loc-RIB&lt;/code&gt;. Does that peer get a &lt;code&gt;WITHDRAW&lt;/code&gt;? Which line decides, and why is that the correct protocol behavior rather than an optimization?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You're done with this session when you can explain why export refresh is a separate pass after recompute, and when you can look at &lt;code&gt;current&lt;/code&gt; and &lt;code&gt;desired&lt;/code&gt; for a peer and say — without running anything — whether that peer sees an advertise, a withdraw, or silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4271.html" rel="noopener noreferrer"&gt;RFC 4271&lt;/a&gt; — A Border Gateway Protocol 4 (BGP-4). Section 9.1 covers the decision process; section 9.2 covers update-send process and the Adj-RIB-Out.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4271.html#section-9.2.1.1" rel="noopener noreferrer"&gt;RFC 4271 §9.2.1.1&lt;/a&gt; — the MRAI timer this toy model omits.&lt;/li&gt;
&lt;li&gt;Source for this session: &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/export_refresh.py" rel="noopener noreferrer"&gt;&lt;code&gt;export_refresh.py&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/export_policy.py" rel="noopener noreferrer"&gt;&lt;code&gt;export_policy.py&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>bgp</category>
      <category>protocol</category>
    </item>
    <item>
      <title>Established is not a status label — it's a write permission</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Thu, 30 Jul 2026 00:00:09 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/established-is-not-a-status-label-its-a-write-permission-2dbk</link>
      <guid>https://dev.to/pathvector-dev/established-is-not-a-status-label-its-a-write-permission-2dbk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-bgp-11/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-bgp-11/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols not as configuration examples but as logic — inputs, state, and branches. Every session points at a small Python file and asks you to read it the way you'd read any other program. The source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;github.com/pathvector-studio/protocol-in-code&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you're earlier in the journey and want hands-on packet-level exercises before diving into implementation logic, start with the &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series instead. This series assumes you're comfortable reading Python and want to know &lt;em&gt;why&lt;/em&gt; the branch is where it is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;Here's the one to keep turning over as you read:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What has to be true before the control plane should accept an UPDATE from a neighbor?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That question sounds like it should have a long answer. Route validity, attribute well-formedness, policy, next-hop reachability, max-prefix limits — a real BGP implementation checks all of it. But before any of that, there's a gate so simple it's easy to skip past entirely, and it's the one this session isolates.&lt;/p&gt;

&lt;p&gt;The previous session built an integrated pipeline: an UPDATE arrives, gets stored, gets compared, gets selected. It worked. But it quietly assumed something — that the route arriving at the front of the pipeline had already earned the right to be there. This session makes that assumption explicit and gives it a name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the file in three moves
&lt;/h2&gt;

&lt;p&gt;The file is &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/peer_state.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/peer_state.py&lt;/code&gt;&lt;/a&gt;, with &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/session.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/session.py&lt;/code&gt;&lt;/a&gt; as its companion. It's short enough that you could read it top to bottom in thirty seconds and learn nothing. Read it in this order instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. What a peer actually is
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PeerSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BGPSessionConfig&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SessionState&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;open_peer_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BGPSessionConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PeerSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PeerSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;establish_neighbor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;open_peer_session()&lt;/code&gt; does almost nothing. It takes an identifier and a config, calls &lt;code&gt;establish_neighbor()&lt;/code&gt; from &lt;code&gt;session.py&lt;/code&gt;, and staples the resulting state onto a frozen dataclass. That's the whole constructor.&lt;/p&gt;

&lt;p&gt;The thing worth noticing is what it &lt;em&gt;doesn't&lt;/em&gt; do. It doesn't retry. It doesn't leave the state mutable so a later step can nudge a peer into &lt;code&gt;Established&lt;/code&gt; because the operator wants the route. The dataclass is &lt;code&gt;frozen=True&lt;/code&gt;: whatever &lt;code&gt;establish_neighbor()&lt;/code&gt; decided, that's the peer's state for the lifetime of this object. The session outcome is an input to everything downstream, not something downstream code negotiates with.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;PeerSession&lt;/code&gt; isn't really a connection object. It's a decision, packaged.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The gate, stated as one question
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;session_accepts_updates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PeerSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;SessionState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ESTABLISHED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. One comparison. No &lt;code&gt;or&lt;/code&gt;, no fallback, no "well, &lt;code&gt;OpenConfirm&lt;/code&gt; is close enough."&lt;/p&gt;

&lt;p&gt;The function name is doing real work here — it could have been called &lt;code&gt;is_established()&lt;/code&gt;, which would describe the &lt;em&gt;state&lt;/em&gt;. Instead it's named for the &lt;em&gt;consequence&lt;/em&gt;: does this session accept updates? That naming is the whole point of the session. &lt;code&gt;Established&lt;/code&gt; isn't a label you read off a &lt;code&gt;show bgp summary&lt;/code&gt; output to feel good about. It's the answer to a permission question that gets asked on every inbound UPDATE.&lt;/p&gt;

&lt;p&gt;Note the &lt;code&gt;is&lt;/code&gt; rather than &lt;code&gt;==&lt;/code&gt;. &lt;code&gt;SessionState&lt;/code&gt; is an enum, and identity comparison means there's exactly one object that satisfies this check. There's no clever coercion, no truthy near-miss. Either the peer holds that exact enum member or it doesn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Where the &lt;code&gt;if&lt;/code&gt; sits
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_update_if_established&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PeerSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathAttributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;session_accepts_updates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="nf"&gt;store_received_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the entire lesson, and it's four lines of body.&lt;/p&gt;

&lt;p&gt;The important thing is not the amount of code. It is the &lt;strong&gt;placement of the &lt;code&gt;if&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Look at what's on each side of the early return. Above it: nothing has happened. Below it: a write into &lt;code&gt;Adj-RIB-In&lt;/code&gt;. There is no path through this function where a route lands in the RIB without first passing &lt;code&gt;session_accepts_updates()&lt;/code&gt;. Not "gets stored and then filtered later." Not "gets stored with a flag." The write is &lt;em&gt;structurally&lt;/em&gt; downstream of the check.&lt;/p&gt;

&lt;p&gt;Compare that to the alternative you've probably seen in real codebases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# NOT what this module does — the shape to avoid
&lt;/span&gt;&lt;span class="nf"&gt;store_received_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;session_accepts_updates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;mark_as_pending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same information, same check, completely different guarantee. In that version, &lt;code&gt;Adj-RIB-In&lt;/code&gt; contains routes from peers that never reached &lt;code&gt;Established&lt;/code&gt;, and correctness depends on every reader downstream remembering to filter. In the version the module actually implements, &lt;code&gt;Adj-RIB-In&lt;/code&gt; has an invariant: &lt;strong&gt;everything in it came from an established session.&lt;/strong&gt; Downstream code doesn't have to re-check, because the shape of the function made the bad state unrepresentable.&lt;/p&gt;

&lt;p&gt;That's why the return type is &lt;code&gt;bool&lt;/code&gt; and not &lt;code&gt;None&lt;/code&gt;. The caller gets told whether the write happened. A silent no-op would be a worse API — the caller couldn't distinguish "stored" from "dropped at the gate."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;store_received_path()&lt;/code&gt; call takes &lt;code&gt;peer.peer_id&lt;/code&gt; as the key, not the prefix alone. &lt;code&gt;Adj-RIB-In&lt;/code&gt; is per-peer by construction — which is exactly why the gate can be per-peer too. If the RIB were flat, the gate would have to live somewhere messier.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Same shape, different protocol
&lt;/h2&gt;

&lt;p&gt;This gate is worth recognizing because you've already met it under other names.&lt;/p&gt;

&lt;p&gt;A TLS session that hasn't completed the handshake can carry bytes — the TCP connection is up, the socket is writable — but application data sent before &lt;code&gt;Finished&lt;/code&gt; isn't protected by the negotiated keys, and a correct implementation refuses to hand it to the application. Same shape: transport reachability is not the same thing as protocol readiness, and the code has to enforce the difference with a branch, not a comment.&lt;/p&gt;

&lt;p&gt;Or take conntrack: a packet matching an existing flow tuple gets fast-pathed, but only if the state machine says the flow is &lt;code&gt;ESTABLISHED&lt;/code&gt;. A packet arriving for a &lt;code&gt;SYN_SENT&lt;/code&gt; entry takes a different branch entirely. Again: the state isn't decoration, it's the gate on which code path runs.&lt;/p&gt;

&lt;p&gt;In all three cases the mistake looks identical — treating "the pipe is open" as "the protocol is ready." BGP is just the clearest place to see it, because the state has a name printed in every operator's terminal, and it's easy to read that name as a status indicator rather than as a precondition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This is where the series stays honest with you.&lt;/p&gt;

&lt;p&gt;This lesson isolates &lt;strong&gt;only&lt;/strong&gt; the session-state gate. &lt;code&gt;Established&lt;/code&gt; is the &lt;em&gt;first&lt;/em&gt; gate in this toy model — it is not the only gate a real implementation uses, and reading it as such will mislead you.&lt;/p&gt;

&lt;p&gt;Specifically, what's missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Address-family activation.&lt;/strong&gt; A real BGP session negotiates which AFI/SAFI pairs are active. A peer can be perfectly &lt;code&gt;Established&lt;/code&gt; for IPv4 unicast and still have no business sending you IPv6 or VPNv4 routes. The toy model has one implicit address family and no per-family gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negotiated capability checks.&lt;/strong&gt; Multiprotocol extensions, route refresh, add-path, 4-byte ASN handling — all negotiated during OPEN, all things that change what an UPDATE is even allowed to contain. None of that exists here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The rest of the state machine.&lt;/strong&gt; &lt;code&gt;establish_neighbor()&lt;/code&gt; collapses what RFC 4271 models as &lt;code&gt;Idle → Connect → Active → OpenSent → OpenConfirm → Established&lt;/code&gt; into a single call with a single outcome. There are no hold timers, no keepalives, no transitions &lt;em&gt;out&lt;/em&gt; of &lt;code&gt;Established&lt;/code&gt;. In a real implementation, a peer can be established at the moment an UPDATE arrives and gone by the time it's processed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inbound policy.&lt;/strong&gt; Passing the session gate gets a route into &lt;code&gt;Adj-RIB-In&lt;/code&gt;. In a real router, inbound route-maps, prefix lists, and max-prefix limits all sit between the wire and that store.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason the toy model cuts all of it is that each of those gates has the &lt;em&gt;same structural shape&lt;/em&gt; as the one you just read — a boolean question placed before a write. Once you can see the shape clearly in four lines, the real implementation's dozen gates read as variations rather than as new material.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The walkthrough is runnable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/bgp/session_11_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to watch for in the output:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;One peer whose TCP reachability never gets it to &lt;code&gt;Established&lt;/code&gt;.&lt;/strong&gt; The transport is fine. The session isn't. Watch what &lt;code&gt;session_accepts_updates()&lt;/code&gt; returns for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One peer whose UPDATE is accepted.&lt;/strong&gt; Trace it through &lt;code&gt;receive_update_if_established()&lt;/code&gt; and confirm it takes the path past the early return.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only the established peer appearing in &lt;code&gt;Adj-RIB-In&lt;/code&gt;.&lt;/strong&gt; This is the invariant made visible. The rejected peer left no trace — no partial entry, no pending flag, nothing to clean up later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third point is the one to sit with. The absence in the RIB is the whole result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Answer these by reading the source, not by reasoning from what you know about BGP. If you have to guess, go back to the file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A peer's TCP connection is established and packets are flowing, but &lt;code&gt;session_accepts_updates()&lt;/code&gt; returns &lt;code&gt;False&lt;/code&gt;. Where exactly in &lt;code&gt;peer_state.py&lt;/code&gt; does an inbound route get stopped, and what is left behind in &lt;code&gt;Adj-RIB-In&lt;/code&gt; afterward?&lt;/li&gt;
&lt;li&gt;Suppose you moved the &lt;code&gt;store_received_path()&lt;/code&gt; call &lt;em&gt;above&lt;/em&gt; the &lt;code&gt;if&lt;/code&gt; and returned &lt;code&gt;False&lt;/code&gt; afterward anyway. What invariant about &lt;code&gt;Adj-RIB-In&lt;/code&gt; would break, and which downstream reader would notice first?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PeerSession&lt;/code&gt; is &lt;code&gt;frozen=True&lt;/code&gt;. If a peer's session dropped out of &lt;code&gt;Established&lt;/code&gt; after &lt;code&gt;open_peer_session()&lt;/code&gt; returned, what in this file would detect it — and what does your answer tell you about which parts of the state machine this toy model doesn't have?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You're done with this session when you can explain why &lt;code&gt;Established&lt;/code&gt; is a write permission rather than a status label, and point at the exact &lt;code&gt;if&lt;/code&gt; that keeps a route out of &lt;code&gt;Adj-RIB-In&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4271" rel="noopener noreferrer"&gt;RFC 4271 — A Border Gateway Protocol 4 (BGP-4)&lt;/a&gt;, especially §8 (BGP Finite State Machine) and §3.2 (Routing Information Bases)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4760" rel="noopener noreferrer"&gt;RFC 4760 — Multiprotocol Extensions for BGP-4&lt;/a&gt;, for the address-family activation the toy model omits&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc5492" rel="noopener noreferrer"&gt;RFC 5492 — Capabilities Advertisement with BGP-4&lt;/a&gt;, for what else gets negotiated before &lt;code&gt;Established&lt;/code&gt; means anything&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>bgp</category>
      <category>protocol</category>
    </item>
    <item>
      <title>One route, one function chain: reading a BGP pipeline as code</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Wed, 29 Jul 2026 00:00:18 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/one-route-one-function-chain-reading-a-bgp-pipeline-as-code-pdl</link>
      <guid>https://dev.to/pathvector-dev/one-route-one-function-chain-reading-a-bgp-pipeline-as-code-pdl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-bgp-10/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-bgp-10/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic rather than configuration — inputs, state, branches, and the functions that connect them. The full source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;pathvector-studio/protocol-in-code&lt;/a&gt;, and every module reads one real Python file you can open alongside the article. If you're newer to this and want to build up hands-on muscle first, start with the companion &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series, then come back here.&lt;/p&gt;

&lt;p&gt;This is Session 10 of the BGP track, and it's the integration lesson. Sessions 01 through 09 taught the pieces in isolation: attributes, validation, import policy, best-path selection, RIB storage, export. Each one made sense on its own. None of them told you what actually happens to a route.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you trace one received route end to end, which functions touch it before it becomes an advertisement or disappears?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the question to keep turning over while you read. Not "what does best-path do" — you already know that. The question is about the &lt;em&gt;chain&lt;/em&gt;: what order, what state changes where, and which step is responsible for each decision.&lt;/p&gt;

&lt;p&gt;The file is &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/pipeline.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/pipeline.py&lt;/code&gt;&lt;/a&gt;. It's short. That's the point — the integration should be small enough to hold in your head all at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape, before the details
&lt;/h2&gt;

&lt;p&gt;Strip the pipeline down to its skeleton and it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;store_received_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received_attributes&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;received_attributes_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;installable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_candidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received_attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;select_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installable_candidates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;install_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;exported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prepare_export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;export_peer_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;export_policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five lines, five phases: store, evaluate, select, install, export. Everything else in the file is either a data structure that keeps those phases from bleeding into each other, or a branch handling the case where a phase produces nothing.&lt;/p&gt;

&lt;p&gt;Read it in this order and it unfolds cleanly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;PipelinePolicies&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PipelineResult&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;evaluate_candidate()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apply_policy_action()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;process_single_route()&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Two dataclasses that do the structural work
&lt;/h2&gt;

&lt;p&gt;Start with the inputs and outputs, because they tell you what the pipeline thinks its own boundaries are.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PipelinePolicies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;import_policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ImportPolicy&lt;/span&gt;
    &lt;span class="n"&gt;validation_policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ValidationPolicy&lt;/span&gt;
    &lt;span class="n"&gt;export_policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExportPolicy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three separate policy objects, not one blob of configuration. Import policy decides whether a route enters at all. Validation policy decides what an RPKI outcome &lt;em&gt;means&lt;/em&gt; for this operator. Export policy decides what leaves. In a real speaker these live in different config sections for good reason, and the toy keeps that separation visible in the type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PipelineResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;received_validation_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ValidationState&lt;/span&gt;
    &lt;span class="n"&gt;received_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PolicyAction&lt;/span&gt;
    &lt;span class="n"&gt;candidate_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;selected_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;selected_exported_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one is worth staring at. Notice that it reports two different things about the same call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;received_validation_state&lt;/code&gt; and &lt;code&gt;received_action&lt;/code&gt; describe &lt;strong&gt;the route that just arrived&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;selected_path&lt;/code&gt; and &lt;code&gt;selected_exported_path&lt;/code&gt; describe &lt;strong&gt;whatever won for the prefix&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not the same route. A perfectly valid route can arrive, pass validation, pass import policy, survive policy action — and still lose best-path to an incumbent. &lt;code&gt;candidate_count&lt;/code&gt; is the tell: it exists so you can see that the pipeline compared &lt;em&gt;N&lt;/em&gt; survivors, not that it rubber-stamped the one route you happened to feed it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Most toy pipelines you'll find online collapse this into a single "did the route get installed" boolean. That collapse is exactly what makes them useless as a mental model — it hides the fact that arrival and selection are separate events with separate outcomes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Bridging two sessions with one function
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;origin_as_from_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathAttributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four lines, and they're the seam between Session 02 (attributes) and Session 04 (validation). Validation wants a &lt;code&gt;BGPRoute&lt;/code&gt; with an &lt;code&gt;origin_as&lt;/code&gt;. What actually arrived on the wire was a &lt;code&gt;PathAttributes&lt;/code&gt; with an &lt;code&gt;AS_PATH&lt;/code&gt;. Something has to convert one into the other, and in this pipeline it's this function.&lt;/p&gt;

&lt;p&gt;It takes the rightmost AS in the path. Hold onto that — we'll come back to it when we talk about what this model leaves out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a route lives or dies
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;evaluate_candidate()&lt;/code&gt; is the heart of the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate_candidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathAttributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VRP&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PipelinePolicies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ValidationState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PolicyAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BGPRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;origin_as&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;origin_as_from_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;validation_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate_origin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;raw_candidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;candidate_from_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;imported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_import_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;import_policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;imported&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PolicyAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REJECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decide_route_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validation_policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;installed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_policy_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;installed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the order carefully, because the order is the lesson.&lt;/p&gt;

&lt;p&gt;Validation runs &lt;strong&gt;first&lt;/strong&gt;, and it produces a &lt;code&gt;ValidationState&lt;/code&gt; — Valid, Invalid, NotFound. It does not reject anything. It just labels.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;apply_import_policy()&lt;/code&gt; gets both the candidate &lt;em&gt;and&lt;/em&gt; that label. This is the first place a route can vanish: if import policy returns &lt;code&gt;None&lt;/code&gt;, the function short-circuits and reports &lt;code&gt;PolicyAction.REJECT&lt;/code&gt;. The route never reaches best-path.&lt;/p&gt;

&lt;p&gt;If it survives import, &lt;code&gt;decide_route_policy()&lt;/code&gt; translates the validation state into a &lt;code&gt;PolicyAction&lt;/code&gt; under the operator's validation policy. And &lt;em&gt;that&lt;/em&gt; is a separate decision from the label itself. An RPKI-invalid route doesn't have to be dropped; the operator decides.&lt;/p&gt;

&lt;p&gt;The action is then applied:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_policy_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PolicyAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PolicyAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REJECT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PolicyAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEPRIORITIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lowered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_pref&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_pref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lowered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three branches, three outcomes. &lt;code&gt;REJECT&lt;/code&gt; removes the candidate. &lt;code&gt;DEPRIORITIZE&lt;/code&gt; keeps it but knocks 50 off &lt;code&gt;local_pref&lt;/code&gt;, floored at zero — the route stays eligible, it just loses ties it would otherwise win. Anything else passes through untouched.&lt;/p&gt;

&lt;p&gt;This is the distinction that survives all the way into the full pipeline: &lt;strong&gt;a validation result is not a decision&lt;/strong&gt;. &lt;code&gt;ValidationState&lt;/code&gt; is a fact about the route. &lt;code&gt;PolicyAction&lt;/code&gt; is what this operator has chosen to do about that fact. Two separate values, computed by two separate functions, both reported separately in &lt;code&gt;PipelineResult&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full chain
&lt;/h2&gt;

&lt;p&gt;Now &lt;code&gt;process_single_route()&lt;/code&gt;, which is where the state actually moves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="nf"&gt;store_received_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_candidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;installable_candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PathCandidate&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received_attributes&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;received_attributes_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;installable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_candidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received_attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vrps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;installable&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;installable_candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line stores the raw received path in Adj-RIB-In — before any policy, before any validation, before any selection. That ordering matters and it isn't an accident: Adj-RIB-In holds what the peer said, not what you decided about it.&lt;/p&gt;

&lt;p&gt;Then two evaluations happen, and they look redundant until you notice the discard. The first call's result is kept for reporting (&lt;code&gt;validation_state&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;) with the candidate thrown away via &lt;code&gt;_&lt;/code&gt;. The loop then re-evaluates &lt;strong&gt;every&lt;/strong&gt; stored path for the prefix, including the one that just arrived, and collects the survivors. That's the difference between "what did this update do" and "what does the prefix look like now."&lt;/p&gt;

&lt;p&gt;The empty case comes next, and it's the branch most toy models skip entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;installable_candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;remove_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;withdraw_staged_advertisement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;export_peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PipelineResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;received_validation_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;received_action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;candidate_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;selected_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;selected_exported_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nothing survives, stale state has to go. Loc-RIB loses its entry, Adj-RIB-Out loses its staged advertisement. A pipeline that only handles the happy path leaves a withdrawn route advertised forever — which is a real bug class, not a hypothetical.&lt;/p&gt;

&lt;p&gt;Otherwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;select_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;installable_candidates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;install_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;exported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;prepare_export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;export_peer_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;export_policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exported&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;stage_advertisement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;export_peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exported&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;withdraw_staged_advertisement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;export_peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;select_best_path()&lt;/code&gt; compares all survivors — this is the only place multiple candidates meet. &lt;code&gt;install_best_path()&lt;/code&gt; writes to Loc-RIB. &lt;code&gt;prepare_export()&lt;/code&gt; transforms the winner for the outbound peer, and can still return &lt;code&gt;None&lt;/code&gt;, in which case the advertisement gets withdrawn rather than staged.&lt;/p&gt;

&lt;p&gt;Three RIBs, three different moments:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RIB&lt;/th&gt;
&lt;th&gt;Holds&lt;/th&gt;
&lt;th&gt;Changes at&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Adj-RIB-In&lt;/td&gt;
&lt;td&gt;raw received attributes, per peer&lt;/td&gt;
&lt;td&gt;the very first line, before anything is decided&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loc-RIB&lt;/td&gt;
&lt;td&gt;the selected &lt;code&gt;PathCandidate&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;after best-path selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adj-RIB-Out&lt;/td&gt;
&lt;td&gt;the export-transformed candidate&lt;/td&gt;
&lt;td&gt;after export policy, per outbound peer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They hold different objects and they change at different points. That's the sentence to be able to say without notes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same shape, different protocol
&lt;/h2&gt;

&lt;p&gt;The structure here — &lt;em&gt;store what arrived, label it, decide what the label means, select among survivors, transform for output&lt;/em&gt; — is not BGP-specific. It's the shape of every control plane that accepts untrusted input and has to publish a decision.&lt;/p&gt;

&lt;p&gt;DNS resolvers do it: the cache holds what the authoritative server said (Adj-RIB-In), DNSSEC validation labels it (&lt;code&gt;ValidationState&lt;/code&gt;), local policy decides whether a bogus answer is served or SERVFAIL'd (&lt;code&gt;PolicyAction&lt;/code&gt;), and the answer that goes back to the client is a transformed view of the stored record (Adj-RIB-Out). The names differ; the branches are the same.&lt;/p&gt;

&lt;p&gt;Once you can see that shape, reading a new protocol's control plane becomes a matter of finding where each stage lives rather than learning it from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The walkthrough is executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/bgp/session_10_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It seeds one existing candidate first, then introduces a second valid route and recomputes the whole prefix through import policy, validation, policy action, best-path, Loc-RIB installation, and eBGP export. The printed &lt;code&gt;PipelineResult&lt;/code&gt; distinguishes the received route's validation state and action from the selected route that actually won for the prefix — which is precisely the distinction that's easy to nod along to on the page and easy to get wrong in your head.&lt;/p&gt;

&lt;p&gt;Watch &lt;code&gt;candidate_count&lt;/code&gt; in the output. If it's ever 1 when you expected 2, you've learned something about where a route disappeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This is a pipeline, not a BGP speaker. Being specific about the gap:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No session layer.&lt;/strong&gt; There's no peer state machine, no OPEN/KEEPALIVE/NOTIFICATION handling, no hold timers. &lt;code&gt;process_single_route()&lt;/code&gt; assumes a route arrived from a peer that exists and is established. Real speakers spend most of their code on the part this file assumes away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No event dispatch.&lt;/strong&gt; A real speaker has an event loop: updates arrive, timers fire, sessions flap, and each event schedules work. Here you call a function and it runs to completion synchronously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No export refresh after every recompute.&lt;/strong&gt; The pipeline updates Adj-RIB-Out for the single &lt;code&gt;export_peer_id&lt;/code&gt; passed in. A real speaker re-evaluates export for &lt;em&gt;all&lt;/em&gt; peers whenever Loc-RIB changes, and handles route refresh requests. This model does one peer, one call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Origin AS is a shortcut.&lt;/strong&gt; &lt;code&gt;origin_as_from_attributes()&lt;/code&gt; takes the rightmost AS in &lt;code&gt;AS_PATH&lt;/code&gt;. That's a teaching simplification. Real origin determination has to deal with AS_SET segments, AS path prepending edge cases, confederations, and empty paths from iBGP-originated routes. The function returns &lt;code&gt;0&lt;/code&gt; for an empty path, which is a sentinel, not a real AS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The integration order is a choice.&lt;/strong&gt; The course separated best-path, validation, and policy so each could be learned alone. Reconnecting them required picking an order, and this pipeline lets validation influence import and policy before local installation. Real implementations put those boundaries in different places — some run RPKI validation earlier, some fold it into import policy entirely. The order here is pedagogically clean, not canonical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No withdrawal handling from peers.&lt;/strong&gt; Stale state is cleaned up when nothing survives evaluation, but there's no path for a peer explicitly withdrawing a prefix.&lt;/p&gt;

&lt;p&gt;None of these make the model wrong. They make it &lt;em&gt;small&lt;/em&gt; — and small enough to read is the whole point. But don't carry the diagram in this file into a conversation about a production speaker without carrying these caveats too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Close the article and answer these from the source alone. If you have to guess, open &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/pipeline.py" rel="noopener noreferrer"&gt;&lt;code&gt;pipeline.py&lt;/code&gt;&lt;/a&gt; and trace it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which single step can drop a candidate &lt;em&gt;before&lt;/em&gt; it ever reaches best-path — and what does the pipeline report as the action when that happens?&lt;/li&gt;
&lt;li&gt;Which step lowers local preference without rejecting the route, and what's the floor?&lt;/li&gt;
&lt;li&gt;Which step is the only one that compares multiple surviving candidates, and which step changes the outbound AS path?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They're not trick questions. Every answer is a specific function name in a 130-line file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4271" rel="noopener noreferrer"&gt;RFC 4271 — A Border Gateway Protocol 4 (BGP-4)&lt;/a&gt; — §3.2 on the three RIBs, §9.1 on the decision process&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc6811" rel="noopener noreferrer"&gt;RFC 6811 — BGP Prefix Origin Validation&lt;/a&gt; — the Valid / Invalid / NotFound states and the explicit separation of validation from local policy&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc6480" rel="noopener noreferrer"&gt;RFC 6480 — An Infrastructure to Support Secure Internet Routing&lt;/a&gt; — where VRPs come from&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc7454" rel="noopener noreferrer"&gt;RFC 7454 — BGP Operations and Security&lt;/a&gt; — what import and export policy look like in practice&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>bgp</category>
      <category>protocol</category>
    </item>
    <item>
      <title>When a BGP peer disappears, one session loss becomes many route decisions</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Wed, 29 Jul 2026 00:00:10 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/when-a-bgp-peer-disappears-one-session-loss-becomes-many-route-decisions-906</link>
      <guid>https://dev.to/pathvector-dev/when-a-bgp-peer-disappears-one-session-loss-becomes-many-route-decisions-906</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-bgp-09/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-bgp-09/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic — inputs, state, and branches — rather than as configuration examples. Every module points at one small Python file and asks you to read it the way you'd read any other piece of code. The source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;github.com/pathvector-studio/protocol-in-code&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you're earlier on the path and want hands-on packet-level exercises before diving into implementation logic, start with the companion &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What happens to Loc-RIB when one peer disappears and the best path came from that peer?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hold onto that while you read. It sounds like a single question, but it hides at least four: how many routes are affected, which store changes first, what replaces the lost path, and what happens when nothing can.&lt;/p&gt;

&lt;p&gt;The operational answer everyone knows is "the routes go away." That's true and useless. It doesn't tell you why some prefixes survive a peer loss with a brief reconvergence and others vanish entirely, or why a peer that advertised 500 prefixes generates 500 independent decisions rather than one.&lt;/p&gt;

&lt;p&gt;The code answer is more precise, and it's about thirty lines long.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this sits
&lt;/h2&gt;

&lt;p&gt;Session 06 introduced per-peer received state — the idea that Adj-RIB-In isn't one flat table but a mapping keyed by peer, with each peer's advertisements kept separately. That structure is what makes this session possible. If you'd stored all received paths in a single undifferentiated table, "which routes came from this peer?" would be a scan; with per-peer state it's a key lookup.&lt;/p&gt;

&lt;p&gt;The file we're reading is &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/recompute.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/recompute.py&lt;/code&gt;&lt;/a&gt;. Read it in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;recompute_best_path_for_prefix()&lt;/code&gt; — the single-prefix decision&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;handle_peer_loss()&lt;/code&gt; — the loop that applies it&lt;/li&gt;
&lt;li&gt;How lost prefixes are collected from one peer&lt;/li&gt;
&lt;li&gt;How each affected prefix is recomputed &lt;em&gt;independently&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Read it like code
&lt;/h2&gt;

&lt;p&gt;Start with the smaller function, because the bigger one is just a loop around it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recompute_best_path_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocRIB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_candidates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;remove_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="n"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;select_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;install_best_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;best&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four lines of logic and one branch. That branch is the whole story.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;build_candidates(adj_rib_in, prefix)&lt;/code&gt; reads the &lt;em&gt;current&lt;/em&gt; contents of Adj-RIB-In and assembles every path anyone is still advertising for this prefix. Note the tense: current. This function has no memory of what used to be there and no notion of "the peer that just went down." It sees state, not events.&lt;/p&gt;

&lt;p&gt;Then the branch. If the candidate set is empty, there is nothing to install, so &lt;code&gt;remove_best_path()&lt;/code&gt; deletes the Loc-RIB entry and the function returns &lt;code&gt;None&lt;/code&gt;. If the set is non-empty, &lt;code&gt;select_best_path()&lt;/code&gt; runs the decision process over the survivors and &lt;code&gt;install_best_path()&lt;/code&gt; writes the winner into Loc-RIB.&lt;/p&gt;

&lt;p&gt;Read that carefully and notice what &lt;em&gt;isn't&lt;/em&gt; there: the function never asks "was the previous best path from the dead peer?" It doesn't compare old and new. It doesn't need to. It recomputes from scratch and overwrites. This is the difference between an event-driven and a state-driven design, and BGP implementations lean state-driven for exactly this reason — recomputing from current state is much harder to get wrong than incrementally patching a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop that makes it plural
&lt;/h2&gt;

&lt;p&gt;Now the outer function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_peer_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AdjRIBIn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LocRIB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;lost_prefixes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paths_by_peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paths_by_peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lost_prefixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recompute_best_path_for_prefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;adj_rib_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loc_rib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things happen, strictly in order, and the ordering is not incidental.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, snapshot.&lt;/strong&gt; &lt;code&gt;lost_prefixes&lt;/code&gt; is materialized as a &lt;code&gt;tuple&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; anything is mutated. That &lt;code&gt;tuple(...)&lt;/code&gt; isn't stylistic — it copies the keys out of the dict so the subsequent &lt;code&gt;pop&lt;/code&gt; doesn't invalidate what we're about to iterate over. This is the list of prefixes that were affected, captured at the moment of loss.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, delete.&lt;/strong&gt; &lt;code&gt;adj_rib_in.paths_by_peer.pop(peer_id, None)&lt;/code&gt; removes the entire per-peer subtree. Every path that peer ever advertised is gone from received state in one operation. The &lt;code&gt;None&lt;/code&gt; default means calling this for an unknown peer is a no-op rather than a &lt;code&gt;KeyError&lt;/code&gt; — losing a peer you weren't tracking should be harmless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third, recompute.&lt;/strong&gt; Only &lt;em&gt;after&lt;/em&gt; the deletion does the loop run. This ordering answers one of the questions you should be holding: Adj-RIB-In changes first, Loc-RIB second. It has to. &lt;code&gt;recompute_best_path_for_prefix()&lt;/code&gt; calls &lt;code&gt;build_candidates()&lt;/code&gt;, which reads Adj-RIB-In — if the dead peer's paths were still in there, &lt;code&gt;build_candidates()&lt;/code&gt; would happily hand them back and the peer's paths could get reinstalled as best. The deletion isn't cleanup after the decision; it's the input to it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;for&lt;/code&gt; loop is where "one session loss" becomes "many route decisions." One peer, N prefixes, N independent invocations of the decision process. There is no batching, no shared state between iterations, no notion of "the peer went down" inside the per-prefix function at all. Each prefix asks the same question in isolation: &lt;em&gt;given who's still advertising, what's best now?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And each can get a different answer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Another peer also advertises this prefix&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;build_candidates()&lt;/code&gt; returns a non-empty set, &lt;code&gt;select_best_path()&lt;/code&gt; picks a survivor, Loc-RIB gets a new best path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The dead peer was the only source&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;build_candidates()&lt;/code&gt; returns empty, &lt;code&gt;remove_best_path()&lt;/code&gt; fires, prefix disappears from Loc-RIB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The dead peer wasn't best for this prefix anyway&lt;/td&gt;
&lt;td&gt;Recomputation runs regardless and reinstalls the same winner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That third row is worth sitting with. The code doesn't optimize it away. It recomputes prefixes whose best path was never in danger, because checking "was this prefix's best path from the dead peer?" is itself a decision that can be wrong, while recomputing is unconditionally correct. Correctness first; a real implementation would add the optimization later, guarded carefully.&lt;/p&gt;

&lt;p&gt;The return value — &lt;code&gt;dict[str, PathCandidate | None]&lt;/code&gt; — is the outcome of every one of those decisions. A &lt;code&gt;PathCandidate&lt;/code&gt; means a path survived or replaced; &lt;code&gt;None&lt;/code&gt; means the prefix is gone. The &lt;code&gt;| None&lt;/code&gt; in the type signature is the withdrawal case made explicit in the type system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same shape, elsewhere
&lt;/h2&gt;

&lt;p&gt;The pattern here isn't BGP-specific: &lt;strong&gt;maintain per-source state, and when a source disappears, delete its contribution and re-derive the aggregate from what's left.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DNS resolvers do this when a nameserver stops responding — the cached RRsets attributed to it become unusable, and resolution falls back to re-deriving an answer from the remaining authoritative servers. Link-state protocols do it when an LSA ages out: remove the contribution, rerun SPF over the surviving topology. Even TCP's congestion state has the shape, one level down — a loss signal doesn't patch &lt;code&gt;cwnd&lt;/code&gt; incrementally so much as re-derive it from the current state of the connection.&lt;/p&gt;

&lt;p&gt;The alternative design — event-driven incremental patching, where "peer down" tries to surgically fix only the Loc-RIB entries it believes are affected — is faster and much easier to get subtly wrong. Every protocol that has tried it has accumulated a long tail of "stale entry" bugs. The recompute-from-current-state shape trades CPU for the guarantee that the output is always a function of the present input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The walkthrough is executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/bgp/session_09_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It installs a best path from one peer, simulates that peer going down, and shows the backup path taking over. Watch the transition specifically: the prefix stays in Loc-RIB but its best path changes identity. Then modify the script to remove the second peer's advertisement and run it again — now the same code path produces a disappearance instead of a substitution. Same function, same branch, different candidate set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This module is a reading aid, not a BGP implementation. Be clear about what it leaves out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No BGP session machinery.&lt;/strong&gt; &lt;code&gt;handle_peer_loss()&lt;/code&gt; is called by you, with a &lt;code&gt;peer_id&lt;/code&gt; string. Real peer loss is detected by the FSM — hold timer expiry, TCP reset, &lt;code&gt;NOTIFICATION&lt;/code&gt; received, an explicit administrative shutdown — and each of those has different timing characteristics. Hold-timer detection can take tens of seconds; a TCP RST is near-instant. The model has no timers at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No withdrawal propagation.&lt;/strong&gt; In the real protocol, changes to Loc-RIB feed Adj-RIB-Out and generate UPDATE messages toward other peers, subject to export policy. Here, the function returns a dict and stops. Everything downstream of the local decision is absent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No route flap damping, no MRAI.&lt;/strong&gt; Real BGP deliberately slows reconvergence: the MinRouteAdvertisementInterval timer rate-limits how often a prefix's changes go out, and damping penalizes prefixes that flap. This model reconverges instantaneously and without hysteresis, which is precisely the behavior operators spend effort suppressing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No graceful restart.&lt;/strong&gt; &lt;a href="https://www.rfc-editor.org/rfc/rfc4724.html" rel="noopener noreferrer"&gt;RFC 4724&lt;/a&gt; exists so that a peer going down &lt;em&gt;doesn't&lt;/em&gt; immediately trigger this code path — routes are marked stale and retained while the peer restarts. The model has no stale-route concept and no restart signalling; loss is always immediate and total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete-then-recompute is not atomic.&lt;/strong&gt; The &lt;code&gt;pop&lt;/code&gt; happens, then the loop runs. In between, Adj-RIB-In and Loc-RIB disagree. A single-threaded toy never observes that window, but real implementations have concurrent readers and need locking or versioning to keep anyone from reading the inconsistent middle state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;select_best_path()&lt;/code&gt; is simplified.&lt;/strong&gt; The decision process here is a reduced version of the real tie-break ladder in &lt;a href="https://www.rfc-editor.org/rfc/rfc4271.html#section-9.1" rel="noopener noreferrer"&gt;RFC 4271 §9.1&lt;/a&gt;, and the model has no ADD-PATH, no multipath, no RIB failure states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost is not modeled.&lt;/strong&gt; N prefixes means N recomputations. With a full table that's ~1M invocations, and real implementations do a great deal of work — prefix indexing, batched walks, deferred best-path runs — to make peer loss survivable. The model's flat loop makes the logic obvious and the performance question invisible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Can you answer these just by reading the source? Don't take my word for any of it — go verify against &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/recompute.py" rel="noopener noreferrer"&gt;&lt;code&gt;recompute.py&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Which store changes first, Adj-RIB-In or Loc-RIB — and what would break if you swapped the order?&lt;/strong&gt; Trace what &lt;code&gt;build_candidates()&lt;/code&gt; would return if &lt;code&gt;pop&lt;/code&gt; ran after the loop instead of before it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Under exactly what condition does a prefix disappear from Loc-RIB entirely?&lt;/strong&gt; Find the single expression that decides it, and name the state that has to hold across all remaining peers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why is &lt;code&gt;lost_prefixes&lt;/code&gt; built with &lt;code&gt;tuple(...)&lt;/code&gt; rather than used directly as a dict view?&lt;/strong&gt; What concretely goes wrong if you drop the &lt;code&gt;tuple()&lt;/code&gt; call?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You've got this module when you can say, without notes: peer loss deletes Adj-RIB-In state for that peer first; then each affected prefix is recomputed independently; a backup path can become best; and if no backup exists, the Loc-RIB entry disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4271.html" rel="noopener noreferrer"&gt;RFC 4271 — A Border Gateway Protocol 4 (BGP-4)&lt;/a&gt;, particularly §9.1 (Decision Process) and §9.2 (Update-Send Process)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc4724.html" rel="noopener noreferrer"&gt;RFC 4724 — Graceful Restart Mechanism for BGP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc2439.html" rel="noopener noreferrer"&gt;RFC 2439 — BGP Route Flap Damping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Source for this module: &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/recompute.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/recompute.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Walkthrough: &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/examples/bgp/session_09_walkthrough.py" rel="noopener noreferrer"&gt;&lt;code&gt;examples/bgp/session_09_walkthrough.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>bgp</category>
      <category>routing</category>
    </item>
    <item>
      <title>Export policy decides what leaves: reading BGP outbound state as code</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:00:23 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/export-policy-decides-what-leaves-reading-bgp-outbound-state-as-code-2550</link>
      <guid>https://dev.to/pathvector-dev/export-policy-decides-what-leaves-reading-bgp-outbound-state-as-code-2550</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-bgp-08/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-bgp-08/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic — inputs, state, and branches — rather than as configuration examples. Each module points at one small Python file and asks you to read it the way you'd read any other code: what comes in, what gets mutated, where does it return early. The source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;github.com/pathvector-studio/protocol-in-code&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you're earlier in the journey and want to &lt;em&gt;do&lt;/em&gt; things with protocols before dissecting them, start with the hands-on &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series instead — it builds the muscle memory this series assumes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;Here's the thing that trips people up when they first look at a real BGP table: &lt;strong&gt;why is the route we advertise to a peer not always identical to the route we installed locally?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You run &lt;code&gt;show ip bgp&lt;/code&gt; on a router, you see a prefix with a next-hop and an AS path. You run the equivalent command on the peer that received it from you, and the fields don't match. The next-hop changed. The AS path grew. Sometimes the prefix isn't there at all — even though it's clearly still installed on your side.&lt;/p&gt;

&lt;p&gt;None of this is mysterious once you stop thinking of advertisement as "copying the table over the wire." It isn't a copy. It's a &lt;em&gt;transform&lt;/em&gt;, and it lives in its own function with its own inputs. Session 06 already established that Adj-RIB-Out is a separate place where outbound state lives. This session gives that outbound state its own policy step.&lt;/p&gt;

&lt;h2&gt;
  
  
  The file
&lt;/h2&gt;

&lt;p&gt;Everything in this module happens in &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/export_policy.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/export_policy.py&lt;/code&gt;&lt;/a&gt;. It's short enough to hold entirely in your head, which is the point.&lt;/p&gt;

&lt;p&gt;Read it in this order: &lt;code&gt;PeerType&lt;/code&gt;, then &lt;code&gt;ExportPolicy&lt;/code&gt;, then &lt;code&gt;prepare_export()&lt;/code&gt;. Inside &lt;code&gt;prepare_export()&lt;/code&gt;, notice four things in sequence — the deny check, &lt;code&gt;next_hop_self&lt;/code&gt;, the eBGP branch, and what the function actually returns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two peer types, because export isn't uniform
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PeerType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;EBGP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ebgp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;IBGP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ibgp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole enum. It exists because the export path branches on it — eBGP export and iBGP export do not look the same, and the code makes that structural rather than incidental. If you've only ever configured BGP through a vendor CLI, this is worth pausing on: the peer type isn't a label on a neighbor statement, it's a &lt;em&gt;branch condition&lt;/em&gt; in the outbound path.&lt;/p&gt;

&lt;h3&gt;
  
  
  The policy is data, not behavior
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExportPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;local_as&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;next_hop_self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;deny_prefixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;extra_prepend_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four fields, each of which corresponds to something you've probably typed into a router config without thinking about it as a function parameter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deny_prefixes&lt;/code&gt; — the set of prefixes that never leave, regardless of what's installed locally.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next_hop_self&lt;/code&gt; — whether outbound state gets a rewritten next-hop.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;local_as&lt;/code&gt; — what gets prepended on eBGP export.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extra_prepend_count&lt;/code&gt; — deliberate path inflation, the traffic-engineering knob.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dataclass is &lt;code&gt;frozen=True&lt;/code&gt;, and that matters for how you read the next function. Policy is immutable input. The path is immutable input. Everything that happens in &lt;code&gt;prepare_export()&lt;/code&gt; produces a &lt;em&gt;new&lt;/em&gt; object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading &lt;code&gt;prepare_export()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here's the function in full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;prepare_export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;peer_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PeerType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExportPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deny_prefixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="n"&gt;exported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_hop_self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;exported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_hop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;self&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;peer_type&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PeerType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EBGP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prepend_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extra_prepend_count&lt;/span&gt;
        &lt;span class="n"&gt;exported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;exported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;as_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_as&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;prepend_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exported&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at the signature first. It takes a &lt;code&gt;PathCandidate&lt;/code&gt; and returns &lt;code&gt;PathCandidate | None&lt;/code&gt;. That &lt;code&gt;None&lt;/code&gt; in the return type is doing a lot of conceptual work — it's the type system telling you that "there is a best path" and "the peer hears about it" are two separate propositions.&lt;/p&gt;

&lt;h3&gt;
  
  
  The deny check comes first
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deny_prefixes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the first statement in the function, before any transformation happens, and it returns &lt;code&gt;None&lt;/code&gt; rather than raising, logging, or removing anything.&lt;/p&gt;

&lt;p&gt;Notice what it does &lt;em&gt;not&lt;/em&gt; touch: &lt;code&gt;path&lt;/code&gt; is unchanged, and nothing anywhere in this function reaches back into Loc-RIB. A denied prefix is still installed. It's still the best path. Your forwarding table still uses it. The peer simply never hears about it.&lt;/p&gt;

&lt;p&gt;That asymmetry is the single most useful thing in this module. "The route isn't in my table" and "the route isn't in my neighbor's table" are different failures with different fixes, and this early return is where the difference is made concrete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next-hop rewrite is a state divergence
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;exported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_hop_self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;exported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_hop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;self&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;exported = path&lt;/code&gt; starts the transform chain by aliasing the input, then every step uses &lt;code&gt;dataclasses.replace&lt;/code&gt; to build a new frozen instance. Read this as an accumulator: &lt;code&gt;exported&lt;/code&gt; is the outbound view under construction, and &lt;code&gt;path&lt;/code&gt; stays pristine as the locally installed view.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;next-hop-self&lt;/code&gt; is the canonical case where those two views diverge. The local route points at whatever next-hop it learned. The advertised route points at you. Same prefix, same best-path decision, different attribute on the wire.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;next_hop="self"&lt;/code&gt; here is a symbolic placeholder, not a literal forwarding address rewrite. The toy model is showing you &lt;em&gt;that&lt;/em&gt; outbound state can differ from local state and &lt;em&gt;where&lt;/em&gt; that divergence is implemented — not the address arithmetic a real implementation performs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The eBGP branch transforms the path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;peer_type&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PeerType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EBGP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;prepend_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extra_prepend_count&lt;/span&gt;
        &lt;span class="n"&gt;exported&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;exported&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;as_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_as&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;prepend_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exported&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;as_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are packed into &lt;code&gt;prepend_count = 1 + policy.extra_prepend_count&lt;/code&gt;. The &lt;code&gt;1&lt;/code&gt; is the baseline: crossing an AS boundary means your AS number goes on the front of the path. The &lt;code&gt;extra_prepend_count&lt;/code&gt; is deliberate — you're making the path look longer than it is, because path length is an input to somebody else's best-path selection and you'd rather they picked a different entrance.&lt;/p&gt;

&lt;p&gt;The tuple arithmetic makes the semantics unambiguous. &lt;code&gt;(policy.local_as,) * prepend_count&lt;/code&gt; builds a run of repeated AS numbers, and &lt;code&gt;+ exported.as_path&lt;/code&gt; puts them at the &lt;em&gt;front&lt;/em&gt;. AS path is read left to right as most-recent-first; prepending is how you get there.&lt;/p&gt;

&lt;p&gt;And the iBGP case? There's no &lt;code&gt;else&lt;/code&gt;. When &lt;code&gt;peer_type&lt;/code&gt; is &lt;code&gt;IBGP&lt;/code&gt;, this block is skipped entirely and &lt;code&gt;as_path&lt;/code&gt; passes through untouched — which is exactly right, because no AS boundary was crossed. The absence of code is the protocol behavior here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same shape, different protocol
&lt;/h2&gt;

&lt;p&gt;Once you've seen this structure, you start noticing it elsewhere. The pattern is: &lt;strong&gt;a local decision produces internal state, and a separate policy step decides what portion of that state is externally visible, in what form.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DNS does this with authoritative zone data versus what a resolver actually returns to a client. TLS does it with the full certificate chain a server holds versus what it chooses to present in a given handshake. In every case the mistake is the same — assuming the outbound view is a read of the internal view rather than a transform of it. Debugging gets much faster when you stop asking "why doesn't my peer see it?" and start asking "which function decides what my peer sees, and what are its inputs?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The walkthrough is executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/bgp/session_08_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs three cases against the same input path: eBGP export, iBGP export with &lt;code&gt;next-hop-self&lt;/code&gt; enabled, and a deny case where the route stays local but never gets advertised. Watching the same &lt;code&gt;PathCandidate&lt;/code&gt; come out three different ways — and once as &lt;code&gt;None&lt;/code&gt; — is more convincing than reading about it.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/examples/bgp/session_08_walkthrough.py" rel="noopener noreferrer"&gt;&lt;code&gt;examples/bgp/session_08_walkthrough.py&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This is a teaching model, and it's worth being precise about what it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;next_hop="self"&lt;/code&gt; is symbolic.&lt;/strong&gt; A real implementation rewrites the NEXT_HOP attribute to an actual address — typically a local interface or loopback address chosen per session. The string &lt;code&gt;"self"&lt;/code&gt; here is a marker that a rewrite happened, not a representation of the value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The eBGP branch compresses several behaviors into one.&lt;/strong&gt; Real eBGP export does more than prepend the local AS. It handles NEXT_HOP semantics that vary by topology and session type, MED treatment across AS boundaries, LOCAL_PREF stripping (a well-known attribute that must not cross an eBGP boundary), community handling, and more. This module collapses all of that into a single visible transformation so the &lt;em&gt;shape&lt;/em&gt; — outbound state is transformed, not copied — is unmissable. Don't read the single prepend as a claim of completeness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;next_hop_self&lt;/code&gt; is presented as a boolean knob.&lt;/strong&gt; In production, NEXT_HOP behavior on export depends on peer type, whether the route was learned from an eBGP or iBGP peer, whether it's a directly connected route, route-reflector configuration, and per-vendor defaults. Treating it as one flag is a teaching simplification, not a claim about default behavior in every real eBGP export case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no route-reflector logic here.&lt;/strong&gt; iBGP export in real deployments involves reflection rules, cluster lists, and originator IDs. This model's iBGP path is simply "skip the prepend."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policy is a single function with four fields.&lt;/strong&gt; Real export policy is a chain of route-maps and filter lists with match/set clauses evaluated in sequence, capable of modifying essentially any attribute. &lt;code&gt;prepare_export()&lt;/code&gt; is one function with fixed branches so you can see the control flow rather than a policy DSL.&lt;/p&gt;

&lt;p&gt;The boundary is the point, not an apology for it. The model is deliberately small enough that the branching structure is visible, and once you've internalized that structure the real implementations read as elaborations rather than mysteries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Go back to the source and answer these without looking anywhere else. If you can't, you haven't read it closely enough yet.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which single condition causes &lt;code&gt;prepare_export()&lt;/code&gt; to return &lt;code&gt;None&lt;/code&gt; — and what does the function do to the locally installed path in that case?&lt;/li&gt;
&lt;li&gt;Under what circumstances does &lt;code&gt;next_hop&lt;/code&gt; become &lt;code&gt;"self"&lt;/code&gt;, and does the peer type affect that at all?&lt;/li&gt;
&lt;li&gt;Trace the value of &lt;code&gt;exported.as_path&lt;/code&gt; for an iBGP peer versus an eBGP peer with &lt;code&gt;extra_prepend_count=2&lt;/code&gt;. Which line is responsible for the difference, and which line is responsible for the &lt;em&gt;absence&lt;/em&gt; of a difference?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can articulate why "installed route" and "exported route" are different objects in practice — and point at the exact lines that make them different — the module has done its job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc4271" rel="noopener noreferrer"&gt;RFC 4271 — A Border Gateway Protocol 4 (BGP-4)&lt;/a&gt;, particularly §9.2 on Update-Send Process and §9.1 on the Decision Process, for how advertisement is specified as a phase distinct from path selection.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc4456" rel="noopener noreferrer"&gt;RFC 4456 — BGP Route Reflection&lt;/a&gt;, for what this model's iBGP path deliberately omits.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://datatracker.ietf.org/doc/html/rfc7454" rel="noopener noreferrer"&gt;RFC 7454 — BGP Operations and Security&lt;/a&gt;, for why &lt;code&gt;deny_prefixes&lt;/code&gt; is the least toy-like field in the whole dataclass.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>bgp</category>
      <category>protocol</category>
    </item>
    <item>
      <title>Import policy rewrites the route before best-path ever sees it</title>
      <dc:creator>pathvector-dev</dc:creator>
      <pubDate>Mon, 27 Jul 2026 16:00:15 +0000</pubDate>
      <link>https://dev.to/pathvector-dev/import-policy-rewrites-the-route-before-best-path-ever-sees-it-3429</link>
      <guid>https://dev.to/pathvector-dev/import-policy-rewrites-the-route-before-best-path-ever-sees-it-3429</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://blog.pathvector.dev/protocol-in-code-bgp-07/" rel="noopener noreferrer"&gt;https://blog.pathvector.dev/protocol-in-code-bgp-07/&lt;/a&gt; — part of the free &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; series.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post is part of &lt;strong&gt;Protocol in Code&lt;/strong&gt;, a free series that reads network protocols as logic — inputs, state, and branches — rather than as configuration examples. Every module points at one real Python file and asks you to read it the way you'd read any other code: what comes in, what mutates, where does control leave early. The source lives at &lt;a href="https://github.com/pathvector-studio/protocol-in-code" rel="noopener noreferrer"&gt;github.com/pathvector-studio/protocol-in-code&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you're newer to this and want to &lt;em&gt;run&lt;/em&gt; things before you read things, start with &lt;a href="https://github.com/pathvector-studio/protocol-lab" rel="noopener noreferrer"&gt;Protocol Lab&lt;/a&gt; — the hands-on companion series that builds the muscle memory this one assumes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How does local import policy change or reject a path before best-path selection runs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the whole module in one line, and it hides a claim worth being suspicious of. Best-path selection in BGP is the famous part — the ordered tiebreaker list everyone half-remembers: highest weight, highest &lt;code&gt;local_pref&lt;/code&gt;, shortest AS path, and so on. It's easy to treat that comparison as the decision point, as if routes arrive from peers and get ranked.&lt;/p&gt;

&lt;p&gt;They don't arrive and get ranked. They arrive, get &lt;em&gt;rewritten&lt;/em&gt;, and then get ranked. Import policy is a function that runs between the wire and the comparison, and it has two powers: it can change the values the comparison reads, and it can make the candidate not exist at all.&lt;/p&gt;

&lt;p&gt;Which means the interesting question isn't "who won best-path" but "what did best-path actually receive."&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the code
&lt;/h2&gt;

&lt;p&gt;The file is &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/import_policy.py" rel="noopener noreferrer"&gt;&lt;code&gt;src/protocol_in_code/bgp/import_policy.py&lt;/code&gt;&lt;/a&gt;. It's short enough to hold in your head all at once, which is the point — the shape is the lesson.&lt;/p&gt;

&lt;p&gt;Start with the policy object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ImportPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;local_pref_override&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;reject_next_hops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;reject_invalid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four knobs, and notice they're not four of the same thing. Two of them (&lt;code&gt;local_pref_override&lt;/code&gt;, &lt;code&gt;weight&lt;/code&gt;) change a value. Two of them (&lt;code&gt;reject_next_hops&lt;/code&gt;, &lt;code&gt;reject_invalid&lt;/code&gt;) delete the route. A frozen dataclass, so the policy itself is immutable — the policy doesn't accumulate state across candidates, it's just a description of a transformation.&lt;/p&gt;

&lt;p&gt;Also notice the default of &lt;code&gt;local_pref_override&lt;/code&gt;: it's &lt;code&gt;None&lt;/code&gt;, not &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;100&lt;/code&gt;. That's load-bearing. &lt;code&gt;weight&lt;/code&gt; defaults to &lt;code&gt;0&lt;/code&gt; because zero &lt;em&gt;is&lt;/em&gt; a meaningful weight; &lt;code&gt;local_pref_override&lt;/code&gt; needs a sentinel because there's no integer that means "don't touch this."&lt;/p&gt;

&lt;p&gt;Now the function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply_import_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;validation_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ValidationState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ImportPolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PathCandidate&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The return type tells you the story before you read a single line of the body: &lt;code&gt;PathCandidate | None&lt;/code&gt;. This function is allowed to return nothing. A path goes in and possibly no path comes out — the candidate is gone before best-path selection has any opinion about it.&lt;/p&gt;

&lt;p&gt;Read the body in the order it executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_hop&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reject_next_hops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;validation_state&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;ValidationState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INVALID&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reject_invalid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rejection first, both times. These are early returns, and their position matters: nothing gets rewritten before it gets dropped. There's no point spending work on a candidate that's about to stop existing, and — more importantly for reading — it means you can answer "does this route survive?" without reading the second half of the function at all.&lt;/p&gt;

&lt;p&gt;The two rejections are different in kind. The first is unconditional local intent: this next-hop is on the list, so no. The second is conditional on a value computed elsewhere — &lt;code&gt;validation_state&lt;/code&gt; comes in as a parameter, decided by validation logic this file never sees. And it's gated by &lt;code&gt;policy.reject_invalid&lt;/code&gt;. Read that conjunction carefully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;validation_state&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;ValidationState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INVALID&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reject_invalid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;INVALID&lt;/code&gt; alone doesn't drop the route. The local operator has to have opted in. This is the module's real point about validation: validation produces a &lt;em&gt;result&lt;/em&gt;, and policy decides what that result &lt;em&gt;does&lt;/em&gt;. A route can be known-invalid and still sail through to best-path selection if &lt;code&gt;reject_invalid&lt;/code&gt; is &lt;code&gt;False&lt;/code&gt;. That separation — result versus action — is the thing Session 05 set up, and here's where it gets consumed.&lt;/p&gt;

&lt;p&gt;Then the rewrite half:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_pref_override&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_pref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local_pref_override&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;replace()&lt;/code&gt; is &lt;code&gt;dataclasses.replace&lt;/code&gt; — it builds a new frozen &lt;code&gt;PathCandidate&lt;/code&gt; with one field swapped. Nothing mutates. &lt;code&gt;updated&lt;/code&gt; is rebound, never modified in place, and the original &lt;code&gt;candidate&lt;/code&gt; the caller passed in is still intact afterward. If you've been reading this codebase in order, that pattern should be familiar by now: every stage returns a new value rather than editing the one it received, which is what makes it possible to reason about a pipeline stage in isolation.&lt;/p&gt;

&lt;p&gt;The two conditions guarding the rewrites are worth comparing side by side, because they don't match:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;if policy.local_pref_override is not None:&lt;/code&gt; — a sentinel check. Is an override configured at all?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if policy.weight != updated.weight:&lt;/code&gt; — a difference check. Is the configured weight already what the candidate has?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second one is an optimization more than a semantic: with the default &lt;code&gt;weight=0&lt;/code&gt; and a candidate that also has &lt;code&gt;weight=0&lt;/code&gt;, you skip building a new object for no change. But it also means &lt;code&gt;weight&lt;/code&gt; has no "unset" state. Leaving &lt;code&gt;weight&lt;/code&gt; alone in your policy isn't neutral — it's asserting &lt;code&gt;0&lt;/code&gt;. If a candidate arrived with a non-zero weight from somewhere upstream, a policy that never mentions &lt;code&gt;weight&lt;/code&gt; will still flatten it back to &lt;code&gt;0&lt;/code&gt;. Whether that's a bug or the intended semantics is exactly the kind of thing worth deciding for yourself by reading it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does to best-path
&lt;/h2&gt;

&lt;p&gt;Here's the connection that makes the module worth the time. &lt;code&gt;local_pref&lt;/code&gt; and &lt;code&gt;weight&lt;/code&gt; are not arbitrary fields — they're the top two inputs to the best-path comparison. Import policy writes directly into the highest-priority tiebreakers &lt;em&gt;before&lt;/em&gt; the tiebreakers run.&lt;/p&gt;

&lt;p&gt;So the pipeline is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A peer sends you a path with whatever attributes it chose.&lt;/li&gt;
&lt;li&gt;Validation produces a &lt;code&gt;ValidationState&lt;/code&gt; for it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apply_import_policy&lt;/code&gt; either drops it or hands back a modified copy.&lt;/li&gt;
&lt;li&gt;Best-path selection compares what survived.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 4 has no idea steps 1–3 happened. It sees a &lt;code&gt;PathCandidate&lt;/code&gt; with a &lt;code&gt;local_pref&lt;/code&gt; and a &lt;code&gt;weight&lt;/code&gt; and compares them, and it cannot distinguish "the peer sent this" from "we wrote this a microsecond ago." That's the sentence to walk away with: &lt;strong&gt;the candidate entering best-path is not always the one the peer originally sent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is also why "the same route looks different on different routers" isn't a mystery. Two routers receiving the identical UPDATE can hand two different candidates to their comparison logic, because import policy is local. Nothing about it is negotiated with the peer. The peer doesn't know and can't tell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same shape, different protocol
&lt;/h2&gt;

&lt;p&gt;Once you've seen this shape you'll notice it everywhere: &lt;strong&gt;a transform stage that sits in front of a decision stage and quietly rewrites the decision's inputs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's the same structure as a firewall's mangle table rewriting packet marks before the routing decision reads them — the routing table lookup doesn't know the mark was synthetic. It's the same structure as a DNS resolver's local overrides answering before recursion happens. It's the same structure as a TLS stack's cipher-suite preference list reordering what the handshake "sees" as offered.&lt;/p&gt;

&lt;p&gt;In each case the decision logic is honest and deterministic, and the interesting behavior lives entirely in what got fed to it. When a protocol surprises you, the transform stage in front of the decision is usually a better place to look than the decision itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it
&lt;/h2&gt;

&lt;p&gt;The walkthrough is executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python3 examples/bgp/session_07_walkthrough.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs four candidates through &lt;code&gt;apply_import_policy&lt;/code&gt;: one that gets its &lt;code&gt;local_pref&lt;/code&gt; rewritten, one that picks up a local &lt;code&gt;weight&lt;/code&gt;, one dropped by the next-hop rule, and one invalid path rejected before best-path ever sees it. Watch which ones come back as objects and which come back as &lt;code&gt;None&lt;/code&gt; — that binary is the entire first half of the function, made visible.&lt;/p&gt;

&lt;p&gt;Then change the policy and run it again. Set &lt;code&gt;reject_invalid=False&lt;/code&gt; and watch the invalid path survive. Set &lt;code&gt;local_pref_override&lt;/code&gt; without &lt;code&gt;weight&lt;/code&gt; and see exactly one field move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toy model boundary
&lt;/h2&gt;

&lt;p&gt;This is a toy model, and being precise about what it isn't is the point of this section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real import policy is a rule chain, not a single struct.&lt;/strong&gt; Vendor implementations evaluate an ordered list of route-map or filter clauses, each with match conditions and set actions, with terminating and continue semantics. &lt;code&gt;ImportPolicy&lt;/code&gt; here is one flat set of unconditional knobs applied to every candidate — there is no "match prefix 10.0.0.0/8 then set local-pref 200, else next clause." That ordering and matching is most of the complexity of real policy, and none of it is here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only two attributes are writable.&lt;/strong&gt; Real policy can rewrite MED, prepend or replace the AS path, add, remove, and modify communities and large communities, set the next-hop, tag the route, and more. This model touches &lt;code&gt;local_pref&lt;/code&gt; and &lt;code&gt;weight&lt;/code&gt; because those are the two that most cleanly demonstrate "policy writes best-path's inputs."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rejection is a return value, not a state.&lt;/strong&gt; Here a dropped path simply ceases to exist. Real implementations distinguish between filtered-at-input routes that are discarded and routes retained in an Adj-RIB-In (soft reconfiguration) so policy can be re-applied without bouncing the session. There's no RIB in this file at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;weight&lt;/code&gt; isn't a protocol field.&lt;/strong&gt; &lt;code&gt;local_pref&lt;/code&gt; is a real BGP path attribute carried in UPDATE messages within an AS. &lt;code&gt;weight&lt;/code&gt; is a vendor-local, Cisco-originated concept that never leaves the router and never appears on the wire. Putting them adjacent in one dataclass is pedagogically useful and protocol-wise a bit of a lie — worth knowing before you go looking for weight in a packet capture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No prefix matching, no peer scoping, no direction.&lt;/strong&gt; Real policy is bound per-neighbor and per-address-family, and there's a symmetric export policy on the other side doing the same kind of rewriting outbound. This function takes one candidate with no notion of which peer it came from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation state arrives from nowhere.&lt;/strong&gt; &lt;code&gt;validation_state&lt;/code&gt; is a parameter. In a real system it comes from RPKI-to-Router state that changes asynchronously as VRPs are added and withdrawn, which raises re-evaluation questions this model doesn't have to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check yourself
&lt;/h2&gt;

&lt;p&gt;Don't look these up — answer them by reading &lt;a href="https://github.com/pathvector-studio/protocol-in-code/blob/main/src/protocol_in_code/bgp/import_policy.py" rel="noopener noreferrer"&gt;&lt;code&gt;import_policy.py&lt;/code&gt;&lt;/a&gt; and then confirm with the walkthrough.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exactly which conditions cause &lt;code&gt;apply_import_policy&lt;/code&gt; to return &lt;code&gt;None&lt;/code&gt;?&lt;/strong&gt; Can you state both, including what has to be true for the second one to fire?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If a policy sets only &lt;code&gt;local_pref_override&lt;/code&gt; and leaves everything else at its default, what does the returned &lt;code&gt;PathCandidate&lt;/code&gt; look like?&lt;/strong&gt; Be specific about &lt;code&gt;weight&lt;/code&gt; — is it untouched, or not?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why is rewriting an input a fundamentally different operation from rejecting a path?&lt;/strong&gt; Both change the outcome of best-path selection. What can the downstream comparison logic tell about which one happened to it?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You should be able to finish this module able to say, without notes: import policy runs before best-path; it can rewrite &lt;code&gt;local_pref&lt;/code&gt; and &lt;code&gt;weight&lt;/code&gt;; it can drop a path before comparison; and the candidate entering best-path is not always the one the peer sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4271" rel="noopener noreferrer"&gt;RFC 4271&lt;/a&gt; — A Border Gateway Protocol 4 (BGP-4), for the decision process and &lt;code&gt;LOCAL_PREF&lt;/code&gt; semantics&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc4456" rel="noopener noreferrer"&gt;RFC 4456&lt;/a&gt; — BGP Route Reflection, for why &lt;code&gt;local_pref&lt;/code&gt; propagation inside an AS matters&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc6811" rel="noopener noreferrer"&gt;RFC 6811&lt;/a&gt; — BGP Prefix Origin Validation, for where &lt;code&gt;ValidationState&lt;/code&gt; comes from and what "invalid" is supposed to mean&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>network</category>
      <category>bgp</category>
      <category>protocol</category>
    </item>
  </channel>
</rss>
