<?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: Mikhail</title>
    <description>The latest articles on DEV Community by Mikhail (@vhex).</description>
    <link>https://dev.to/vhex</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%2F3979874%2F147a190f-90d5-4527-b2b7-e84b1f4066f1.jpg</url>
      <title>DEV Community: Mikhail</title>
      <link>https://dev.to/vhex</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vhex"/>
    <language>en</language>
    <item>
      <title>How I Wrote a BGP Server and Didn't Lose My Mind</title>
      <dc:creator>Mikhail</dc:creator>
      <pubDate>Thu, 11 Jun 2026 20:18:03 +0000</pubDate>
      <link>https://dev.to/vhex/how-i-wrote-a-bgp-server-and-didnt-lose-my-mind-16fc</link>
      <guid>https://dev.to/vhex/how-i-wrote-a-bgp-server-and-didnt-lose-my-mind-16fc</guid>
      <description>&lt;p&gt;&lt;em&gt;BGPLite — an open-source BGP route server built with C# and .NET 10 in roughly 2,500 lines of code. It can accept BGP sessions, dynamically load prefixes through RIPE Stat, and be managed via an HTTP API. The source code is available on &lt;a href="https://github.com/ruhex/BGPLite" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When I first opened &lt;a href="https://datatracker.ietf.org/doc/html/rfc4271" rel="noopener noreferrer"&gt;RFC 4271&lt;/a&gt;, BGP seemed like some dark magic from the carrier world. A few weeks later, I was debugging my own BGP server, accepting real peerings, and arguing with MikroTik about 4-byte ASNs.&lt;/p&gt;

&lt;p&gt;In this article, I'll walk through how BGPLite came to be, what pitfalls I hit along the way, and why writing your own BGP server turned out to be much simpler than it first appeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Why I Built This&lt;/li&gt;
&lt;li&gt;Why Not BIRD, FRR, or GoBGP&lt;/li&gt;
&lt;li&gt;Why .NET&lt;/li&gt;
&lt;li&gt;How BGP Works (Briefly)&lt;/li&gt;
&lt;li&gt;BGPLite Architecture&lt;/li&gt;
&lt;li&gt;
The Most Interesting Technical Challenges

&lt;ul&gt;
&lt;li&gt;TCP Fragmentation: Why the First Prototype Broke in a Real Network&lt;/li&gt;
&lt;li&gt;ASN32: Two Days of Debugging&lt;/li&gt;
&lt;li&gt;Capability Negotiation: Why Mikrotik Kept Tearing Down the Session&lt;/li&gt;
&lt;li&gt;Socket Write Race Condition: The First Production Bug&lt;/li&gt;
&lt;li&gt;UPDATE Batching: When 8,000 Prefixes Don't Fit in One Packet&lt;/li&gt;
&lt;li&gt;RIPE Stat Caching: 50 Peers → 1 Request&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;It's Already Running in Production&lt;/li&gt;
&lt;li&gt;What's Not There Yet&lt;/li&gt;
&lt;li&gt;What's Next&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;Let me start with an honest admission: I wanted to understand BGP from the inside out.&lt;/p&gt;

&lt;p&gt;When you work with networks, BGP looks like something monstrous: RFCs spanning dozens of pages, supplementary specifications galore, complex configurations in BIRD, FRR, Cisco, and Juniper. You configure &lt;code&gt;neighbor X.X.X.X remote-as 65444&lt;/code&gt;, and everything works — but what exactly happens under the hood often remains a mystery.&lt;/p&gt;

&lt;p&gt;What messages do routers exchange? How does capability negotiation work? Why does a session sometimes fail to come up? What's behind all those OPEN, KEEPALIVE, and UPDATE packets?&lt;/p&gt;

&lt;p&gt;To figure this out not just from documentation but through hands-on practice, I decided to write my own BGP implementation.&lt;/p&gt;

