<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Tommi Niemi · Writing</title>
    <link>https://niemi.lol/blog</link>
    <atom:link href="https://niemi.lol/feed.xml" rel="self" type="application/rss+xml" />
    <description>Writing on networking, infrastructure, and systems by Tommi Niemi.</description>
    <language>en</language>
    <lastBuildDate>Sun, 21 Jun 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>your neural net as a function</title>
      <link>https://niemi.lol/blog/your-neural-net-as-a-function</link>
      <guid isPermaLink="true">https://niemi.lol/blog/your-neural-net-as-a-function</guid>
      <pubDate>Sun, 21 Jun 2026 00:00:00 GMT</pubDate>
      <description>marius eriksen's "your server as a function" is really a claim that a server is just three composable pieces. modgrad, a neural-net SDK in Rust, is built on all three on purpose, and they carry further than they have any right to.</description>
      <content:encoded><![CDATA[<p>marius eriksen&#39;s <a href="https://monkey.org/~marius/funsrv.pdf">&quot;your server as a function&quot;</a> (2013) is a short paper about keeping Twitter&#39;s serving stack alive under load. it is probably a cornerstone of why the site stayed up when elon took over and let most of the engineers go: the structure it describes takes very few people to keep running. but the durable idea in it is simpler than that. a server is just three composable pieces, and if you pick them well, the hard parts of the system stop fighting the parts you actually wanted to write.</p>
<p>the three pieces are futures, services, and filters. i will get to each. the reason i am writing this is that i spent the last several months building modgrad, a neural-net SDK in Rust, a domain with nothing obvious to do with RPC servers, and modgrad&#39;s core is eriksen&#39;s three abstractions, on purpose. we had the paper open. the surprise was never that we used it, it was how little we had to bend it: the mapping comes out almost line for line, down to a trait literally named <code>Filter</code> that composes with a method literally named <code>and_then</code>. this post is about how cleanly a 2013 server paper transfers to backprop, and where it stops.</p>
<p>the short version: a neural net is just a service, a filter, and a runtime. (the fourth piece, futures, doesn&#39;t map as cleanly. we&#39;ll deal with it last.)</p>
<h2>the paper, compressed</h2>
<p>eriksen&#39;s argument is that safe, modular, efficient server software falls out of three abstractions, and that almost everything else is plumbing.</p>
<p>a future is a handle on the result of an asynchronous operation: empty, succeeded, or failed. futures compose through combinators, and the composition is a dataflow graph that the runtime evaluates on your behalf. you describe the dependencies. you do not schedule them.</p>
<p>a service is an asynchronous function, and the definition is the entire idea:</p>
<pre><code class="language-scala">type Service[Req, Rep] = Req =&gt; Future[Rep]
</code></pre>
<p>the same type is both a client and a server. (sounds like a trick until you notice it means a proxy is just a server whose handler happens to be a client.)</p>
<p>a filter is a service that wraps a service:</p>
<pre><code class="language-scala">type Filter[Req, Rep] = (Req, Service[Req, Rep]) =&gt; Future[Rep]
</code></pre>
<p>everything application-agnostic (timeouts, retries, auth, logging) lives in a filter, and filters compose with <code>andThen</code>. Twitter&#39;s frontend proxy is, end to end, a filter stack. an excerpt of the real configuration:</p>
<pre><code class="language-text">recordHandletime andThen
traceRequest andThen
collectJvmStats andThen
parseRequest andThen
sanitize andThen
applyTrafficControl andThen
virtualHostServer
</code></pre>
<p>and then the sentence the whole paper rests on: &quot;operations describe what is computed; execution is handled separately.&quot; you write semantics. the runtime (Finagle) owns mechanism: threads, pools, retries, tracing, cancellation. the contract between the two is the only thing you have to get right, and you only have to get it right once.</p>
<p>ok, that&#39;s the paper. here are the same three again, in a deep learning library.</p>
<h2>a brain is a service</h2>
<p>everything in modgrad hangs off a single trait. trimmed down:</p>
<pre><code class="language-rust">trait Brain {
    type Weights;
    type State;
    type Input;
    type Cache;
    type Gradients;

    fn forward(
        weights: &amp;Self::Weights,
        state: Self::State,
        input: &amp;Self::Input,
    ) -&gt; (BrainOutput, Self::State);

    // same call, but keep the intermediates the backward pass will need
    fn forward_cached(
        weights: &amp;Self::Weights,
        state: Self::State,
        input: &amp;Self::Input,
    ) -&gt; (BrainOutput, Self::State, Self::Cache);

    fn backward(
        weights: &amp;Self::Weights,
        cache: Self::Cache,
        d_predictions: &amp;[Vec&lt;f32&gt;],
    ) -&gt; Self::Gradients;
}
</code></pre>
<p>the signature says most of it: a brain is a pure function from <code>(weights, state, input)</code> to <code>(output, new state)</code>. state is threaded through explicitly and never mutated behind your back. there is no module registry, no implicit graph, no global tape. (our own docs call this &quot;pure functions over magic,&quot; and the landing page says &quot;no framework, no YAML, just functions.&quot; i will not pretend those particular slogans were conscious nods to eriksen, but the structural decisions in the rest of this post were.)</p>
<p>put it beside the service type. <code>Req =&gt; Future[Rep]</code> is &quot;request in, response later.&quot; <code>(Weights, State, Input) =&gt; (Output, State)</code> is the same arrow with recurrent state carried across the gap, because a model that thinks over time has to hand something to its future self. a continuous thought machine is, with very little squinting, a service you call in a loop, where last tick&#39;s state is part of next tick&#39;s request.</p>
<p>the forward and backward halves are paired at the type level, the same way eriksen ties asynchrony to the <code>Future</code> type. modgrad has a <code>Vjp</code> trait (vector-Jacobian product) whose two methods share associated types:</p>
<pre><code class="language-rust">trait Vjp {
    type Primal;
    type Output;
    type Cache;
    type Cotangent;
    type Grad;

    fn forward(primal: &amp;Self::Primal) -&gt; (Self::Output, Self::Cache);
    fn backward(primal: &amp;Self::Primal, cache: &amp;Self::Cache, cotangent: &amp;Self::Cotangent) -&gt; Self::Grad;
}
</code></pre>
<p>you cannot write a forward without committing to the backward that undoes it, and the <code>Cache</code> type is the contract between them. a signature returning <code>Future[T]</code> tells you a call is async without your reading the body. a type implementing <code>Vjp</code> tells you it is differentiable, and exactly what it needs to remember for the backward pass. the type is the spec.</p>
<h2>filters</h2>
<p>modgrad has an inference path where a backbone emits logits and a stack of cognitive modules edits them before they become a token. an amygdala suppresses dangerous continuations. a hippocampus injects biases from recalled episodes. a cerebellum applies learned rules. working memory tracks recent context. i built each as an isolated unit, mostly because i wanted to test them without standing up the whole brain, and the trait i converged on lives in <code>modgrad-io</code>:</p>
<pre><code class="language-rust">trait Filter {
    fn apply(&amp;mut self, req: Request, mods: &amp;mut LogitMods, inner: &amp;mut dyn Service) -&gt; Response;
}

trait ServiceExt: Service + Sized {
    fn and_then&lt;F: Filter&gt;(self, filter: F) -&gt; Filtered&lt;F, Self&gt; {
        Filtered { filter, inner: self }
    }
}
</code></pre>
<p>that is eriksen&#39;s filter, almost unchanged. <code>(Req, Service) =&gt; Rep</code>, composed onto a service with <code>and_then</code> to produce a new service. and yes, the paper was open on the other monitor: the name and the <code>and_then</code> are borrowed on purpose. but a borrowed type only sticks if the problem actually has its shape, and this one did. i had a pile of modules that each wanted to see a request, do one thing, and pass it down. the paper handed me the abstraction. the problem is what made it fit.</p>
<p>here is the brain&#39;s actual pipeline, lifted from the doc comment over the type:</p>
<pre><code class="language-text">request
  → CTM deliberation    (4-region thinking loop)
  → AvoidanceFilter     (amygdala)
  → EpisodicFilter      (hippocampus)
  → RulesFilter         (cerebellum)
  → WorkingMemoryFilter (PFC)
  → InferenceService    (backbone)
</code></pre>
<p>now set it next to Twitter&#39;s proxy stack from two sections ago. one threads an HTTP request through logging and traffic control to a virtual host. the other threads a thought through fear, memory, and habit to a language model. same <code>andThen</code>, same orthogonal modules, same property that you can unit-test any single one of them with a fake inner service and about three lines of setup. eriksen, on filters: &quot;quite simple to unit test in isolation, without any set up, and with minimal mocking.&quot; that holds here: each of these cognitive filters tests on its own, against a fake inner service, with nothing else stood up.</p>
<h2>composition as data</h2>
<p>this is where modgrad leaves the paper. the way it leaves is, i suspect, the interesting part.</p>
<p>a server is a line. requests flow one direction and <code>andThen</code> is concatenation. a brain is not a line. it is a graph with feedback: the cortical loop runs input to attention to output to motor and then motor feeds back to input, the cerebellum and basal ganglia hang off the side, the hippocampus reads and writes from the middle. you cannot say that with a linear chain.</p>
<p>so modgrad does not compose regions in code. it composes them as data:</p>
<pre><code class="language-rust">struct RegionalConfig {
    regions: Vec&lt;CtmConfig&gt;,      // what
    connections: Vec&lt;Connection&gt;, // how
    // ...
}

struct Connection {
    from: Vec&lt;usize&gt;, // source regions; their states get summed at the target
    to: usize,        // target region
    receives_observation: bool,
    // ...
}
</code></pre>
<p>the eight-region brain is a <code>Vec&lt;Connection&gt;</code> plus a runtime that walks it each tick, summing signals where edges meet (they sum at the target, which is also how biological synapses work). there is even a learned variant: a small router that decides the wiring per tick instead of you fixing it once, up front.</p>
<input type="checkbox" id="lb-modgrad" class="lb-toggle" aria-hidden="true" />
<figure class="diagram">
<label for="lb-modgrad" class="lb-zoom" title="click to enlarge"><img src="https://niemi.lol/modgrad-brain.webp" width="1051" height="773" loading="lazy" decoding="async" alt="modgrad's eight-region brain rendered as a 3d particle cloud, with labeled regions attention, output, motor, insula, basal ganglia, hippocampus, and cerebellum connected by faint connectome edges, and a visual retina panel at the left." /></label>
<figcaption>the same eight-region brain, rendered live on the /play page. the bright clusters are the regions: attention, output, and motor at 32 neurons each form the cortical loop, with insula, basal ganglia, hippocampus, and cerebellum smaller off to the side. the faint lines are the connectome wiring them together, and the panel at the left is the visual retina reading the maze. click to enlarge.</figcaption>
</figure>
<label for="lb-modgrad" class="lb-backdrop" aria-hidden="true"><img src="https://niemi.lol/modgrad-brain.webp" loading="lazy" decoding="async" alt="" /></label>

<p>this is not really a departure from eriksen. it is taking him at his word: he separates what from how, and his what happens to be a chain. make the what a value instead of a chain and you can serialize it, diff it, mutate it, search it, or learn it. we do all five. the principle (describe the computation, hand execution to a runtime) does not care whether the description is a pipeline or a directed graph. it cares only that you wrote down structure instead of control flow.</p>
<h2>the runtime is the entire point</h2>
<p>the most important sentence in the paper is easy to read past. Finagle is separate from everything above it, and because the semantics never name an execution strategy, Twitter could add Dapper-style tracing, QoS, thread affinity, and end-to-end cancellation &quot;without changing APIs or otherwise modifying any existing user code.&quot; they bought themselves one place to put every hard thing, and amortized it across every service they would ever write.</p>
<p>modgrad has the same seam, and it is a large part of why a small team can carry a project this size. model code does not name hardware. a backward pass deep inside a continuous thought machine says:</p>
<pre><code class="language-rust">ops::matvec_t(d_out, &amp;linear.weight, d_input, out_dim, in_dim)?;
</code></pre>
<p>that is the whole call. no CUDA, no ROCm, no CPU anywhere in sight. underneath, ops are values in an <code>Op</code> enum (a small IR) and backends are plugins behind a trait:</p>
<pre><code class="language-rust">trait Backend: Send + Sync + &#39;static {
    fn name(&amp;self) -&gt; &amp;&#39;static str;
    fn supports(&amp;self, op: &amp;Op) -&gt; bool;
    fn dispatch(&amp;self, op: &amp;mut Op) -&gt; Result&lt;(), BackendError&gt;;
}
</code></pre>
<p>a registry probes the host and routes each op to the best target it has: a hand-written fast path for our GPU, then ROCm, then CUDA, then Vulkan, then CPU, which is registered last and handles everything the others cannot. set <code>MODGRAD_BACKEND=cpu</code> and the identical binary runs on a laptop with no GPU at all. we added full GPU residency (weights pinned on-device across steps) without editing a single <code>Brain</code> impl. that is the Finagle move: the hard work goes behind the seam once, and everything above it benefits from then on. a good seam is a complexity reset. below it live five backends, device memory, and kernel dispatch; above it none of that exists, and model code stays as simple as the trait it calls. complexity stops at the boundary instead of leaking upward through the codebase. <code>&lt;TODO: insert OOP joke here&gt;</code></p>
<p>it is also why a brain runs in a browser tab. compile to wasm, the registry finds exactly one backend (CPU), and dispatch goes there. nothing upstream notices. (this is literally how the demo on our site runs.)</p>
<h2>where the analogy breaks</h2>
<p>ok, the honest part. one leg of the analogy does not hold, and it is worth being specific about which.</p>
<p>eriksen&#39;s futures are asynchronous and read-only: producer and consumer are held apart, and you compose operations that have not happened yet. modgrad is eager. you call forward and it runs, right then, on this thread. there is no promise, no scheduler, nothing waiting on I/O.</p>
<p>what there is instead is the autodiff tape. the forward pass records a <code>Cache</code> per step (every tick of a CTM stashes its intermediates) and the backward pass walks that record in reverse. so there is a deferred computation here, the gradient, latent in the cache and executed separately, which rhymes with a future but is not one. if you wanted the real thing (concurrent, cancelable, &quot;this might fail in 50ms&quot;) you would reach for an async executor, and modgrad does not ship one, because a synchronous training step is compute-bound, with no I/O latency worth hiding work behind. two of the three abstractions map exactly; the third maps only in spirit.</p>
<h2>so, did we just copy a server framework</h2>
<p>a little, and that is the point rather than the embarrassment. we had a hard problem (compose neural modules you can reason about, run them on whatever silicon is in the room, and refuse to drown in framework magic), we knew eriksen had solved the structural half of it for a different domain, and we borrowed his answer instead of rediscovering it the slow way. the thing worth noticing is not that we reused a paper. it is that a paper about RPC servers had anything to say about backprop at all. that is what a good abstraction is: not tied to the domain it was invented for. services, filters, and the semantics-versus-runtime split are facts about composition, and composition does not care whether the parts are RPC handlers or brain regions.</p>
<p>nor is the trick specific to servers or brains. bend the three types a little and they fit most software. express and axum middleware are filters on a request service. unix pipes are filters between processes. a compiler is a service (source in, IR out) with passes as filters and codegen as the runtime seam. a shell, a message queue, a game loop: same skeleton. most systems reduce to a core function, the concerns you want kept out of that function, and an execution layer you want to swap without touching either. eriksen did not invent that decomposition, but he named the pieces and showed how little glue they need, and once you have the names you start seeing them everywhere.</p>
<p>the payoff is the one the paper promised, in a new domain. import one modgrad crate or fourteen; the compute crate has never heard of the brain crate. every module tests in isolation. the hardware target is an environment variable. and you can hand a stranger a URL where a real eight-region brain (the same functions we train on a GPU, not a reimplementation of them) runs in their browser, reads a maze through a visual retina, thinks for sixteen ticks, commits to a move, and learns its own motor readout while they watch. it is at <a href="https://modgrad.com/play">modgrad.com/play</a>: give it the 21×21 maze, flip it to hardcore, and watch where it still gets lost.</p>
<p>eriksen wrote his paper to keep a website up. thirteen years later we used it to build a brain. good abstractions travel.</p>
]]></content:encoded>
    </item>
    <item>
      <title>building an unnumbered bgp fabric</title>
      <link>https://niemi.lol/blog/unnumbered-bgp</link>
      <guid isPermaLink="true">https://niemi.lol/blog/unnumbered-bgp</guid>
      <pubDate>Tue, 16 Jun 2026 00:00:00 GMT</pubDate>
      <description>how we wired the AS64500 rack with interior BGP unnumbered. IPv6 link-local, RFC 5549, no /31s, and the places it broke.</description>
      <content:encoded><![CDATA[<p>most rack designs hand out a /31 (and a /127) on every internal link, write them into an IPAM spreadsheet, then configure a BGP neighbor by address. it works fine. it&#39;s also a lot of bookkeeping for links that only ever talk to the one box on the other end.</p>
<p>i recently got to write the design and configuration for a client&#39;s new rack (AS64500). most network work is migrating and iterating on someone&#39;s old setup. this one was a blank page, nothing to inherit, so i did the opposite and configured the interior fabric with BGP unnumbered. no addresses on the internal links. peers find each other over IPv6 link-local, IPv4 routes ride an IPv6 next-hop, and the numbered links are the external ones, where the other side isn&#39;t ours to configure.</p>
<p>this is the long version of how it&#39;s built, node by node, plus the two things that didn&#39;t work the way i expected.</p>
<figure class="diagram">
<svg id="my-svg" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1284.25px; background-color: transparent;" viewBox="0 -47 1284.25 1138" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#my-svg{font-size:22px;fill:#1d2125;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#my-svg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#my-svg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#my-svg .error-icon{fill:hsl(180, 0%, 100%);}#my-svg .error-text{fill:rgb(0, 0, 0);stroke:rgb(0, 0, 0);}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:#0f4c5c;stroke:#0f4c5c;}#my-svg .marker.cross{stroke:#0f4c5c;}#my-svg svg{font-size:22px;}#my-svg p{margin:0;}#my-svg .label{color:#1d2125;}#my-svg .cluster-label text{fill:rgb(0, 0, 0);}#my-svg .cluster-label span{color:rgb(0, 0, 0);}#my-svg .cluster-label span p{background-color:transparent;}#my-svg .label text,#my-svg span{fill:#1d2125;color:#1d2125;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#ffffff;stroke:#0f4c5c;stroke-width:1px;}#my-svg .rough-node .label text,#my-svg .node .label text,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-anchor:middle;}#my-svg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#my-svg .rough-node .label,#my-svg .node .label,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-align:center;}#my-svg .node.clickable{cursor:pointer;}#my-svg .root .anchor path{fill:#0f4c5c!important;stroke-width:0;stroke:#0f4c5c;}#my-svg .arrowheadPath{fill:#0b0b0b;}#my-svg .edgePath .path{stroke:#0f4c5c;stroke-width:1px;}#my-svg .flowchart-link{stroke:#0f4c5c;fill:none;}#my-svg .edgeLabel{background-color:hsl(-120, 0%, 100%);text-align:center;}#my-svg .edgeLabel p{background-color:hsl(-120, 0%, 100%);}#my-svg .edgeLabel rect{opacity:0.5;background-color:hsl(-120, 0%, 100%);fill:hsl(-120, 0%, 100%);}#my-svg .labelBkg{background-color:rgba(255, 255, 255, 0.5);}#my-svg .cluster rect{fill:#f6f8f9;stroke:#d3dadd;stroke-width:1px;}#my-svg .cluster text{fill:rgb(0, 0, 0);}#my-svg .cluster span{color:rgb(0, 0, 0);}#my-svg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-size:12px;background:hsl(180, 0%, 100%);border:1px solid hsl(180, 0%, 90%);border-radius:2px;pointer-events:none;z-index:100;}#my-svg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#1d2125;}#my-svg rect.text{fill:none;stroke-width:0;}#my-svg .icon-shape,#my-svg .image-shape{background-color:hsl(-120, 0%, 100%);text-align:center;}#my-svg .icon-shape p,#my-svg .image-shape p{background-color:hsl(-120, 0%, 100%);padding:2px;}#my-svg .icon-shape .label rect,#my-svg .image-shape .label rect{opacity:0.5;background-color:hsl(-120, 0%, 100%);fill:hsl(-120, 0%, 100%);}#my-svg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#my-svg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#my-svg .node .neo-node{stroke:#0f4c5c;}#my-svg [data-look="neo"].node rect,#my-svg [data-look="neo"].cluster rect,#my-svg [data-look="neo"].node polygon{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node path{stroke:url(#my-svg-gradient);stroke-width:1px;}#my-svg [data-look="neo"].node .outer-path{filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node .neo-line path{stroke:#0f4c5c;filter:none;}#my-svg [data-look="neo"].node circle{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].node circle .state-start{fill:#000000;}#my-svg [data-look="neo"].icon-shape .icon{fill:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg [data-look="neo"].icon-shape .icon-neo path{stroke:url(#my-svg-gradient);filter:drop-shadow( 1px 2px 2px rgba(185,185,185,1));}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="my-svg_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-pointEnd-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="11.5" refY="7" markerUnits="userSpaceOnUse" markerWidth="10.5" markerHeight="14" orient="auto"><path d="M 0 0 L 11.5 7 L 0 14 z" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-pointStart-margin" class="marker flowchart-v2" viewBox="0 0 11.5 14" refX="1" refY="7" markerUnits="userSpaceOnUse" markerWidth="11.5" markerHeight="14" orient="auto"><polygon points="0,7 11.5,14 11.5,0" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-circleEnd-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refY="5" refX="12.25" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-circleStart-margin" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-2" refY="5" markerUnits="userSpaceOnUse" markerWidth="14" markerHeight="14" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 0; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-crossEnd-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="17.7" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5;"/></marker><marker id="my-svg_flowchart-v2-crossStart-margin" class="marker cross flowchart-v2" viewBox="0 0 15 15" refX="-3.5" refY="7.5" markerUnits="userSpaceOnUse" markerWidth="12" markerHeight="12" orient="auto"><path d="M 1,1 L 14,14 M 1,14 L 14,1" class="arrowMarkerPath" style="stroke-width: 2.5; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"><g class="cluster" id="my-svg-K8S" data-look="classic"><rect style="" x="8" y="438" width="480" height="180"/><g class="cluster-label" transform="translate(101.5, 438)"><g><rect class="background" style="stroke: none"/><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em"><tspan font-style="normal" class="text-inner-tspan" font-weight="normal">kubernetes</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> ·</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> 2x</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> R640</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> /</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> cilium</tspan></tspan></text></g></g></g><g class="cluster" id="my-svg-CORE" data-look="classic"><rect style="" x="40.5" y="8" width="1043" height="360"/><g class="cluster-label" transform="translate(409.5, 8)"><g><rect class="background" style="stroke: none"/><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em"><tspan font-style="normal" class="text-inner-tspan" font-weight="normal">core</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> fabric</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> ·</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> 2x</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> Arista</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> 7060CX</tspan></tspan></text></g></g></g><g class="cluster" id="my-svg-EDGE" data-look="classic"><rect style="" x="763.5" y="438" width="432.75" height="395"/><g class="cluster-label" transform="translate(863.375, 438)"><g><rect class="background" style="stroke: none"/><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em"><tspan font-style="normal" class="text-inner-tspan" font-weight="normal">edge</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> ·</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> 2x</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> R640</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> /</tspan><tspan font-style="normal" class="text-inner-tspan" font-weight="normal"> VyOS</tspan></tspan></text></g></g></g><g class="cluster" id="my-svg-TRANSIT" data-look="classic"><rect style="" x="712" y="903" width="564.25" height="180"/><g class="cluster-label" transform="translate(963.125, 903)"><g><rect class="background" style="stroke: none"/><text y="-10.1" style=""><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em"><tspan font-style="normal" class="text-inner-tspan" font-weight="normal">transit</tspan></tspan></text></g></g></g></g><g class="edgePaths"><path d="M966,559.5L938.917,569.25C911.833,579,857.667,598.5,830.583,614.083C803.5,629.667,803.5,641.333,803.5,662.167C803.5,683,803.5,713,803.5,743C803.5,773,803.5,803,803.5,823.833C803.5,844.667,803.5,856.333,803.5,868C803.5,879.667,803.5,891.333,807.551,903C811.602,914.667,819.704,926.333,823.755,932.167L827.806,938" id="my-svg-L_vyos1_ua_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none;;;stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none" data-edge="true" data-et="edge" data-id="L_vyos1_ua_0" data-points="W3sieCI6OTY2LCJ5Ijo1NTkuNX0seyJ4Ijo4MDMuNSwieSI6NjE4fSx7IngiOjgwMy41LCJ5Ijo2NTN9LHsieCI6ODAzLjUsInkiOjc0M30seyJ4Ijo4MDMuNSwieSI6ODMzfSx7IngiOjgwMy41LCJ5Ijo4Njh9LHsieCI6ODAzLjUsInkiOjkwM30seyJ4Ijo4MjcuODA1NTU1NTU1NTU1NSwieSI6OTM4fV0=" data-look="classic"/><path d="M928.5,798L928.5,803.833C928.5,809.667,928.5,821.333,928.5,833C928.5,844.667,928.5,856.333,928.5,868C928.5,879.667,928.5,891.333,924.449,903C920.398,914.667,912.296,926.333,908.245,932.167L904.194,938" id="my-svg-L_vyos2_ua_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none;;;stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none" data-edge="true" data-et="edge" data-id="L_vyos2_ua_0" data-points="W3sieCI6OTI4LjUsInkiOjc5OH0seyJ4Ijo5MjguNSwieSI6ODMzfSx7IngiOjkyOC41LCJ5Ijo4Njh9LHsieCI6OTI4LjUsInkiOjkwM30seyJ4Ijo5MDQuMTk0NDQ0NDQ0NDQ0NSwieSI6OTM4fV0=" data-look="classic"/><path d="M1059.611,583L1060.259,588.833C1060.907,594.667,1062.204,606.333,1062.852,618C1063.5,629.667,1063.5,641.333,1063.5,662.167C1063.5,683,1063.5,713,1063.5,743C1063.5,773,1063.5,803,1063.5,823.833C1063.5,844.667,1063.5,856.333,1063.5,868C1063.5,879.667,1063.5,891.333,1068.053,903C1072.606,914.667,1081.713,926.333,1086.266,932.167L1090.819,938" id="my-svg-L_vyos1_ub_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none;;;stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none" data-edge="true" data-et="edge" data-id="L_vyos1_ub_0" data-points="W3sieCI6MTA1OS42MTExMTExMTExMTEsInkiOjU4M30seyJ4IjoxMDYzLjUsInkiOjYxOH0seyJ4IjoxMDYzLjUsInkiOjY1M30seyJ4IjoxMDYzLjUsInkiOjc0M30seyJ4IjoxMDYzLjUsInkiOjgzM30seyJ4IjoxMDYzLjUsInkiOjg2OH0seyJ4IjoxMDYzLjUsInkiOjkwM30seyJ4IjoxMDkwLjgxOTQ0NDQ0NDQ0NDMsInkiOjkzOH1d" data-look="classic"/><path d="M1016,774.786L1042.708,784.488C1069.417,794.191,1122.833,813.595,1149.542,829.131C1176.25,844.667,1176.25,856.333,1176.25,868C1176.25,879.667,1176.25,891.333,1173.495,903C1170.741,914.667,1165.231,926.333,1162.477,932.167L1159.722,938" id="my-svg-L_vyos2_ub_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none;;;stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none" data-edge="true" data-et="edge" data-id="L_vyos2_ub_0" data-points="W3sieCI6MTAxNiwieSI6Nzc0Ljc4NjA3NDY3MjA0ODV9LHsieCI6MTE3Ni4yNSwieSI6ODMzfSx7IngiOjExNzYuMjUsInkiOjg2OH0seyJ4IjoxMTc2LjI1LCJ5Ijo5MDN9LHsieCI6MTE1OS43MjIyMjIyMjIyMjIyLCJ5Ijo5Mzh9XQ==" data-look="classic"/><path d="M579.778,153L577.023,158.833C574.269,164.667,568.759,176.333,566.005,197.167C563.25,218,563.25,248,563.25,278C563.25,308,563.25,338,563.25,358.833C563.25,379.667,563.25,391.333,563.25,403C563.25,414.667,563.25,426.333,567.301,438C571.352,449.667,579.454,461.333,583.505,467.167L587.556,473" id="my-svg-L_sw1_cust_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none;;;stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none" data-edge="true" data-et="edge" data-id="L_sw1_cust_0" data-points="W3sieCI6NTc5Ljc3Nzc3Nzc3Nzc3NzgsInkiOjE1M30seyJ4Ijo1NjMuMjUsInkiOjE4OH0seyJ4Ijo1NjMuMjUsInkiOjI3OH0seyJ4Ijo1NjMuMjUsInkiOjM2OH0seyJ4Ijo1NjMuMjUsInkiOjQwM30seyJ4Ijo1NjMuMjUsInkiOjQzOH0seyJ4Ijo1ODcuNTU1NTU1NTU1NTU1NSwieSI6NDczfV0=" data-look="classic"/><path d="M688.25,333L688.25,338.833C688.25,344.667,688.25,356.333,688.25,368C688.25,379.667,688.25,391.333,688.25,403C688.25,414.667,688.25,426.333,684.199,438C680.148,449.667,672.046,461.333,667.995,467.167L663.944,473" id="my-svg-L_sw2_cust_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none;;;stroke:#9aa7ac;stroke-width:1.5px;stroke-dasharray:6 4;fill:none" data-edge="true" data-et="edge" data-id="L_sw2_cust_0" data-points="W3sieCI6Njg4LjI1LCJ5IjozMzN9LHsieCI6Njg4LjI1LCJ5IjozNjh9LHsieCI6Njg4LjI1LCJ5Ijo0MDN9LHsieCI6Njg4LjI1LCJ5Ijo0Mzh9LHsieCI6NjYzLjk0NDQ0NDQ0NDQ0NDUsInkiOjQ3M31d" data-look="classic"/><path d="M693.25,117.924L744.542,129.603C795.833,141.283,898.417,164.641,949.708,191.321C1001,218,1001,248,1001,278C1001,308,1001,338,1001,358.833C1001,379.667,1001,391.333,1001,403C1001,414.667,1001,426.333,1004.403,438C1007.806,449.667,1014.611,461.333,1018.014,467.167L1021.417,473" id="my-svg-L_sw1_vyos1_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#0f4c5c;stroke-width:2px;fill:none;;;stroke:#0f4c5c;stroke-width:2px;fill:none" data-edge="true" data-et="edge" data-id="L_sw1_vyos1_0" data-points="W3sieCI6NjkzLjI1LCJ5IjoxMTcuOTI0MDk4NjcxNzI2NzV9LHsieCI6MTAwMSwieSI6MTg4fSx7IngiOjEwMDEsInkiOjI3OH0seyJ4IjoxMDAxLCJ5IjozNjh9LHsieCI6MTAwMSwieSI6NDAzfSx7IngiOjEwMDEsInkiOjQzOH0seyJ4IjoxMDIxLjQxNjY2NjY2NjY2NjYsInkiOjQ3M31d" data-look="classic"/><path d="M693.25,128.259L722.042,138.216C750.833,148.173,808.417,168.086,837.208,193.043C866,218,866,248,866,278C866,308,866,338,866,358.833C866,379.667,866,391.333,866,403C866,414.667,866,426.333,866,447.167C866,468,866,498,866,528C866,558,866,588,866,608.833C866,629.667,866,641.333,870.051,653C874.102,664.667,882.204,676.333,886.255,682.167L890.306,688" id="my-svg-L_sw1_vyos2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#0f4c5c;stroke-width:2px;fill:none;;;stroke:#0f4c5c;stroke-width:2px;fill:none" data-edge="true" data-et="edge" data-id="L_sw1_vyos2_0" data-points="W3sieCI6NjkzLjI1LCJ5IjoxMjguMjU5MzY1OTk0MjM2M30seyJ4Ijo4NjYsInkiOjE4OH0seyJ4Ijo4NjYsInkiOjI3OH0seyJ4Ijo4NjYsInkiOjM2OH0seyJ4Ijo4NjYsInkiOjQwM30seyJ4Ijo4NjYsInkiOjQzOH0seyJ4Ijo4NjYsInkiOjUyOH0seyJ4Ijo4NjYsInkiOjYxOH0seyJ4Ijo4NjYsInkiOjY1M30seyJ4Ijo4OTAuMzA1NTU1NTU1NTU1NSwieSI6Njg4fV0=" data-look="classic"/><path d="M775.75,298.986L823.708,310.488C871.667,321.991,967.583,344.995,1015.542,362.331C1063.5,379.667,1063.5,391.333,1063.5,403C1063.5,414.667,1063.5,426.333,1062.852,438C1062.204,449.667,1060.907,461.333,1060.259,467.167L1059.611,473" id="my-svg-L_sw2_vyos1_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#0f4c5c;stroke-width:2px;fill:none;;;stroke:#0f4c5c;stroke-width:2px;fill:none" data-edge="true" data-et="edge" data-id="L_sw2_vyos1_0" data-points="W3sieCI6Nzc1Ljc1LCJ5IjoyOTguOTg2MDA5MzI3MTE1MjZ9LHsieCI6MTA2My41LCJ5IjozNjh9LHsieCI6MTA2My41LCJ5Ijo0MDN9LHsieCI6MTA2My41LCJ5Ijo0Mzh9LHsieCI6MTA1OS42MTExMTExMTExMTEsInkiOjQ3M31d" data-look="classic"/><path d="M775.75,310.778L801.208,320.315C826.667,329.852,877.583,348.926,903.042,364.296C928.5,379.667,928.5,391.333,928.5,403C928.5,414.667,928.5,426.333,928.5,447.167C928.5,468,928.5,498,928.5,528C928.5,558,928.5,588,928.5,608.833C928.5,629.667,928.5,641.333,928.5,653C928.5,664.667,928.5,676.333,928.5,682.167L928.5,688" id="my-svg-L_sw2_vyos2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#0f4c5c;stroke-width:2px;fill:none;;;stroke:#0f4c5c;stroke-width:2px;fill:none" data-edge="true" data-et="edge" data-id="L_sw2_vyos2_0" data-points="W3sieCI6Nzc1Ljc1LCJ5IjozMTAuNzc4MzU1ODc5MjkyNH0seyJ4Ijo5MjguNSwieSI6MzY4fSx7IngiOjkyOC41LCJ5Ijo0MDN9LHsieCI6OTI4LjUsInkiOjQzOH0seyJ4Ijo5MjguNSwieSI6NTI4fSx7IngiOjkyOC41LCJ5Ijo2MTh9LHsieCI6OTI4LjUsInkiOjY1M30seyJ4Ijo5MjguNSwieSI6Njg4fV0=" data-look="classic"/><path d="M656.167,153L661.514,158.833C666.861,164.667,677.556,176.333,682.903,188C688.25,199.667,688.25,211.333,688.25,217.167L688.25,223" id="my-svg-L_sw1_sw2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#0f4c5c;stroke-width:2px;fill:none;;;stroke:#0f4c5c;stroke-width:2px;fill:none" data-edge="true" data-et="edge" data-id="L_sw1_sw2_0" data-points="W3sieCI6NjU2LjE2NjY2NjY2NjY2NjYsInkiOjE1M30seyJ4Ijo2ODguMjUsInkiOjE4OH0seyJ4Ijo2ODguMjUsInkiOjIyM31d" data-look="classic"/><path d="M1021.417,583L1018.014,588.833C1014.611,594.667,1007.806,606.333,1004.403,618C1001,629.667,1001,641.333,996.301,653C991.602,664.667,982.204,676.333,977.505,682.167L972.806,688" id="my-svg-L_vyos1_vyos2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#0f4c5c;stroke-width:2px;fill:none;;;stroke:#0f4c5c;stroke-width:2px;fill:none" data-edge="true" data-et="edge" data-id="L_vyos1_vyos2_0" data-points="W3sieCI6MTAyMS40MTY2NjY2NjY2NjY2LCJ5Ijo1ODN9LHsieCI6MTAwMSwieSI6NjE4fSx7IngiOjEwMDEsInkiOjY1M30seyJ4Ijo5NzIuODA1NTU1NTU1NTU1NSwieSI6Njg4fV0=" data-look="classic"/><path d="M518.25,114.313L452.375,126.594C386.5,138.875,254.75,163.438,188.875,190.719C123,218,123,248,123,278C123,308,123,338,123,358.833C123,379.667,123,391.333,123,403C123,414.667,123,426.333,123.648,438C124.296,449.667,125.593,461.333,126.241,467.167L126.889,473" id="my-svg-L_sw1_cilium1_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#0f4c5c;stroke-width:2px;fill:none;;;stroke:#0f4c5c;stroke-width:2px;fill:none" data-edge="true" data-et="edge" data-id="L_sw1_cilium1_0" data-points="W3sieCI6NTE4LjI1LCJ5IjoxMTQuMzEyNzkxMjk5ODQ0NjN9LHsieCI6MTIzLCJ5IjoxODh9LHsieCI6MTIzLCJ5IjoyNzh9LHsieCI6MTIzLCJ5IjozNjh9LHsieCI6MTIzLCJ5Ijo0MDN9LHsieCI6MTIzLCJ5Ijo0Mzh9LHsieCI6MTI2Ljg4ODg4ODg4ODg4ODg5LCJ5Ijo0NzN9XQ==" data-look="classic"/><path d="M518.25,120.646L474.875,131.871C431.5,143.097,344.75,165.549,301.375,191.774C258,218,258,248,258,278C258,308,258,338,258,358.833C258,379.667,258,391.333,258,403C258,414.667,258,426.333,264.806,438C271.611,449.667,285.222,461.333,292.028,467.167L298.833,473" id="my-svg-L_sw1_cilium2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#0f4c5c;stroke-width:2px;fill:none;;;stroke:#0f4c5c;stroke-width:2px;fill:none" data-edge="true" data-et="edge" data-id="L_sw1_cilium2_0" data-points="W3sieCI6NTE4LjI1LCJ5IjoxMjAuNjQ1NTc4NzIwMzQ1MDd9LHsieCI6MjU4LCJ5IjoxODh9LHsieCI6MjU4LCJ5IjoyNzh9LHsieCI6MjU4LCJ5IjozNjh9LHsieCI6MjU4LCJ5Ijo0MDN9LHsieCI6MjU4LCJ5Ijo0Mzh9LHsieCI6Mjk4LjgzMzMzMzMzMzMzMzMsInkiOjQ3M31d" data-look="classic"/><path d="M600.75,295.49L540.292,307.575C479.833,319.66,358.917,343.83,298.458,361.748C238,379.667,238,391.333,238,403C238,414.667,238,426.333,231.194,438C224.389,449.667,210.778,461.333,203.972,467.167L197.167,473" id="my-svg-L_sw2_cilium1_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#0f4c5c;stroke-width:2px;fill:none;;;stroke:#0f4c5c;stroke-width:2px;fill:none" data-edge="true" data-et="edge" data-id="L_sw2_cilium1_0" data-points="W3sieCI6NjAwLjc1LCJ5IjoyOTUuNDkwMjgzMTc2MDEzMzV9LHsieCI6MjM4LCJ5IjozNjh9LHsieCI6MjM4LCJ5Ijo0MDN9LHsieCI6MjM4LCJ5Ijo0Mzh9LHsieCI6MTk3LjE2NjY2NjY2NjY2NjY5LCJ5Ijo0NzN9XQ==" data-look="classic"/><path d="M600.75,306.873L569.875,317.06C539,327.248,477.25,347.624,446.375,363.645C415.5,379.667,415.5,391.333,415.5,403C415.5,414.667,415.5,426.333,412.097,438C408.694,449.667,401.889,461.333,398.486,467.167L395.083,473" id="my-svg-L_sw2_cilium2_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style="stroke:#0f4c5c;stroke-width:2px;fill:none;;;stroke:#0f4c5c;stroke-width:2px;fill:none" data-edge="true" data-et="edge" data-id="L_sw2_cilium2_0" data-points="W3sieCI6NjAwLjc1LCJ5IjozMDYuODcyNTkzOTUwNTA0MX0seyJ4Ijo0MTUuNSwieSI6MzY4fSx7IngiOjQxNS41LCJ5Ijo0MDN9LHsieCI6NDE1LjUsInkiOjQzOH0seyJ4IjozOTUuMDgzMzMzMzMzMzMzMywieSI6NDczfV0=" data-look="classic"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_vyos1_ua_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_vyos2_ua_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_vyos1_ub_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_vyos2_ub_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw1_cust_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw2_cust_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw1_vyos1_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw1_vyos2_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw2_vyos1_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw2_vyos2_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw1_sw2_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_vyos1_vyos2_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw1_cilium1_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw1_cilium2_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw2_cilium1_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g><g class="edgeLabel"><g class="label" data-id="L_sw2_cilium2_0" transform="translate(0, 0)"><text y="-10.1" text-anchor="middle"><tspan class="text-outer-tspan row" x="0" y="-0.1em" dy="1.1em" text-anchor="middle"/></text></g></g><g><rect class="background" style="stroke: none"/></g></g><g class="nodes"><g class="node default" id="my-svg-flowchart-ua-0" data-look="classic" transform="translate(866, 993)"><rect class="basic label-container" style="" x="-105.5" y="-55" width="211" height="110"/><g class="label" style="" transform="translate(-61.5, -33)"><rect/><foreignObject width="123" height="66"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>upstream-a<br />AS65536</p></span></div></foreignObject></g></g><g class="node default" id="my-svg-flowchart-ub-1" data-look="classic" transform="translate(1133.75, 993)"><rect class="basic label-container" style="" x="-105" y="-55" width="210" height="110"/><g class="label" style="" transform="translate(-61, -33)"><rect/><foreignObject width="122" height="66"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>upstream-b<br />AS65537</p></span></div></foreignObject></g></g><g class="node default" id="my-svg-flowchart-vyos1-2" data-look="classic" transform="translate(1053.5, 528)"><rect class="basic label-container" style="" x="-87.5" y="-55" width="175" height="110"/><g class="label" style="" transform="translate(-43.5, -33)"><rect/><foreignObject width="87" height="66"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>vyos1<br />AS64500</p></span></div></foreignObject></g></g><g class="node default" id="my-svg-flowchart-vyos2-3" data-look="classic" transform="translate(928.5, 743)"><rect class="basic label-container" style="" x="-87.5" y="-55" width="175" height="110"/><g class="label" style="" transform="translate(-43.5, -33)"><rect/><foreignObject width="87" height="66"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>vyos2<br />AS64500</p></span></div></foreignObject></g></g><g class="node default" id="my-svg-flowchart-sw1-4" data-look="classic" transform="translate(605.75, 98)"><rect class="basic label-container" style="" x="-87.5" y="-55" width="175" height="110"/><g class="label" style="" transform="translate(-43.5, -33)"><rect/><foreignObject width="87" height="66"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>sw1<br />AS65001</p></span></div></foreignObject></g></g><g class="node default" id="my-svg-flowchart-sw2-5" data-look="classic" transform="translate(688.25, 278)"><rect class="basic label-container" style="" x="-87.5" y="-55" width="175" height="110"/><g class="label" style="" transform="translate(-43.5, -33)"><rect/><foreignObject width="87" height="66"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>sw2<br />AS65002</p></span></div></foreignObject></g></g><g class="node default" id="my-svg-flowchart-cilium1-6" data-look="classic" transform="translate(133, 528)"><rect class="basic label-container" style="" x="-87.5" y="-55" width="175" height="110"/><g class="label" style="" transform="translate(-43.5, -33)"><rect/><foreignObject width="87" height="66"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>cilium1<br />AS65301</p></span></div></foreignObject></g></g><g class="node default" id="my-svg-flowchart-cilium2-7" data-look="classic" transform="translate(363, 528)"><rect class="basic label-container" style="" x="-87.5" y="-55" width="175" height="110"/><g class="label" style="" transform="translate(-43.5, -33)"><rect/><foreignObject width="87" height="66"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>cilium2<br />AS65302</p></span></div></foreignObject></g></g><g class="node default" id="my-svg-flowchart-cust-8" data-look="classic" transform="translate(625.75, 528)"><rect class="basic label-container" style="" x="-93" y="-55" width="186" height="110"/><g class="label" style="" transform="translate(-49, -33)"><rect/><foreignObject width="98" height="66"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>customer<br />AS65538</p></span></div></foreignObject></g></g></g></g></g><defs><filter id="my-svg-drop-shadow" height="130%" width="130%"><feDropShadow dx="4" dy="4" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><defs><filter id="my-svg-drop-shadow-small" height="150%" width="150%"><feDropShadow dx="2" dy="2" stdDeviation="0" flood-opacity="0.06" flood-color="#000000"/></filter></defs><linearGradient id="my-svg-gradient" gradientUnits="objectBoundingBox" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" stop-color="#0f4c5c" stop-opacity="1"/><stop offset="100%" stop-color="hsl(-120, 0%, 90%)" stop-opacity="1"/></linearGradient><text text-anchor="middle" x="642.125" y="-25" class="flowchartTitleText">rack-mvp</text></svg>
<figcaption>rack-mvp, generated from the containerlab topology. solid teal links are unnumbered interior fabric (IPv6 link-local + RFC 5549), dashed grey links are the numbered external transit and customer sessions.</figcaption>
</figure>

<p>none of this went onto the hardware blind. i built the whole rack in containerlab first (<a href="https://github.com/rotkonetworks/unnumbered-bgp">full anonymized lab on github</a>), cEOS standing in for the arista switches and FRR for the vyos edges. in that fabric lab FRR also stands in for the cilium leaves, so i can iterate on switch and edge configs without booting kubernetes for every change. a second lab runs the real thing, Cilium on a kind cluster with its own BGP control-plane, where the server side gets validated against the same switches. that&#39;s the pre-production stage: every change goes through a lab before it touches a real port. the diagram above is generated from the fabric topology, and it&#39;s where the two bugs further down turned up.</p>
<input type="checkbox" id="lb-clab" class="lb-toggle" aria-hidden="true" />
<figure class="diagram">
<label for="lb-clab" class="lb-zoom" title="click to enlarge"><img src="https://niemi.lol/clab-topoviewer.png" width="1837" height="1066" loading="lazy" decoding="async" alt="containerlab topoViewer rendering of the rack: two arista switches, two vyos edges, two cilium leaves, routinators, and the transit path out to the upstreams." /></label>
<figcaption>the same rack in containerlab's topoViewer. this is the fabric lab, where FRR stands in for the cilium leaves so the topology boots without kubernetes, the open panel shows <code>cilium2</code> running the frr image with role <code>cilium-sim</code>. click to enlarge.</figcaption>
</figure>
<label for="lb-clab" class="lb-backdrop" aria-hidden="true"><img src="https://niemi.lol/clab-topoviewer.png" loading="lazy" decoding="async" alt="" /></label>

<h2>the rule</h2>
<p>everything follows from one decision. the interior is IPv6-only, and IPv4 only appears on numbered external links.</p>
<p>inside the AS, switch to switch, switch to edge, edge to edge, there are no IPv4 addresses and no /31s. the loopbacks are IPv6 (<code>fd00::1</code>, <code>fd00::2</code>, <code>fd00::11</code>, <code>fd00::12</code>). the router-ids are still dotted-quad (<code>10.0.0.1</code>, <code>10.0.0.11</code>, and so on) but that&#39;s a 32-bit identifier, not an address you can ping. IPv4 only shows up where it has to, on the transit and customer links, where the peer can&#39;t hand us an IPv6-only session.</p>
<p>the topology is two arista 7060cx switches (<code>sw1</code> AS65001, <code>sw2</code> AS65002), two vyos edges sharing one external face (<code>vyos1</code> and <code>vyos2</code>, both AS64500), and two cilium speakers (<code>cilium1</code> AS65301, <code>cilium2</code> AS65302). transit comes in from two upstreams, and a customer hangs off the switches. it&#39;s eBGP everywhere except the single iBGP session between the two edges: per-switch and per-server private ASNs, no route reflector.</p>
<p>the switches run arista EOS. the edges run vyos, which is FRR under the hood. the servers run Cilium, and in the fabric lab FRR stands in for them. so the configs below are EOS on the switches, FRR on the edges, and Cilium on the servers.</p>
<p>in the diagram, every solid teal link is unnumbered and every dashed grey link is numbered. the dashed ones are the links that leave the building.</p>
<h2>first, a little ipv6</h2>
<p>most people never learn IPv6 properly. they assume it&#39;s just IPv4 with hexadecimals. but the protocol has real depth, and this whole trick leans on one piece of it that IPv4 never had: link addressing without your help, no config required. it&#39;s worth two minutes at the packet level. if you want more than two minutes, APNIC&#39;s week-long onsite <a href="https://academy.apnic.net/en/course/ipv6-fundamentals">IPv6 fundamentals</a> course is where i actually learned this, and it was worth the week.</p>
<p>the moment an interface comes up it gives itself a link-local address in <code>fe80::/10</code>, with a stable, opaque interface id (<a href="https://www.rfc-editor.org/info/rfc7217">RFC 7217</a>). no DHCP, no assignment, and the address never leaves the link it&#39;s on.</p>
<p>before it uses that address it checks nobody else has it. that&#39;s duplicate address detection (DAD, <a href="https://www.rfc-editor.org/info/rfc4862">RFC 4862</a>): it sends one neighbor solicitation, an NS (ICMPv6 type 135), with the source set to <code>::</code>, the unspecified address, because it has no valid address yet, and the destination set to the solicited-node multicast of the tentative address, <code>ff02::1:ffXX:XXXX</code>, formed from the low 24 bits of the target. if some other node already holds that address it answers with a neighbor advertisement (NA, type 136) and the tentative address is abandoned. if nothing answers, it flips from tentative to usable. the interface id is 64 bits of effectively random, so a collision is vanishingly unlikely and DAD passes on the first NS.</p>
<p>then the routers make themselves known. a host can ask with a router solicitation (RS, type 133) sent to <code>ff02::2</code>, the all-routers group, and routers answer, and also advertise on their own schedule, with router advertisements (RA, type 134) to <code>ff02::1</code>, all-nodes. the RA is sourced from the router&#39;s own link-local and carries its presence, the on-link prefixes, the MTU. resolving an address to a MAC uses the same NS/NA pair aimed at the solicited-node multicast, the IPv6 replacement for ARP: a targeted multicast that only the likely owner hears, not a broadcast to the whole segment.</p>
<p>tldr: on any IPv6 link, both ends come up with a stable, unique, self-assigned address and a built-in way to find and reach each other, before a single line of config. that is the property the rest of this rides on.</p>
<h2>what unnumbered means here</h2>
<p>we lean on exactly that. an interior link gets no /31. FRR peers over the <code>fe80::</code> IPv6 already put on the interface, and the arista side just needs <code>ipv6 enable</code>. the only thing we switch on is RAs, so each end learns the other&#39;s link-local and knows who its neighbor is:</p>
<pre><code>interface eth1
 no ipv6 nd suppress-ra
 ipv6 nd ra-interval 5
</code></pre>
<p>without that, <code>neighbor eth1 interface</code> has nothing to discover and the session stays down.</p>
<p>the last piece is carrying IPv4 over that IPv6 next-hop. that&#39;s <a href="https://www.rfc-editor.org/info/rfc5549">RFC 5549</a> (updated by <a href="https://www.rfc-editor.org/info/rfc8950">8950</a>): an IPv4 prefix advertised with an IPv6 next-hop, so <code>192.0.2.0/24</code> crosses a link with no IPv4 address on it. arista has to be told to do this; FRR does it on its own. both below.</p>
<h2>the switch side</h2>
<p>arista EOS doesn&#39;t implement <code>remote-as external</code>. you give it the AS per neighbor and peer the interface into a peer-group:</p>
<pre><code>neighbor PG-INTERIOR peer group
neighbor PG-INTERIOR timers 10 30
neighbor PG-INTERIOR bfd
!
neighbor interface Et33 peer-group PG-INTERIOR remote-as 65002   ! peer-link to sw2
neighbor interface Et1  peer-group PG-INTERIOR remote-as 64500   ! vyos1
neighbor interface Et2  peer-group PG-INTERIOR remote-as 64500   ! vyos2
</code></pre>
<p>and the IPv4-over-IPv6 next-hop that FRR did for free has to be turned on by hand:</p>
<pre><code>address-family ipv4
   neighbor PG-INTERIOR activate
   neighbor PG-INTERIOR next-hop address-family ipv6 originate
</code></pre>
<p><code>next-hop address-family ipv6 originate</code> is the EOS knob for advertising IPv4 NLRI with an IPv6 next-hop. the interior interfaces themselves are just <code>no switchport</code>, <code>no ip address</code>, <code>ipv6 enable</code>, with RA on. the only interior neighbors missing here are the servers, which peer by address in a separate group, for reasons that come up below.</p>
<h2>the edge</h2>
<p>the vyos edges are where the two worlds meet. inside, the iBGP between the edges and the eBGP down to the switches is all unnumbered:</p>
<pre><code>! ibgp to vyos1 (unnumbered)
neighbor eth3 interface remote-as 64500
neighbor eth3 timers 10 30
neighbor eth3 bfd
!
! ebgp to switches (unnumbered)
neighbor eth1 interface remote-as external
neighbor eth1 timers 10 30
neighbor eth1 bfd
neighbor eth2 interface remote-as external
neighbor eth2 timers 10 30
neighbor eth2 bfd
</code></pre>
<p><code>remote-as 64500</code> on the iBGP peer is the one place we give an explicit AS instead of <code>external</code>. the two edges are the same AS on purpose: AS64500 is the single face we show the world, one origin for the prefixes, not two. that makes the link between them iBGP, the only iBGP in the network. everything else has its own private ASN, so it&#39;s eBGP. with just two edges that one session is already a full mesh, so there&#39;s no route reflector to run.</p>
<p>interior timers are tightened to <code>10 30</code> from FRR&#39;s default <code>60 180</code>, with BFD doing the sub-second work on top.</p>
<p>outside, the transit sessions stay numbered. real addresses on the upstream links, <code>local-role customer</code> (<a href="https://www.rfc-editor.org/info/rfc9234">RFC 9234</a>) and <code>ttl-security hops 1</code> (<a href="https://www.rfc-editor.org/info/rfc5082">GTSM</a>) per peer, <code>bgp ebgp-requires-policy</code> left on for <a href="https://www.rfc-editor.org/info/bcp194">BCP 194</a>. that&#39;s the normal ISP pattern and there&#39;s no reason to fight it. unnumbered is for links you own both ends of.</p>
<p>the result is less to manage, and it scales without planning. adding an interior link is <code>ip link set up</code>, <code>no ipv6 nd suppress-ra</code>, and one <code>neighbor ethN interface</code> line, the same on every box, so a new switch or edge is that recipe plus its ASN, nothing to look up. no subnet to allocate, no IPAM row, no /31 pool that grows with the fabric. that&#39;s why unnumbered is the default for large leaf-spine networks, where the interior links dwarf a two-switch rack and hand-numbering them would be a job in itself (<a href="https://www.rfc-editor.org/info/rfc7938">RFC 7938</a>).</p>
<h2>the server side</h2>
<p>the one interior link that doesn&#39;t get unnumbered is the server. the servers run Cilium, and Cilium&#39;s BGP control-plane only peers by explicit address, there&#39;s no <code>interface</code> peer the way FRR has one. so the cilium-to-switch links are the single interior exception: numbered, a <code>/31</code> and a <code>/127</code> per leg.</p>
<p>the speaker is declared in a <code>CiliumBGPClusterConfig</code>, one BGP instance per node with a peer entry per switch and family:</p>
<pre><code class="language-yaml">apiVersion: cilium.io/v2
kind: CiliumBGPClusterConfig
metadata:
  name: cilium1
spec:
  nodeSelector:
    matchLabels:
      server: &quot;1&quot;
  bgpInstances:
    - name: &quot;65301&quot;
      localASN: 65301
      peers:
        - name: sw1-v4
          peerASN: 65001
          peerAddress: 10.10.110.0
          peerConfigRef: { name: cilium-peer }
        - name: sw1-v6
          peerASN: 65001
          peerAddress: &quot;fd00:10:110::&quot;
          peerConfigRef: { name: cilium-peer }
        - name: sw2-v4
          peerASN: 65002
          peerAddress: 10.10.111.0
          peerConfigRef: { name: cilium-peer }
        - name: sw2-v6
          peerASN: 65002
          peerAddress: &quot;fd00:10:111::&quot;
          peerConfigRef: { name: cilium-peer }
</code></pre>
<p>the parts that matter:</p>
<ul>
<li>every peer is keyed by a <code>peerAddress</code>, so a node brings up four sessions, a v4 and a v6 to each switch. the unnumbered side of the fabric gets one interface session that carries both families; here it&#39;s a session per family per neighbor, with an address to allocate for each.</li>
<li>the matching switch side is plain numbered eBGP in its own <code>PG-CILIUM</code> group: <code>neighbor 10.10.110.1 remote-as 65301</code>, <code>neighbor fd00:10:110::1 remote-as 65301</code>, and so on. same peer-group and timers, just addresses instead of interfaces.</li>
<li>what gets advertised isn&#39;t a <code>network</code> statement, it&#39;s a Cilium object. a <code>CiliumLoadBalancerIPPool</code> hands out the service IPs and a <code>CiliumBGPAdvertisement</code> picks which to announce. the anycast LB address comes out of both cilium1 and cilium2 and gets ECMP&#39;d at the switches.</li>
</ul>
<p>this is the boundary between the routed fabric and kubernetes, and it&#39;s the one spot in the interior that looks like an ordinary numbered eBGP session.</p>
<h2>where it broke</h2>
<p>unnumbered works well until something downstream doesn&#39;t speak it. two things caught us.</p>
<p>BFD is fine on unnumbered, broken on numbered eBGP, at least on this EOS. on <code>ceos:4.36.0.1F</code>, BFD on the unnumbered link-local peers comes up reliably, <code>show bfd peers brief</code> shows <code>up</code>. but add <code>neighbor X bfd</code> to a numbered eBGP peer, the switch-to-customer link, and that session wedges in <code>Idle</code> with <code>Last reset: Address family activated (n/a)</code> and never establishes. the unnumbered peers on the same switch stay up. FRR-to-FRR numbered eBGP does BFD fine, so it&#39;s specific to numbered eBGP on EOS. we left the numbered customer peers without BFD and let them fall back to BGP holdtime plus GTSM. the link you&#39;d reach for BFD on first, the external one, is the one place it wouldn&#39;t go.</p>
<p>the second one is cilium, the place i wanted unnumbered most. the server links above are numbered because they have to be: cilium&#39;s BGP doesn&#39;t implement interface peering (<a href="https://github.com/cilium/cilium/issues/22132">cilium/cilium#22132</a>, open since 2022), and it doesn&#39;t negotiate BFD either (<a href="https://github.com/cilium/cilium/issues/22394">#22394</a>) in the OSS build. so failure detection on those links drops from BFD&#39;s sub-second to BGP holdtime, around 30 seconds. the FRR stand-ins in the fabric lab do speak unnumbered and BFD, so any demo that shows sub-second cilium failover is really showing the FRR stubs, not stock cilium.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