&lt;p&gt;In parallel, I had a concrete practical task: I needed a route server that dynamically distributes prefixes to clients. Not a full BGP router, not a policy engine for millions of routes — just "connect via BGP, get your routes, get to work." Moreover, each client has a different set of routes: one wants Cloudflare + Google, another wants prefixes for a specific country, a third wants a custom selection. And all of this should be managed through an HTTP API, because adding a client via CLI in 2026 is a thing of the past.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not BIRD, FRR, or GoBGP
&lt;/h2&gt;

&lt;p&gt;I looked at the existing tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BIRD&lt;/strong&gt; — a wonderful daemon, but its config is a programming language unto itself. Want to add a client via API? Generate a config, feed it to &lt;code&gt;birdc configure&lt;/code&gt;, and pray the parser doesn't choke. Dynamic loading of prefixes from external sources? Not out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FRR&lt;/strong&gt; (formerly Quagga) — heavyweight, with a legacy architecture. Overkill for a simple task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GoBGP&lt;/strong&gt; — powerful but enormous. Figuring out the codebase is harder than writing your own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common problem: they're all &lt;em&gt;full BGP speakers&lt;/em&gt;. They can do everything (policy routing, route reflection, MPLS, EVPN), but for my task, 90% of that is unnecessary.&lt;/p&gt;

&lt;p&gt;And then I thought: what if I wrote my own? First, it's the best way to understand a protocol — implement it from scratch. Second, .NET 10 with its &lt;code&gt;Span&amp;lt;byte&amp;gt;&lt;/code&gt;, async/await, and EF Core is an excellent platform for such a project. Third, ~2,500 lines of code seemed like a feasible weekend project.&lt;/p&gt;

&lt;p&gt;Spoiler: it took more than one weekend. But the protocol really did turn out to be simpler than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why .NET
&lt;/h2&gt;

&lt;p&gt;Spoiler: not because it's "trendy." The decision was driven by several factors:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;Span&amp;lt;byte&amp;gt;&lt;/code&gt; and &lt;code&gt;Memory&amp;lt;byte&amp;gt;&lt;/code&gt;.&lt;/strong&gt; BGP is a binary protocol. Parsing packets on &lt;code&gt;ReadOnlySpan&amp;lt;byte&amp;gt;&lt;/code&gt; without intermediate allocations is exactly what you need for networking code. In Go, you'd wrestle with &lt;code&gt;io.Reader&lt;/code&gt; and copies. In Rust, you'd fight the borrow checker (though it would be more idiomatic there). In .NET, it feels natural.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async I/O out of the box.&lt;/strong&gt; Each BGP session is two parallel coroutines (reading and keepalive). &lt;code&gt;async/await&lt;/code&gt; + &lt;code&gt;CancellationToken&lt;/code&gt; is a standard pattern — no manual callbacks or state machines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EF Core for storage.&lt;/strong&gt; Peers, subscriptions, custom prefixes — a classic relational model. &lt;code&gt;DbContextFactory&lt;/code&gt; + SQLite is three lines of configuration, and you have a thread-safe data store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And honestly — I'm just a C# developer.&lt;/strong&gt; I could have written it in Go, Rust, or C. But why, when you know your tool and can do the same thing with it? The best language for a project is the one you know.&lt;/p&gt;




&lt;h2&gt;
  
  
  How BGP Works (Briefly)
&lt;/h2&gt;

&lt;p&gt;If you've never looked under the hood of BGP — here's everything you need to know to follow this article.&lt;/p&gt;

&lt;p&gt;BGP has exactly &lt;strong&gt;4 message types&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OPEN&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Handshake: ASN, Hold Time, capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Announce/withdraw prefixes with attributes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOTIFICATION&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Error message; session closes after this&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KEEPALIVE&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;"I'm alive," confirms OPEN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each message starts with a 19-byte header: 16 bytes of marker (&lt;code&gt;0xFF&lt;/code&gt;), 2 bytes of length, 1 byte of type. The format is strictly binary — no text fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session establishment&lt;/strong&gt; is a finite state machine (FSM). For a route server scenario, a minimal set of states suffices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Idle → Connect → OpenSent → OpenConfirm → Established
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Established, the session lives: both sides exchange UPDATE (routes) and KEEPALIVE (liveness confirmation). If the HOLD Timer expires or a NOTIFICATION arrives — the session is torn down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UPDATE&lt;/strong&gt; is the most complex message. It contains three sections:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Withdrawn Routes — prefixes the peer is withdrawing&lt;/li&gt;
&lt;li&gt;Path Attributes — route metadata (Origin, AS_PATH, Next Hop, Communities)&lt;/li&gt;
&lt;li&gt;NLRI (Network Layer Reachability Information) — announced prefixes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prefixes are encoded compactly: only significant bytes. &lt;code&gt;192.168.0.0/16&lt;/code&gt; is 3 bytes, not 5. &lt;code&gt;/24&lt;/code&gt; is 4 bytes. &lt;code&gt;/0&lt;/code&gt; is 1 byte (length only).&lt;/p&gt;

&lt;p&gt;Everything else (capabilities, communities, MP-BGP) is optional extensions on top of this foundation. Basic BGP is remarkably simple.&lt;/p&gt;




&lt;h2&gt;
  
  
  BGPLite Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  Client (BIRD/Cisco/Mikrotik)
         │
         │ BGP (TCP :179)
         ▼
┌─────────────────┐    ┌──────────────────┐
│   BGP Server    │    │    HTTP API      │
│  (BgpSession)   │    │  (:5000)         │
│  (BgpMetrics)   │    │                  │
└────────┬────────┘    └────────┬─────────┘
         │                      │
         ▼                      ▼
┌─────────────────────────────────────────┐
│              Peer Store                 │
│         (EF Core + SQLite)              │
│  Peer → Subscription → CustomPrefix     │
│  Peer → Community                       │
└────────────────┬────────────────────────┘
                 │
         ┌───────┴───────┐
         ▼               ▼
┌────────────────┐ ┌──────────────┐
│  Route Table   │ │  Prefix      │
│  (Community    │ │  Service     │
│   Filters)     │ │  (Cache)     │
└────────────────┘ └──────┬───────┘
                          │
                   ┌──────┴───────┐
                   ▼              ▼
            ┌────────────┐ ┌───────────┐
            │ RIPE Stat  │ │ nets.txt  │
            │ API        │ │ (local)   │
            └────────────┘ └───────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project is split into 7 domain modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BGPLite/               # Entry point, DI, host setup
BGPLite.Protocol/      # BGP message encoding/decoding (zero dependencies)
BGPLite.Server/        # TCP listener, BGP session FSM
BGPLite.Routing/       # Route table, community filters
BGPLite.Providers/     # RIPE Stat client, cache, local prefixes
BGPLite.Api/           # HTTP API, EF Core, PeerStore
BGPLite.Configuration/ # YAML config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Protocol&lt;/code&gt; knows nothing about networking. &lt;code&gt;Server&lt;/code&gt; knows nothing about the database. &lt;code&gt;Routing&lt;/code&gt; knows nothing about HTTP. A clean modular monolith without over-engineering.&lt;/p&gt;

&lt;p&gt;The peer data model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Peer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Ip&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Asn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="s"&gt;"inactive"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;LastSessionAt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PeerCommunity&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Communities&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PeerSubscription&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Subscriptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PeerCustomPrefix&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CustomPrefixes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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="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;When a peer connects via BGP, the server looks it up in this table by IP address and determines which routes to send. A peer has three types of relationships: community filters (which routes to receive), AS-list subscriptions (where to source them from), and custom prefixes (additional ones).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Most Interesting Technical Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  TCP Fragmentation: Why the First Prototype Broke in a Real Network
&lt;/h3&gt;

&lt;p&gt;I wrote the first prototype in one evening. Open a TCP socket, read data via &lt;code&gt;NetworkStream.ReadAsync()&lt;/code&gt;, parse a BGP message. With BIRD on localhost — everything worked perfectly.&lt;/p&gt;

&lt;p&gt;Then I connected a real router over the network. And everything broke.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReadAsync&lt;/code&gt; doesn't guarantee that you'll read exactly one BGP packet. TCP is a stream protocol. A single &lt;code&gt;ReadAsync&lt;/code&gt; call can return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Half a message (the TCP segment didn't arrive in full)&lt;/li&gt;
&lt;li&gt;One and a half messages (two packets got glued together in the buffer)&lt;/li&gt;
&lt;li&gt;0 bytes (connection closed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My naïve code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DON'T DO THIS&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BgpMessageReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This worked on localhost because packets almost never get fragmented there. But over a real network — random parser crashes with cryptic "Invalid BGP marker" or "Message too short" errors.&lt;/p&gt;

&lt;p&gt;I had to write &lt;code&gt;ReadExactAsync&lt;/code&gt;, which guarantees reading an exact number of bytes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ReadExactAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Memory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;totalRead&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totalRead&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;totalRead&lt;/span&gt;&lt;span class="p"&gt;..],&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Connection closed by peer"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;totalRead&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;read&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;And read in two passes: first the 19-byte header (to learn the full length), then the remainder. This is also where &lt;code&gt;ArrayPool&amp;lt;byte&amp;gt;&lt;/code&gt; came in handy — instead of &lt;code&gt;new byte[4096]&lt;/code&gt; for every message, we rent a buffer from the pool. With dozens of concurrent sessions, the difference in GC pressure is noticeable.&lt;/p&gt;

&lt;h3&gt;
  
  
  ASN32: Two Days of Debugging
&lt;/h3&gt;

&lt;p&gt;I thought an ASN was just a 16-bit number in the OPEN packet. Initial tests with BIRD (ASN 65444) passed. Then I tried connecting a client with ASN 397143 — the session wouldn't establish.&lt;/p&gt;

&lt;p&gt;I spent two days looking for the problem. The parsing was correct, the bytes were right, but the peer was receiving some other ASN. It turned out that the ASN field in the OPEN packet is 16 bits, while the modern internet uses 4-byte ASNs (AS &amp;gt; 65535). Values &amp;gt; 65535 simply don't fit.&lt;/p&gt;

&lt;p&gt;The solution is described in &lt;a href="https://datatracker.ietf.org/doc/html/rfc4893" rel="noopener noreferrer"&gt;RFC 4893&lt;/a&gt;: a capability with code 65 (Four-Octet ASN), while the 16-bit field gets &lt;code&gt;AS_TRANS&lt;/code&gt; (23456) — a reserved value meaning "look in the capabilities":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;asn16&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_bgpConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Asn&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;ushort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MaxValue&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ushort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="m"&gt;23456&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ushort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;_bgpConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Asn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When reading, you do the reverse: first check capabilities, and only if capability 65 is absent — take the 16-bit field. This is a classic backward-compatible pattern from the RFC.&lt;/p&gt;

&lt;p&gt;But the story didn't end there. I implemented sending the 4-byte ASN in my OPEN, but forgot to &lt;em&gt;parse&lt;/em&gt; it when reading the peer's OPEN. A client with ASN 397143 would connect, and the server saw ASN 23456. The result: another half a day of debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capability Negotiation: Why Mikrotik Kept Tearing Down the Session
&lt;/h3&gt;

&lt;p&gt;When you have only one type of client — BIRD on Linux — everything works. But in the real world, different devices connect to a route server: BIRD, FRR, Mikrotik RouterOS, Cisco IOS, Juniper JunOS. And each has its own BGP "dialect."&lt;/p&gt;

&lt;p&gt;The first problem surfaced with Mikrotik. The session wouldn't come up — Mikrotik was sending a NOTIFICATION error right after my OPEN. A packet dump showed: Mikrotik wasn't sending the MP-BGP (Multiprotocol Extensions) capability, while my server was replying with MP-BGP IPv4/Unicast. Mikrotik didn't expect that and tore down the session.&lt;/p&gt;

&lt;p&gt;I had to implement capability adaptation: our OPEN contains only the capabilities the peer supports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;capabilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BgpCapabilityInfo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BgpCapabilityInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FourOctetAsn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_bgpConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Asn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Only if the peer supports MP IPv4/Unicast&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;PeerHasMpIpv4Unicast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteOpen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Capabilities&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BgpCapabilityInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MultiprotocolIpv4Unicast&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Only if the peer supports Route Refresh&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remoteOpen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Capabilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;BgpConstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Capability&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RouteRefresh&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BgpCapabilityInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RouteRefresh&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The takeaway: capability negotiation isn't an optional feature — it's a critically important part of BGP. Without it, real-world devices will refuse to establish a session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Socket Write Race Condition: The First Production Bug
&lt;/h3&gt;

&lt;p&gt;Each BGP session has two concurrent tasks: &lt;code&gt;ReadLoopAsync&lt;/code&gt; (reading) and &lt;code&gt;KeepAliveLoopAsync&lt;/code&gt; (sending keepalives). When I added &lt;code&gt;RefreshRoutesAsync&lt;/code&gt; — a method for updating routes when subscriptions change via the API — two socket writes started executing simultaneously.&lt;/p&gt;

&lt;p&gt;Two &lt;code&gt;WriteAsync&lt;/code&gt; calls on one &lt;code&gt;NetworkStream&lt;/code&gt; at the same time. The TCP buffer contains fragments of two BGP messages glued together. The peer receives garbage and closes the session.&lt;/p&gt;

&lt;p&gt;BGP is not a framing protocol. There are no delimiters between messages. The length is determined from the header, and if the bytes of two messages get interleaved — the peer can't parse either one.&lt;/p&gt;

&lt;p&gt;The solution — &lt;code&gt;SemaphoreSlim(1, 1)&lt;/code&gt; to serialize all writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SemaphoreSlim&lt;/span&gt; &lt;span class="n"&gt;_sendLock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;RefreshRoutesAsync&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="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;IsEstablished&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_sendLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;WithdrawAllAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SendAllRoutesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_sendLock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Release&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;Why not &lt;code&gt;lock&lt;/code&gt;? Because &lt;code&gt;lock&lt;/code&gt; doesn't work with &lt;code&gt;async&lt;/code&gt;. Why not &lt;code&gt;Monitor.Enter&lt;/code&gt;? Same reason. &lt;code&gt;SemaphoreSlim&lt;/code&gt; is the only primitive in .NET that works correctly with async/await.&lt;/p&gt;

&lt;h3&gt;
  
  
  UPDATE Batching: When 8,000 Prefixes Don't Fit in One Packet
&lt;/h3&gt;

&lt;p&gt;When the first client subscribed to a large prefix list, the server tried to send ~8,000 prefixes in a single UPDATE message. The peer silently ignored this message — and the client didn't receive a single route.&lt;/p&gt;

&lt;p&gt;The maximum BGP message size is 4,096 bytes (&lt;a href="https://datatracker.ietf.org/doc/html/rfc4271#section-4.1" rel="noopener noreferrer"&gt;RFC 4271, §4.1&lt;/a&gt;). One UPDATE fits ~120–130 prefixes, depending on the size of path attributes.&lt;/p&gt;

&lt;p&gt;The solution — batching by 100:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxNlriPerUpdate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;maxNlriPerUpdate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SendRouteBatchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nextHop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&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;Each batch is a separate UPDATE with a full set of path attributes (Origin, AS_PATH, Next Hop). This is redundant (we repeat attributes), but guarantees that each message is valid on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  RIPE Stat Caching: 50 Peers → 1 Request
&lt;/h3&gt;

&lt;p&gt;RIPE NCC provides a free API that returns the prefixes announced by a specific ASN:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://stat.ripe.net/data/ris-prefixes/data.json?resource=AS13335&amp;amp;list_prefixes=true
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem: RIPE has rate limits, and large ASes (Cloudflare, Google) have hundreds or thousands of prefixes. If 50 peers are subscribed to Cloudflare, you can't make 50 requests to RIPE Stat.&lt;/p&gt;

&lt;p&gt;The solution — a 1-hour cache in a &lt;code&gt;ConcurrentDictionary&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IReadOnlyList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;(&lt;/span&gt;&lt;span class="kt"&gt;uint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;CachedAt&lt;/span&gt;&lt;span class="p"&gt;)&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;_cacheTtl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&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;The first request to AS13335 goes to RIPE Stat, the next 49 are served from cache. After an hour — it refreshes on the next access.&lt;/p&gt;

&lt;p&gt;For local prefixes (e.g., a specific country's networks), a &lt;code&gt;nets.txt&lt;/code&gt; file is used — loaded once at startup, with no external requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  It's Already Running in Production
&lt;/h2&gt;

&lt;p&gt;The project is already deployed on a real server and serving several BGP peers: &lt;strong&gt;&lt;a href="https://bgp.vhex.dev/" rel="noopener noreferrer"&gt;bgp.vhex.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP API
&lt;/h3&gt;

&lt;p&gt;The API is built on &lt;code&gt;HttpListener&lt;/code&gt; — without ASP.NET Core. Why pull in Kestrel when you only need routing for 10 endpoints?&lt;/p&gt;

&lt;p&gt;A notable feature of &lt;code&gt;GET /api/server&lt;/code&gt; — it generates ready-made configs for BIRD, Cisco IOS, and Mikrotik RouterOS with pre-filled ASN and Router ID. The client just needs to copy-paste and insert their own IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bird"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"protocol bgp bgplite {"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"  local as &amp;lt;YOUR_ASN&amp;gt;;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"  neighbor 10.0.0.1 as 65444;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"  multihop;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"  hold time 180;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"  ipv4 {"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"    import filter bgplite_in;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"    export none;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"  };"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"}"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source code: &lt;a href="https://github.com/ruhex/BGPLite" rel="noopener noreferrer"&gt;github.com/ruhex/BGPLite&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Not There Yet
&lt;/h2&gt;

&lt;p&gt;I'll be honest — BGPLite doesn't claim to replace BIRD or FRR. Here's what's not implemented yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IPv6 / MP-BGP&lt;/strong&gt; — IPv4 unicast only&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RPKI validation&lt;/strong&gt; — prefixes aren't checked for validity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BGP MD5 Authentication&lt;/strong&gt; — no session spoofing protection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route Refresh&lt;/strong&gt; — updating prefixes requires withdrawing and resending all routes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graceful Restart&lt;/strong&gt; — when the server goes down, peers lose all routes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BFD&lt;/strong&gt; — no fast link-failure detection (only BGP keepalive)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BMP&lt;/strong&gt; — no BGP session monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full Table&lt;/strong&gt; — not designed to receive full internet routing tables (~900K prefixes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are deliberate limitations. BGPLite solves a specific problem — personal prefix distribution via subscriptions. For full BGP routing, use BIRD or FRR. That said, some items from this list will appear in future versions — depending on community interest.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;BGPLite will continue to evolve if the community finds the project interesting. Priorities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Route Refresh (soft reconfig without session teardown)&lt;/li&gt;
&lt;li&gt;RPKI prefix validation&lt;/li&gt;
&lt;li&gt;MP-BGP for IPv6&lt;/li&gt;
&lt;li&gt;Extracting &lt;code&gt;BGPLite.Protocol&lt;/code&gt; into a standalone NuGet package — so the BGP protocol implementation can be reused in other projects without tying them to the route server&lt;/li&gt;
&lt;li&gt;More data sources: alongside RIPE Stat, add Hurricane Electric (HE) BGP Toolkit and others&lt;/li&gt;
&lt;li&gt;Exclusion rules for prefixes — the ability to specify prefixes you don't want to receive, even if they're in your subscriptions&lt;/li&gt;
&lt;li&gt;Prometheus/Grafana metrics&lt;/li&gt;
&lt;li&gt;Web UI for peer management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nowadays, the advancement of networking technologies is more important than ever. CDN, edge computing, multi-cloud, geographically distributed systems — all of these require an understanding of routing. BGP is not vendor magic — it's an understandable protocol that you can implement over a weekend.&lt;/p&gt;

&lt;p&gt;If you find this topic interesting — come join in: &lt;a href="https://github.com/ruhex/BGPLite" rel="noopener noreferrer"&gt;github.com/ruhex/BGPLite&lt;/a&gt;. Stars, issues, and pull requests are all welcome.&lt;/p&gt;

</description>
      <category>bgp</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
