<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.5">Jekyll</generator><link href="https://goyalankit.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://goyalankit.com/" rel="alternate" type="text/html" /><updated>2026-07-20T05:40:30+00:00</updated><id>https://goyalankit.com/feed.xml</id><title type="html">/home/ankit</title><subtitle>developer. curious. learner</subtitle><entry><title type="html">Change the model or change the prompt?</title><link href="https://goyalankit.com/blog/change-the-model-or-change-the-prompt" rel="alternate" type="text/html" title="Change the model or change the prompt?" /><published>2026-05-05T16:00:00+00:00</published><updated>2026-05-05T16:00:00+00:00</updated><id>https://goyalankit.com/blog/change-the-model-or-change-the-prompt</id><content type="html" xml:base="https://goyalankit.com/blog/change-the-model-or-change-the-prompt"><![CDATA[<p>I have been spending time lately on the different ways to make an LLM system better, so I’m documenting my understanding of the main techniques and where each one fits.</p>

<p>Say you have built something with an LLM. It works, but not well enough. The summarizer misses details that matter, or the agent keeps calling the wrong tool. What are our options?</p>

<p>Broadly, there are two. We can change the model itself, which is the fine-tuning family. Or we can change what we tell it, which is prompt engineering, though I’ll use the broader term text-based refinement here, since the interesting new methods go well beyond tweaking prompts by hand.</p>

<figure class="gepa-figure">
  <img src="/assets/img/gepa/llm-improvement-map.svg" alt="Two levers for improving an LLM system: changing model parameters or changing instructions" />
  <figcaption>Model training and text refinement change different parts of the system, but both depend on evaluation.</figcaption>
</figure>

<h2 id="changing-the-model-parametric-learning">Changing the model (parametric learning)</h2>

<p>An LLM’s knowledge lives in billions of numbers called parameters, or weights. Training means nudging those numbers, which is why this family of approaches is called <strong>parametric</strong> learning.</p>

<p>The first thought that comes to mind is <strong>supervised fine-tuning (SFT)</strong>. Collect thousands of examples of the behavior you want, each one an input paired with the ideal output, and adjust the weights until the model imitates them reliably. It is apprenticeship by imitation: watch the master, copy the master.</p>

<p>The other option is <strong>reinforcement learning (RL)</strong>, with methods like GRPO. Here we don’t show the model correct answers. We let it attempt the task, score each attempt, and shift the weights toward whatever scores well. Learning by trial, error and grades.</p>

<p>Both are powerful, but the costs are not as fixed as they sound. Full fine-tuning of a large model does need a big dataset and serious GPU time. Parameter-efficient techniques like LoRA change that math: they train small adapter layers on top of a frozen base model, which brings the data and compute requirements down dramatically (I wrote about <a href="https://goyalankit.com/blog/lora-adapters-one-base-many-skills">LoRA adapters</a> earlier). Many providers now offer fine-tuning as a hosted service too, so sitting behind an API no longer rules it out. RL still needs a reliable way to score attempts, and enough of them to learn from. The one thing that holds across the board is opacity: the improvement lands in the weights, billions of numbers that nobody can read.</p>

<h2 id="changing-the-instructions-text-based-refinement">Changing the instructions (text-based refinement)</h2>

<p>The alternative is to keep the model frozen and improve the text around it: the prompts, the rules, the playbook the model follows.</p>

<p>If parametric learning is sending an employee away for months of retraining, text-based refinement is editing the employee handbook. Same person, sharper instructions.</p>

<p>Everyone already does this by hand. Try a prompt, watch it fail, reword it, try again. The interesting development is that this loop can now be automated, and the best automated versions learn from something richer than a score.</p>

<h2 id="the-score-versus-the-explanation">The score versus the explanation</h2>

<p>Imagine you’re helping a new engineer get better at code reviews.</p>

<p>One approach is to score every review: 6 out of 10, 8 out of 10, 4 out of 10. After enough attempts, perhaps they slowly develop better instincts.</p>

<p>A more useful approach is to sit down with one review and say: “You caught the null check, but missed the race condition because you only followed the happy path. Next time, trace what happens when two requests update the same object.”</p>

<p>The score tells them how well they did. The explanation tells them how to improve.</p>

<p>Keep that distinction in mind. Parametric RL learns from scores. The most interesting text-based methods learn from explanations.</p>

<h2 id="llm-as-a-judge">LLM-as-a-judge</h2>

<p>Before anything can improve, something has to define what “better” means. An <strong>LLM-as-a-judge</strong> is simply an LLM used as an evaluator: given an output and a rubric, it returns a score, a critique, or both.</p>

<p>Note that a judge by itself improves nothing. It plays the role of a teacher grading an assignment, the red ink rather than the revision. The judge is also optional: feedback can come from unit tests, compiler errors, exact-match checks, or human comments.</p>

<figure class="gepa-figure">
  <img src="/assets/img/gepa/llm-feedback-loop.svg" alt="An LLM judge evaluates an output while TextGrad and GEPA use its feedback in different optimization loops" loading="lazy" decoding="async" />
  <figcaption>The judge produces the learning signal. TextGrad and GEPA decide how to act on it.</figcaption>
</figure>

<h2 id="textgrad">TextGrad</h2>

<p>Real LLM systems are rarely a single prompt. A research assistant might have one prompt that writes search queries, a retrieval step, and another prompt that composes the final answer. When the final answer is bad, which prompt is at fault?</p>

<p><strong>TextGrad</strong> is an optimizer inspired by backpropagation, the algorithm used to train neural networks. It records how text flows through the pipeline, then carries criticism backward through it. If a judge says the answer lacks supporting evidence, TextGrad translates that into one critique for the answer-writing prompt and a different critique for the query-writing prompt. An LLM then revises each piece of text accordingly.</p>

<p>These “textual gradients” are natural-language suggestions, not numerical derivatives. The spirit is the same though: figure out which upstream component to blame, and route the criticism to it.</p>

<h2 id="gepa">GEPA</h2>

<p><strong>GEPA</strong> (short for Genetic-Pareto) is a prompt optimizer that treats a failed attempt as a story worth reading, not just a number.</p>

<p>Say a retrieval agent gets a question wrong. A score of 0 tells us it failed. The execution trace, the record of what actually happened along the way, tells us much more:</p>

<ul>
  <li>The first search found the right person but the wrong event.</li>
  <li>The second search merely rephrased the first.</li>
  <li>The retriever surfaced a useful source, but the final answer ignored it.</li>
</ul>

<p>An LLM can read that trace and infer a general lesson: follow-up searches should use new entities discovered in earlier results instead of repeating the original query. GEPA writes that lesson back into the query-writing prompt and tries again.</p>

<p>The loop is roughly:</p>

<ol>
  <li>Run the current system on a few examples.</li>
  <li>Reflect on its traces and feedback.</li>
  <li>Rewrite one prompt in the system.</li>
  <li>Test the new version and keep it if it helps.</li>
</ol>

<p>This works for a single prompt, but it really shines in compound systems with several LLM calls, retrieval steps and tools, since GEPA can improve one module at a time instead of treating the whole pipeline as a black box.</p>

<p>The name describes the search strategy.</p>

<p><strong>Genetic</strong> refers to evolution. GEPA maintains a population of prompt candidates. Reflection mutates one candidate, the new version inherits the rest of its parent’s instructions, and a merge step can combine improvements found along different lineages.</p>

<p><strong>Pareto</strong> refers to selection. GEPA doesn’t only keep the candidate with the best average score. It also preserves candidates that are unusually good at something:</p>

<div class="gepa-table-wrap" tabindex="0" aria-label="Scrollable candidate score comparison">
  <table>
    <thead>
      <tr>
        <th>Candidate</th>
        <th>Billing example</th>
        <th>Login example</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">A</th>
        <td>9</td>
        <td>5</td>
      </tr>
      <tr>
        <th scope="row">B</th>
        <td>7</td>
        <td>8</td>
      </tr>
      <tr class="gepa-table-row--muted">
        <th scope="row">C</th>
        <td>6</td>
        <td>4</td>
      </tr>
    </tbody>
  </table>
</div>

<p>Candidate C can be discarded since A beats it on both examples. But A and B have different strengths, so GEPA keeps both alive. This prevents the optimizer from fixating on one early winner and getting stuck, and it lets useful specialist strategies survive long enough to contribute later.</p>

<figure class="gepa-figure">
  <img src="/assets/img/gepa/textgrad-vs-gepa.svg" alt="TextGrad routes criticism backward through one computation graph while GEPA explores multiple candidate lineages" loading="lazy" decoding="async" />
  <figcaption>TextGrad follows responsibility backward; GEPA searches forward across alternatives.</figcaption>
</figure>

<h2 id="putting-them-side-by-side">Putting them side by side</h2>

<div class="gepa-table-wrap" tabindex="0" aria-label="Scrollable optimization technique comparison">
  <table class="gepa-technique-table">
    <thead>
      <tr>
        <th>Technique</th>
        <th>What changes</th>
        <th>Learns from</th>
        <th>What you need</th>
      </tr>
    </thead>
    <tbody>
      <tr class="gepa-technique-row--model">
        <th scope="row">SFT</th>
        <td>Model weights</td>
        <td>Example demonstrations</td>
        <td>Labeled examples, plus GPUs or a hosted fine-tuning service</td>
      </tr>
      <tr class="gepa-technique-row--model">
        <th scope="row">RL (e.g. GRPO)</th>
        <td>Model weights</td>
        <td>Scores and rewards</td>
        <td>A reliable reward signal and many scored attempts</td>
      </tr>
      <tr class="gepa-technique-row--text">
        <th scope="row">Manual prompting</th>
        <td>Prompt text</td>
        <td>Human intuition</td>
        <td>Patience</td>
      </tr>
      <tr class="gepa-technique-row--judge">
        <th scope="row">LLM-as-a-judge</th>
        <td>Nothing, it evaluates</td>
        <td>A rubric</td>
        <td>A capable judge model</td>
      </tr>
      <tr class="gepa-technique-row--text">
        <th scope="row">TextGrad</th>
        <td>Prompt text</td>
        <td>Critiques routed backward through the pipeline</td>
        <td>A pipeline whose intermediate text you can inspect</td>
      </tr>
      <tr class="gepa-technique-row--text">
        <th scope="row">GEPA</th>
        <td>Prompt text</td>
        <td>Reflection on traces and feedback</td>
        <td>Rich traces and an evaluator</td>
      </tr>
    </tbody>
  </table>
</div>

<p>Note the split in the “what changes” column. SFT and RL change the model. Everything else changes the words.</p>

<h2 id="which-one-should-you-use">Which one should you use?</h2>

<p>In the GEPA paper’s experiments across four tasks and two model families, GEPA outperformed both a GRPO fine-tuning setup and MIPROv2, an earlier automatic prompt optimizer, while sometimes using far fewer attempts. The Pareto strategy also beat greedily evolving only the single best prompt.</p>

<p>I’d treat those results as promising rather than universal. GEPA adds extra reflection calls, leans heavily on the quality of evaluator feedback, and was tested on a limited set of systems. With abundant data, updating weights may still be the better choice.</p>

<p>The honest answer is that it depends on more variables than data volume. The ones I find myself weighing:</p>

<ol>
  <li><strong>Knowledge or behavior</strong>: If what’s missing is facts, especially facts that change often, retrieval (RAG) is usually the right tool and neither lever needs to move. Fine-tuning is better at teaching skills, style and format than at storing fresh information.</li>
  <li><strong>Rate of change</strong>: A fine-tuned model bakes in today’s behavior. If requirements or data shift every few weeks, retraining becomes a treadmill, while a prompt can be edited and shipped in an afternoon.</li>
  <li><strong>Auditability</strong>: An optimized prompt is a document. You can read it, diff it, and walk a reviewer through it. A weight update is a pile of numbers.</li>
  <li><strong>Economics</strong>: A long prompt is paid for on every single call, while fine-tuning moves that cost up front and makes each call cheaper. At high volume, the economics often decide ties.</li>
  <li><strong>Feedback richness</strong>: If failed attempts leave clues behind (compiler errors, missing documents, tool output, grading feedback), reflective methods like GEPA have material to work with. If all you get is a score, that advantage shrinks.</li>
</ol>

<p>And whichever way you lean, build the eval first. Every technique in this post is steered by feedback, so the quality of your evaluation sets the ceiling on your optimization. Evals before opinions.</p>

<h2 id="conclusion">Conclusion</h2>

<p>An execution trace isn’t just exhaust. It can be training data.</p>

<p>Parametric methods compress experience into weights you can’t read. Text-based methods compress it into instructions you can. You can open the optimized prompt, see the lesson the system learned, and decide whether you agree with it.</p>

<p>When an LLM can read its own attempts, understand the grader’s complaint, and rewrite the instruction that led it astray, prompt optimization starts to look less like wordsmithing and more like learning.</p>

<p>This is all for now, hopefully this was useful. I’d appreciate any feedback or comments.</p>

<h2 id="references">References</h2>

<ol>
  <li>Lakshya A. Agrawal et al., GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learning, 2025.</li>
  <li>DSPy GEPA documentation.</li>
  <li>Mert Yuksekgonul et al., Optimizing generative AI by backpropagating language model feedback, 2025.</li>
  <li>Zhihong Shao et al., DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models (introduces GRPO), 2024.</li>
  <li>Lianmin Zheng et al., Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena, 2023.</li>
</ol>]]></content><author><name></name></author><category term="posts" /><category term="ai" /><category term="llm" /><category term="prompt-optimization" /><category term="fine-tuning" /><category term="gepa" /><summary type="html"><![CDATA[I have been spending time lately on the different ways to make an LLM system better, so I’m documenting my understanding of the main techniques and where each one fits.]]></summary></entry><entry><title type="html">The Case for Systems Engineering in the Agentic Era</title><link href="https://goyalankit.com/blog/the-case-for-systems-engineering-in-the-agentic-era" rel="alternate" type="text/html" title="The Case for Systems Engineering in the Agentic Era" /><published>2026-01-25T20:08:00+00:00</published><updated>2026-01-25T20:08:00+00:00</updated><id>https://goyalankit.com/blog/the-case-for-systems-engineering-in-the-agentic-era</id><content type="html" xml:base="https://goyalankit.com/blog/the-case-for-systems-engineering-in-the-agentic-era"><![CDATA[<p>I remember attending Docker meetups in Austin while I was in graduate school. There was a real sense of excitement in the room. Containers were still new, people were experimenting, and it felt like we were watching the way software was built change in real time.</p>

<p>Much of the conversation was about systems fundamentals: isolation, networking, resource limits, security, and what happened when things failed. Over time, the technology matured and those concerns became part of the platform. Most developers could simply assume that containers worked. Only specialized use cases continued to demand closer attention.</p>

<figure class="systems-figure" aria-label="Abstract agent runtime surrounded by concentric systems safeguards">
  <div class="systems-figure__scroll" tabindex="0" aria-label="Scrollable abstract systems diagram">
    <svg class="systems-map" viewBox="0 0 860 400" role="img" aria-labelledby="systems-map-title systems-map-desc">
      <title id="systems-map-title">Agent runtime surrounded by concentric systems safeguards</title>
      <desc id="systems-map-desc">A goal enters through a policy gate and reaches an agent runtime inside resource, reliability, and security boundaries. Tools, data, and network capabilities connect at the perimeter. Failed work follows a bounded retry path through a checkpoint, while trace state remains observable.</desc>
      <defs>
        <marker id="systems-arrow" markerWidth="9" markerHeight="9" refX="8" refY="4.5" orient="auto">
          <path d="M 0 0 L 9 4.5 L 0 9 z"></path>
        </marker>
      </defs>

      <circle class="systems-map__ring systems-map__ring--security" cx="430" cy="200" r="160"></circle>
      <circle class="systems-map__ring systems-map__ring--reliability" cx="430" cy="200" r="120"></circle>
      <circle class="systems-map__ring systems-map__ring--resources" cx="430" cy="200" r="82"></circle>

      <rect class="systems-map__ring-label systems-map__ring-label--security" x="350" y="46" width="86" height="24" rx="12"></rect>
      <text class="systems-map__ring-label-text" x="393" y="62" text-anchor="middle">security</text>
      <rect class="systems-map__ring-label systems-map__ring-label--reliability" x="319" y="99" width="94" height="24" rx="12"></rect>
      <text class="systems-map__ring-label-text" x="366" y="115" text-anchor="middle">reliability</text>
      <rect class="systems-map__ring-label systems-map__ring-label--resources" x="355" y="132" width="88" height="24" rx="12"></rect>
      <text class="systems-map__ring-label-text" x="399" y="148" text-anchor="middle">resources</text>

      <rect class="systems-map__goal" x="24" y="185" width="66" height="30" rx="15"></rect>
      <text class="systems-map__contained-label" x="57" y="204" text-anchor="middle">goal</text>
      <path class="systems-map__path systems-map__path--active" d="M 90 200 L 125 200" marker-end="url(#systems-arrow)"></path>

      <polygon class="systems-map__gate" points="150,175 175,200 150,225 125,200"></polygon>
      <text class="systems-map__contained-label" x="150" y="204" text-anchor="middle">policy</text>
      <path class="systems-map__path systems-map__path--active" d="M 175 200 L 392 200" marker-end="url(#systems-arrow)"></path>

      <path class="systems-map__link" d="M 430 162 L 515 88"></path>
      <path class="systems-map__link" d="M 468 181 L 600 112"></path>
      <path class="systems-map__link" d="M 468 219 L 620 251"></path>
      <path class="systems-map__link" d="M 405 230 L 310 295"></path>

      <polygon class="systems-map__agent" points="430,162 468,181 468,219 430,238 392,219 392,181"></polygon>
      <circle class="systems-map__agent-core" cx="430" cy="202" r="5"></circle>
      <text class="systems-map__agent-text" x="430" y="192" text-anchor="middle">agent</text>
      <text class="systems-map__agent-text systems-map__agent-text--small" x="430" y="220" text-anchor="middle">runtime</text>

      <circle class="systems-map__capability systems-map__capability--tools" cx="515" cy="66" r="30"></circle>
      <text class="systems-map__contained-label" x="515" y="70" text-anchor="middle">tools</text>

      <rect class="systems-map__capability systems-map__capability--data" x="585" y="88" width="72" height="46" rx="4"></rect>
      <text class="systems-map__contained-label" x="621" y="115" text-anchor="middle">data</text>

      <circle class="systems-map__capability systems-map__capability--network" cx="650" cy="260" r="34"></circle>
      <text class="systems-map__contained-label" x="650" y="264" text-anchor="middle">network</text>

      <rect class="systems-map__capability systems-map__capability--checkpoint" x="252" y="282" width="112" height="46" rx="23"></rect>
      <text class="systems-map__contained-label" x="308" y="309" text-anchor="middle">checkpoint</text>

      <rect class="systems-map__trace-label" x="394" y="310" width="72" height="30" rx="15"></rect>
      <text class="systems-map__contained-label" x="430" y="329" text-anchor="middle">trace</text>
      <path class="systems-map__trace" d="M 430 238 L 430 310"></path>
      <circle class="systems-map__trace-dot" cx="430" cy="264" r="4"></circle>
      <circle class="systems-map__trace-dot" cx="430" cy="286" r="4"></circle>

      <path class="systems-map__retry" d="M 626 286 C 566 350 424 368 364 315" marker-end="url(#systems-arrow)"></path>
      <rect class="systems-map__retry-label-box" x="466" y="340" width="112" height="24" rx="12"></rect>
      <text class="systems-map__retry-label" x="522" y="356" text-anchor="middle">bounded retry</text>

      <path class="systems-map__path systems-map__path--output" d="M 468 200 L 770 200" marker-end="url(#systems-arrow)"></path>
      <rect class="systems-map__result" x="770" y="185" width="66" height="30" rx="15"></rect>
      <text class="systems-map__contained-label" x="803" y="204" text-anchor="middle">action</text>
    </svg>
  </div>
</figure>

<p>Today, working with AI agents gives me a similar feeling. We are once again exploring a new way of running software—and rediscovering why systems engineering matters.</p>

<p>The reason lies in how control shifts at runtime. Traditional applications follow paths written and reviewed in advance. Agents interpret goals, choose tools, interact with external systems, and decide what to do next at runtime. Some can operate for hours with limited supervision.</p>

<p>That changes the security boundary. We need to control not only where an agent runs, but also which tools it can use, what data it can access, where it can communicate, and how long it can continue. Policies must be explicit, credentials short-lived, and actions observable.</p>

<p>The familiar reliability problems return as well. Agents depend on APIs that time out, tools that partially succeed, and services that become unavailable. Long-running work requires checkpoints, durable state, bounded retries, and a safe way to resume without repeating actions.</p>

<p>Finally, autonomy makes resource management critical. An agent can consume compute, storage, model capacity, and external APIs for as long as it is allowed to run. And unlike a person, it does not get tired. It may continue retrying a broken operation or following an unproductive path indefinitely.</p>

<p>In many ways, agents are bringing us back to the questions we faced during the early days of containers. The problems are familiar, but the environment has changed. The model may be the most visible part of an agent, but systems engineering is what will allow agents to operate safely, reliably, and at scale.</p>]]></content><author><name></name></author><category term="posts" /><category term="ai" /><category term="agents" /><category term="systems" /><category term="engineering" /><summary type="html"><![CDATA[I remember attending Docker meetups in Austin while I was in graduate school. There was a real sense of excitement in the room. Containers were still new, people were experimenting, and it felt like we were watching the way software was built change in real time.]]></summary></entry><entry><title type="html">LoRA adapters: one base model, many skills</title><link href="https://goyalankit.com/blog/lora-adapters-one-base-many-skills" rel="alternate" type="text/html" title="LoRA adapters: one base model, many skills" /><published>2025-03-18T16:31:00+00:00</published><updated>2025-03-18T16:31:00+00:00</updated><id>https://goyalankit.com/blog/lora-adapters-one-base-many-skills</id><content type="html" xml:base="https://goyalankit.com/blog/lora-adapters-one-base-many-skills"><![CDATA[<p>Imagine you bought a giant encyclopedia. It represents a massive AI model, like GPT-4 or Llama 3. You want to customize it so it becomes an expert on your company’s internal rules. That customization is <strong>fine-tuning</strong>.</p>

<h2 id="1-what-is-lora-and-why-is-it-needed">1. What is LoRA, and why is it needed?</h2>

<p>Full fine-tuning rewrites the encyclopedia and saves another giant copy. Three specialties can mean three complete model checkpoints.</p>

<p><strong>LoRA (Low-Rank Adaptation)</strong> keeps the original model frozen and learns a slim packet of edits called an <strong>adapter</strong>. At each selected layer:</p>

<div class="lora-equation" role="img" aria-label="W prime equals W plus delta W, where delta W equals alpha divided by r times B A">
  <span>W' = W + &Delta;W</span>
  <span>&Delta;W = (&alpha; / r) BA</span>
</div>

<p><code class="language-plaintext highlighter-rouge">W</code> is the frozen pretrained matrix. The adapter contains two much smaller trainable matrices, <code class="language-plaintext highlighter-rouge">A</code> and <code class="language-plaintext highlighter-rouge">B</code>. Rank <code class="language-plaintext highlighter-rouge">r</code> controls the narrow bottleneck between them.</p>

<p>LoRA works when the task-specific change fits inside this smaller subspace. It is effective often, but not guaranteed; rank and target layers still need evaluation.</p>

<h2 id="2-is-training-a-lora-adapter-cheaper">2. Is training a LoRA adapter cheaper?</h2>

<aside class="lora-note">
  <strong>Short answer: yes for trainable memory and checkpoint size; not by the same factor for compute.</strong> The frozen base still runs during both the forward and backward passes.
</aside>

<p>For a <code class="language-plaintext highlighter-rouge">4096 x 4096</code> matrix, full fine-tuning can update 16,777,216 parameters. A rank-16 adapter learns 131,072: <strong>0.78%</strong> as many for that matrix.</p>

<p>Across 32 layers with LoRA on <code class="language-plaintext highlighter-rouge">q_proj</code> and <code class="language-plaintext highlighter-rouge">v_proj</code>, that illustrative rank-16 adapter is about <strong>16 MiB</strong> in 16-bit storage. A 7-billion-parameter 16-bit base is about <strong>14 GB</strong>: the saved adapter is roughly 800x smaller.</p>

<p>Training memory does not fall 800x because activations and the base weights remain. The saving comes from keeping gradients and optimizer state only for <code class="language-plaintext highlighter-rouge">A</code> and <code class="language-plaintext highlighter-rouge">B</code>.</p>

<figure class="lora-pass-figure lora-wide" aria-labelledby="pass-caption">
  <figcaption id="pass-caption"><strong>Figure 1. Step through training.</strong> Compare full fine-tuning with LoRA, then follow what is computed, traversed, stored, and updated in each layer.</figcaption>

  <div class="lora-pass-demo" data-lora-pass="" data-mode="lora" data-phase="idle">
    <div class="lora-pass-demo__controls lora-js-only">
      <fieldset>
        <legend>Training mode</legend>
        <label>
          <input type="radio" name="training-mode" value="lora" checked="" />
          <span>LoRA</span>
        </label>
        <label>
          <input type="radio" name="training-mode" value="full" />
          <span>Full fine-tuning</span>
        </label>
      </fieldset>

      <div>
        <button type="button" data-pass-next="">Next forward</button>
        <button type="button" data-pass-reset="">Reset</button>
      </div>
    </div>

    <div class="lora-training-cost" aria-label="A rank-16 adapter has 131,072 trainable parameters versus 16,777,216 in the full matrix, a 128 times reduction">
      <span><strong>16,777,216</strong> full-matrix parameters</span>
      <b aria-hidden="true">&rarr;</b>
      <span><strong>131,072</strong> rank-16 adapter parameters</span>
      <span><strong>0.78%</strong> trainable</span>
    </div>

    <div class="lora-pass-demo__legend" aria-label="Diagram state legend">
      <span><i class="is-computed"></i> computed</span>
      <span><i class="is-gradient"></i> gradient traversed</span>
      <span><i class="is-updated"></i> updated</span>
      <span><i class="is-frozen"></i> frozen</span>
    </div>

    <div class="lora-pass-demo__flow" tabindex="0" aria-label="Scrollable three-layer LoRA computation graph">
      <svg class="lora-pass-svg" viewBox="0 0 820 225" role="img" aria-labelledby="pass-flow-title pass-flow-desc">
        <title id="pass-flow-title">Three sequential layers with parallel LoRA paths</title>
        <desc id="pass-flow-desc">Each activation enters a layer, splits through frozen W and trainable A then B, sums, and becomes the input activation for the next layer. The final activation produces the loss.</desc>
        <defs>
          <marker id="pass-flow-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
            <path d="M 0 0 L 8 4 L 0 8 z"></path>
          </marker>
        </defs>

        <circle class="lora-pass-activation" cx="22" cy="105" r="14"></circle>
        <text class="lora-pass-node-label" x="22" y="109" text-anchor="middle">h0</text>
        <path class="lora-pass-connector" d="M 36 105 L 55 105" marker-end="url(#pass-flow-arrow)"></path>

        <g class="lora-pass-layer" data-pass-layer="0" data-state="idle" transform="translate(55 0)">
          <text class="lora-pass-layer__title" x="0" y="18">Layer 1</text>
          <text class="lora-pass-layer__status" data-pass-layer-status="" x="150" y="18" text-anchor="end">waiting</text>
          <path class="lora-pass-base-path" d="M 0 105 C 8 105 8 61 20 61 M 95 61 C 108 61 108 105 107 105"></path>
          <path class="lora-pass-adapter-path" d="M 0 105 C 8 105 8 160 20 160 M 50 160 L 65 160 M 95 160 C 108 160 108 105 107 105"></path>
          <rect class="lora-pass-base-block" x="20" y="45" width="75" height="32"></rect>
          <text class="lora-pass-base-text" data-pass-base="" x="57.5" y="65" text-anchor="middle">W frozen</text>
          <rect class="lora-pass-adapter-block" x="20" y="145" width="30" height="30"></rect>
          <text class="lora-pass-adapter-text" x="35" y="164" text-anchor="middle">A1</text>
          <rect class="lora-pass-adapter-block" x="65" y="145" width="30" height="30"></rect>
          <text class="lora-pass-adapter-text" x="80" y="164" text-anchor="middle">B1</text>
          <circle class="lora-pass-sum" cx="120" cy="105" r="13"></circle>
          <text class="lora-pass-node-label" x="120" y="110" text-anchor="middle">+</text>
          <path class="lora-pass-base-path" d="M 133 105 L 136 105"></path>
          <circle class="lora-pass-activation" cx="150" cy="105" r="14"></circle>
          <text class="lora-pass-node-label" x="150" y="109" text-anchor="middle">h1</text>
        </g>

        <path class="lora-pass-connector" d="M 219 105 L 275 105" marker-end="url(#pass-flow-arrow)"></path>

        <g class="lora-pass-layer" data-pass-layer="1" data-state="idle" transform="translate(275 0)">
          <text class="lora-pass-layer__title" x="0" y="18">Layer 2</text>
          <text class="lora-pass-layer__status" data-pass-layer-status="" x="150" y="18" text-anchor="end">waiting</text>
          <path class="lora-pass-base-path" d="M 0 105 C 8 105 8 61 20 61 M 95 61 C 108 61 108 105 107 105"></path>
          <path class="lora-pass-adapter-path" d="M 0 105 C 8 105 8 160 20 160 M 50 160 L 65 160 M 95 160 C 108 160 108 105 107 105"></path>
          <rect class="lora-pass-base-block" x="20" y="45" width="75" height="32"></rect>
          <text class="lora-pass-base-text" data-pass-base="" x="57.5" y="65" text-anchor="middle">W frozen</text>
          <rect class="lora-pass-adapter-block" x="20" y="145" width="30" height="30"></rect>
          <text class="lora-pass-adapter-text" x="35" y="164" text-anchor="middle">A2</text>
          <rect class="lora-pass-adapter-block" x="65" y="145" width="30" height="30"></rect>
          <text class="lora-pass-adapter-text" x="80" y="164" text-anchor="middle">B2</text>
          <circle class="lora-pass-sum" cx="120" cy="105" r="13"></circle>
          <text class="lora-pass-node-label" x="120" y="110" text-anchor="middle">+</text>
          <path class="lora-pass-base-path" d="M 133 105 L 136 105"></path>
          <circle class="lora-pass-activation" cx="150" cy="105" r="14"></circle>
          <text class="lora-pass-node-label" x="150" y="109" text-anchor="middle">h2</text>
        </g>

        <path class="lora-pass-connector" d="M 439 105 L 495 105" marker-end="url(#pass-flow-arrow)"></path>

        <g class="lora-pass-layer" data-pass-layer="2" data-state="idle" transform="translate(495 0)">
          <text class="lora-pass-layer__title" x="0" y="18">Layer 3</text>
          <text class="lora-pass-layer__status" data-pass-layer-status="" x="150" y="18" text-anchor="end">waiting</text>
          <path class="lora-pass-base-path" d="M 0 105 C 8 105 8 61 20 61 M 95 61 C 108 61 108 105 107 105"></path>
          <path class="lora-pass-adapter-path" d="M 0 105 C 8 105 8 160 20 160 M 50 160 L 65 160 M 95 160 C 108 160 108 105 107 105"></path>
          <rect class="lora-pass-base-block" x="20" y="45" width="75" height="32"></rect>
          <text class="lora-pass-base-text" data-pass-base="" x="57.5" y="65" text-anchor="middle">W frozen</text>
          <rect class="lora-pass-adapter-block" x="20" y="145" width="30" height="30"></rect>
          <text class="lora-pass-adapter-text" x="35" y="164" text-anchor="middle">A3</text>
          <rect class="lora-pass-adapter-block" x="65" y="145" width="30" height="30"></rect>
          <text class="lora-pass-adapter-text" x="80" y="164" text-anchor="middle">B3</text>
          <circle class="lora-pass-sum" cx="120" cy="105" r="13"></circle>
          <text class="lora-pass-node-label" x="120" y="110" text-anchor="middle">+</text>
          <path class="lora-pass-base-path" d="M 133 105 L 136 105"></path>
          <circle class="lora-pass-activation" cx="150" cy="105" r="14"></circle>
          <text class="lora-pass-node-label" x="150" y="109" text-anchor="middle">h3</text>
        </g>

        <path class="lora-pass-connector" d="M 659 105 L 700 105" marker-end="url(#pass-flow-arrow)"></path>
        <g class="lora-pass-loss" data-pass-loss="" data-state="idle">
          <rect x="700" y="77" width="100" height="56"></rect>
          <text class="lora-pass-loss__label" x="750" y="99" text-anchor="middle">objective</text>
          <text class="lora-pass-loss__value" x="750" y="119" text-anchor="middle">loss</text>
        </g>
      </svg>
    </div>

    <p class="lora-pass-demo__status" data-pass-status="" aria-live="polite">LoRA mode: W is computed but frozen; A/B are trainable. Start with the forward pass.</p>

    <div class="lora-memory-compare">
      <header>
        <strong>Illustrative memory comparison per layer</strong>
        <span>Bars are scaled within each row. Values are explanatory, not model-wide measurements.</span>
      </header>
      <div class="lora-memory-compare__grid" role="table" aria-label="Full fine-tuning and LoRA memory comparison per layer">
        <div role="row" class="lora-memory-compare__head">
          <span role="columnheader">Memory</span>
          <span role="columnheader">Full fine-tuning</span>
          <span role="columnheader">LoRA</span>
          <span role="columnheader">Why it matters</span>
        </div>
        <div role="row">
          <strong role="rowheader">Weights</strong>
          <span role="cell" class="lora-memory-cell"><b>2 MB</b><i class="lora-memory-bar is-near-full"></i><small>W</small></span>
          <span role="cell" class="lora-memory-cell"><b>2.016 MB</b><i class="lora-memory-bar is-full"></i><small>W + A/B</small></span>
          <span role="cell">The frozen base still has to be resident.</span>
        </div>
        <div role="row">
          <strong role="rowheader">Gradients</strong>
          <span role="cell" class="lora-memory-cell"><b>2 MB</b><i class="lora-memory-bar is-full"></i><small>W gradients</small></span>
          <span role="cell" class="lora-memory-cell"><b>16 KB</b><i class="lora-memory-bar is-tiny"></i><small>A/B gradients</small></span>
          <span role="cell"><strong>125x less</strong> gradient memory.</span>
        </div>
        <div role="row">
          <strong role="rowheader">Optimizer</strong>
          <span role="cell" class="lora-memory-cell"><b>4 MB</b><i class="lora-memory-bar is-full"></i><small>W state</small></span>
          <span role="cell" class="lora-memory-cell"><b>32 KB</b><i class="lora-memory-bar is-tiny"></i><small>A/B state</small></span>
          <span role="cell"><strong>125x less</strong> optimizer memory.</span>
        </div>
        <div role="row">
          <strong role="rowheader">Activations</strong>
          <span role="cell" class="lora-memory-cell"><b>64 KB</b><i class="lora-memory-bar is-full"></i><small>saved for backward</small></span>
          <span role="cell" class="lora-memory-cell"><b>64 KB</b><i class="lora-memory-bar is-full"></i><small>saved for backward</small></span>
          <span role="cell">No saving in this simplified example.</span>
        </div>
        <div role="row" class="lora-memory-compare__summary">
          <strong role="rowheader">Trainable state</strong>
          <span role="cell" class="lora-memory-cell"><b>6 MB</b><i class="lora-memory-bar is-full"></i><small>gradients + optimizer</small></span>
          <span role="cell" class="lora-memory-cell"><b>48 KB</b><i class="lora-memory-bar is-tiny"></i><small>gradients + optimizer</small></span>
          <span role="cell"><strong>125x smaller</strong></span>
        </div>
        <div role="row" class="lora-memory-compare__summary">
          <strong role="rowheader">Total shown</strong>
          <span role="cell" class="lora-memory-cell"><b>8.064 MB</b><i class="lora-memory-bar is-full"></i></span>
          <span role="cell" class="lora-memory-cell"><b>2.128 MB</b><i class="lora-memory-bar is-quarter"></i></span>
          <span role="cell"><strong>3.8x smaller overall</strong></span>
        </div>
      </div>
    </div>
  </div>
</figure>

<p>There is no universal “training is N times cheaper” number. Sequence length, batch size, quantization, checkpointing, and target modules still dominate the real GPU bill.</p>

<h2 id="3-is-serving-lora-cheaper">3. Is serving LoRA cheaper?</h2>

<aside class="lora-note">
  <strong>Cheaper for many specialists, not for each token.</strong> Every request still runs the base model plus a small adapter computation.
</aside>

<p>The operational win is sharing one base. One hundred 16 MiB adapters add about 1.6 GB of artifacts; one hundred separate 14 GB checkpoints would be about 1.4 TB.</p>

<figure class="adapter-router lora-wide" data-lora-router="" data-route="support" aria-labelledby="router-caption">
  <figcaption id="router-caption"><strong>Figure 2. One base, many named adapters.</strong> A router selects the small update used for each request.</figcaption>

  <fieldset class="adapter-router__controls lora-js-only">
    <legend>Choose an illustrative request</legend>
    <label>
      <input type="radio" name="lora-route" value="support" checked="" />
      <span>Support</span>
    </label>
    <label>
      <input type="radio" name="lora-route" value="finance" />
      <span>Finance</span>
    </label>
    <label>
      <input type="radio" name="lora-route" value="base" />
      <span>Base only</span>
    </label>
  </fieldset>

  <div class="adapter-router__request">
    <span class="adapter-router__step">request</span>
    <strong data-router-prompt="">My password reset link expired. What should I do?</strong>
  </div>

  <div class="adapter-router__loaded" aria-label="Loaded adapter bank">
    <span class="adapter-router__step">loaded adapters</span>
    <div data-adapter-node="support" aria-current="true">
      <strong>support</strong>
      <span data-adapter-status="">active</span>
    </div>
    <div data-adapter-node="finance" aria-current="false">
      <strong>finance</strong>
      <span data-adapter-status="">idle</span>
    </div>
    <div data-adapter-node="base" aria-current="false">
      <strong>base only</strong>
      <span data-adapter-status="">idle</span>
    </div>
    <span>active: <strong data-router-active-name="">support</strong></span>
  </div>

  <div class="adapter-router__actions lora-js-only">
    <button type="button" data-inference-next="">Next layer</button>
    <button type="button" data-inference-reset="">Reset</button>
  </div>

  <div class="adapter-router__flow" tabindex="0" aria-label="Scrollable three-layer inference graph">
    <svg class="lora-pass-svg" viewBox="0 0 820 225" role="img" aria-labelledby="inference-flow-title inference-flow-desc">
      <title id="inference-flow-title">Three-layer inference with a selected LoRA adapter</title>
      <desc id="inference-flow-desc">A request activation passes through three sequential layers. In each layer, the frozen base path and selected adapter path are added. Base-only mode removes the adapter path. The final activation produces a token.</desc>
      <defs>
        <marker id="inference-flow-arrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
          <path d="M 0 0 L 8 4 L 0 8 z"></path>
        </marker>
      </defs>

      <circle class="lora-pass-activation" cx="22" cy="105" r="14"></circle>
      <text class="lora-pass-node-label" x="22" y="109" text-anchor="middle">h0</text>
      <path class="lora-pass-connector" d="M 36 105 L 55 105" marker-end="url(#inference-flow-arrow)"></path>

      <g class="lora-pass-layer adapter-router__layer" data-inference-layer="0" data-state="idle" transform="translate(55 0)">
        <text class="lora-pass-layer__title" x="0" y="18">Layer 1</text>
        <text class="lora-pass-layer__status" data-inference-layer-status="" x="150" y="18" text-anchor="end">waiting</text>
        <path class="lora-pass-base-path" d="M 0 105 C 8 105 8 61 20 61 M 95 61 C 108 61 108 105 107 105"></path>
        <path class="lora-pass-adapter-path" d="M 0 105 C 8 105 8 160 20 160 M 50 160 L 65 160 M 95 160 C 108 160 108 105 107 105"></path>
        <rect class="lora-pass-base-block" x="20" y="45" width="75" height="32"></rect>
        <text class="lora-pass-base-text" x="57.5" y="65" text-anchor="middle">W frozen</text>
        <rect class="lora-pass-adapter-block" x="20" y="145" width="30" height="30"></rect>
        <text class="lora-pass-adapter-text" x="35" y="164" text-anchor="middle">A1</text>
        <rect class="lora-pass-adapter-block" x="65" y="145" width="30" height="30"></rect>
        <text class="lora-pass-adapter-text" x="80" y="164" text-anchor="middle">B1</text>
        <circle class="lora-pass-sum" cx="120" cy="105" r="13"></circle>
        <text class="lora-pass-node-label" x="120" y="110" text-anchor="middle">+</text>
        <path class="lora-pass-base-path" d="M 133 105 L 136 105"></path>
        <circle class="lora-pass-activation" cx="150" cy="105" r="14"></circle>
        <text class="lora-pass-node-label" x="150" y="109" text-anchor="middle">h1</text>
      </g>

      <path class="lora-pass-connector" d="M 219 105 L 275 105" marker-end="url(#inference-flow-arrow)"></path>

      <g class="lora-pass-layer adapter-router__layer" data-inference-layer="1" data-state="idle" transform="translate(275 0)">
        <text class="lora-pass-layer__title" x="0" y="18">Layer 2</text>
        <text class="lora-pass-layer__status" data-inference-layer-status="" x="150" y="18" text-anchor="end">waiting</text>
        <path class="lora-pass-base-path" d="M 0 105 C 8 105 8 61 20 61 M 95 61 C 108 61 108 105 107 105"></path>
        <path class="lora-pass-adapter-path" d="M 0 105 C 8 105 8 160 20 160 M 50 160 L 65 160 M 95 160 C 108 160 108 105 107 105"></path>
        <rect class="lora-pass-base-block" x="20" y="45" width="75" height="32"></rect>
        <text class="lora-pass-base-text" x="57.5" y="65" text-anchor="middle">W frozen</text>
        <rect class="lora-pass-adapter-block" x="20" y="145" width="30" height="30"></rect>
        <text class="lora-pass-adapter-text" x="35" y="164" text-anchor="middle">A2</text>
        <rect class="lora-pass-adapter-block" x="65" y="145" width="30" height="30"></rect>
        <text class="lora-pass-adapter-text" x="80" y="164" text-anchor="middle">B2</text>
        <circle class="lora-pass-sum" cx="120" cy="105" r="13"></circle>
        <text class="lora-pass-node-label" x="120" y="110" text-anchor="middle">+</text>
        <path class="lora-pass-base-path" d="M 133 105 L 136 105"></path>
        <circle class="lora-pass-activation" cx="150" cy="105" r="14"></circle>
        <text class="lora-pass-node-label" x="150" y="109" text-anchor="middle">h2</text>
      </g>

      <path class="lora-pass-connector" d="M 439 105 L 495 105" marker-end="url(#inference-flow-arrow)"></path>

      <g class="lora-pass-layer adapter-router__layer" data-inference-layer="2" data-state="idle" transform="translate(495 0)">
        <text class="lora-pass-layer__title" x="0" y="18">Layer 3</text>
        <text class="lora-pass-layer__status" data-inference-layer-status="" x="150" y="18" text-anchor="end">waiting</text>
        <path class="lora-pass-base-path" d="M 0 105 C 8 105 8 61 20 61 M 95 61 C 108 61 108 105 107 105"></path>
        <path class="lora-pass-adapter-path" d="M 0 105 C 8 105 8 160 20 160 M 50 160 L 65 160 M 95 160 C 108 160 108 105 107 105"></path>
        <rect class="lora-pass-base-block" x="20" y="45" width="75" height="32"></rect>
        <text class="lora-pass-base-text" x="57.5" y="65" text-anchor="middle">W frozen</text>
        <rect class="lora-pass-adapter-block" x="20" y="145" width="30" height="30"></rect>
        <text class="lora-pass-adapter-text" x="35" y="164" text-anchor="middle">A3</text>
        <rect class="lora-pass-adapter-block" x="65" y="145" width="30" height="30"></rect>
        <text class="lora-pass-adapter-text" x="80" y="164" text-anchor="middle">B3</text>
        <circle class="lora-pass-sum" cx="120" cy="105" r="13"></circle>
        <text class="lora-pass-node-label" x="120" y="110" text-anchor="middle">+</text>
        <path class="lora-pass-base-path" d="M 133 105 L 136 105"></path>
        <circle class="lora-pass-activation" cx="150" cy="105" r="14"></circle>
        <text class="lora-pass-node-label" x="150" y="109" text-anchor="middle">h3</text>
      </g>

      <path class="lora-pass-connector" d="M 659 105 L 700 105" marker-end="url(#inference-flow-arrow)"></path>
      <g class="adapter-router__token" data-inference-output="" data-state="idle">
        <rect x="700" y="77" width="100" height="56"></rect>
        <text class="adapter-router__token-label" x="750" y="99" text-anchor="middle">generated</text>
        <text class="adapter-router__token-value" x="750" y="119" text-anchor="middle">token</text>
      </g>
    </svg>
  </div>

  <p class="adapter-router__status" data-router-result="" aria-live="polite">The request uses W + delta W_support. The finance adapter stays loaded but inactive.</p>

  <div class="adapter-router__memory">
    <span><small>shared base</small><strong>14 GB</strong></span>
    <span><small>loaded adapters</small><strong>32 MiB</strong></span>
    <span><small>active adapter</small><strong data-router-active-memory="">16 MiB</strong></span>
    <span><small>KV cache</small><strong>grows per token</strong></span>
  </div>
</figure>

<p>The implementation idea is small:</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="c1"># Pseudocode: train independent adapters from one frozen base.
</span><span class="n">base</span> <span class="o">=</span> <span class="n">load_model</span><span class="p">(</span><span class="s">"base"</span><span class="p">,</span> <span class="n">frozen</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">data</span> <span class="ow">in</span> <span class="n">tasks</span><span class="p">:</span>
    <span class="n">adapter</span> <span class="o">=</span> <span class="n">train_lora</span><span class="p">(</span><span class="n">base</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">rank</span><span class="o">=</span><span class="mi">16</span><span class="p">)</span>
    <span class="n">save_adapter</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">adapter</span><span class="p">)</span>

<span class="c1"># Serve one base with several named adapters.
</span><span class="n">server</span> <span class="o">=</span> <span class="n">load_model</span><span class="p">(</span><span class="s">"base"</span><span class="p">)</span>
<span class="n">server</span><span class="p">.</span><span class="n">load_adapter</span><span class="p">(</span><span class="s">"support"</span><span class="p">)</span>
<span class="n">server</span><span class="p">.</span><span class="n">load_adapter</span><span class="p">(</span><span class="s">"finance"</span><span class="p">)</span>

<span class="k">for</span> <span class="n">batch</span> <span class="ow">in</span> <span class="n">requests</span><span class="p">:</span>
    <span class="n">names</span> <span class="o">=</span> <span class="p">[</span><span class="n">route</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="k">for</span> <span class="n">request</span> <span class="ow">in</span> <span class="n">batch</span><span class="p">]</span>
    <span class="n">responses</span> <span class="o">=</span> <span class="n">server</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span><span class="n">batch</span><span class="p">,</span> <span class="n">adapter_names</span><span class="o">=</span><span class="n">names</span><span class="p">)</span></code></pre></figure>

<h2 id="deployment-strategies-in-practice">Deployment Strategies in Practice</h2>

<p>When putting LoRA into production, you have a few ways to manage how adapters interact with the base model:</p>

<ul>
  <li><strong>Switching:</strong> Activating one specific adapter for an entire request or a homogeneous batch.</li>
  <li><strong>Mixed-Adapter Batching:</strong> Assigning different adapters to individual sequences within the same batch. Production-scale implementations typically use specialized serving engines such as vLLM or LoRAX.</li>
  <li><strong>Composition:</strong> Mathematically blending multiple adapters together dynamically, rather than just routing requests between them.</li>
  <li><strong>Merging:</strong> Permanently baking the adapter’s weights into the base model. This removes the separate adapter-layer inference overhead but destroys the ability to hot-swap adapters on the fly.</li>
</ul>

<p><strong>LoRA makes specialization dramatically smaller. Training still traverses the base, and serving still computes the base, but many specialists can share that one expensive foundation.</strong></p>

<h2 id="references">References</h2>

<ol>
  <li><a href="https://arxiv.org/abs/2106.09685">LoRA: Low-Rank Adaptation of Large Language Models</a></li>
  <li><a href="https://huggingface.co/docs/peft/en/package_reference/lora">Hugging Face PEFT LoRA reference</a></li>
  <li><a href="https://huggingface.co/docs/peft/en/developer_guides/checkpoint">PEFT checkpoint format</a></li>
</ol>]]></content><author><name></name></author><category term="posts" /><category term="machine-learning" /><category term="transformers" /><category term="lora" /><category term="peft" /><summary type="html"><![CDATA[A visual explanation of what LoRA is, what it saves during training, and how one base model serves multiple adapters.]]></summary></entry><entry><title type="html">Why VXLAN (rfc7348)?</title><link href="https://goyalankit.com/blog/note-on-vxlan" rel="alternate" type="text/html" title="Why VXLAN (rfc7348)?" /><published>2024-01-24T10:04:00+00:00</published><updated>2024-01-24T10:04:00+00:00</updated><id>https://goyalankit.com/blog/note-on-vxlan</id><content type="html" xml:base="https://goyalankit.com/blog/note-on-vxlan"><![CDATA[<p>From the <a href="//tools.ietf.org/html/rfc7348#page-5">rfc7348</a></p>
<blockquote>
  <p>VXLAN is a Layer 2 overlay scheme on a Layer 3 network.</p>
</blockquote>

<p>To understand VXLAN better, let’s first understand what’s subnetting and VLAN.</p>

<p>Let’s say we have a physical LAN where there are multiple hosts with IPs in <code class="language-plaintext highlighter-rouge">10.1.2.0/24</code> network. Each host can talk to the other host using a switch alone. Now, we want to group set of hosts and separate them from each other. What are our options?</p>

<h1 id="ip-subnetting">IP Subnetting</h1>
<p>The first thought that comes to my mind is IP subnetting. It can prevent two hosts in different subnets from talking to each other unless we explicitly allow it using a router.</p>

<p>Say now we have two subnets, <code class="language-plaintext highlighter-rouge">10.1.2.0/25</code> where the two ranges are <code class="language-plaintext highlighter-rouge">10.1.2.1 - 10.1.2.126</code> and <code class="language-plaintext highlighter-rouge">10.1.2.129  - 10.1.2.254</code>. The broadcast address for first subnet is <code class="language-plaintext highlighter-rouge">10.1.2.127</code> and for second subnet it’s <code class="language-plaintext highlighter-rouge">10.1.2.255</code>.</p>

<p>However, <strong>they still have the same layer 2 broadcast domain</strong>.</p>

<p>Consider the following example (in gif below) where even though the host (<code class="language-plaintext highlighter-rouge">10.1.2.2</code>) can’t ping the other hosts (<code class="language-plaintext highlighter-rouge">10.1.2.130</code> and <code class="language-plaintext highlighter-rouge">10.1.2.134</code>) but it can broadcast in the other subnet since the broadcast domain at layer 2 is same and switch will broadcast all the frames with destination <code class="language-plaintext highlighter-rouge">FFFF.FFFF.FFFF</code> to all the ports irrespective of layer 3 source/destination IPs.</p>

<p class="image_size_600"><img src="//gist.githubusercontent.com/goyalankit/df3686b62ac9bfd20f5eb292c02697bd/raw/3225ce3acb8e1d2b8a83da34f6028ac6fbaef9a7/broadcast_switch.gif" alt="bradcast switch" /></p>

<p>The behavior is same in presence of router that allows the two subnets to talk to each other. Since switch operates at Layer 2.</p>

<p class="image_size_600"><img src="//gist.githubusercontent.com/goyalankit/df3686b62ac9bfd20f5eb292c02697bd/raw/cc30fb145049c25966637c02e11d01f8277ff8d3/icmp_own_broadcast.gif" alt="icmp own subnet broadcast" /></p>

<h1 id="vlan">VLAN</h1>
<p>So how can we split the broadcast domains for the subnets? This is where VLAN comes in. VLAN can be used to separate the broadcast domains as well.</p>

<p>VLAN frames are tagged by switch based on what port they arrive at. VLAN header is 4 bytes long and is placed before the type field in ethernet frame. It contains a 12 bit VLAN Identifier (VID) that identifies the frame it belongs to.</p>

<p>In the example below, hosts (<code class="language-plaintext highlighter-rouge">10.1.2.3</code>, <code class="language-plaintext highlighter-rouge">10.1.2.2</code> and <code class="language-plaintext highlighter-rouge">10.1.2.135</code>) are in the same vlan (<code class="language-plaintext highlighter-rouge">vlan1</code>) and the remaining hosts belong to a different vlan. As in the previous experiments, let’s ping the broadcast address (<code class="language-plaintext highlighter-rouge">10.1.2.127</code>) from the host <code class="language-plaintext highlighter-rouge">10.1.2.3</code>. Note that in this case, the packet is only broadcasted to the hosts that belong to the same vlan irrespective of the subnet. Note that the host (<code class="language-plaintext highlighter-rouge">10.1.2.35</code>) will drop the packet since the destination belongs to a different subnet and we don’t have a default gateway set. However, hosts in the other vlan don’t even receive the <strong>icmp</strong> request.</p>

<p class="image_size_600"><img src="//gist.githubusercontent.com/goyalankit/df3686b62ac9bfd20f5eb292c02697bd/raw/30cf4ba8859f9dd0ab171cce7a43c41f779e71bc/vlan_broadcast.gif" alt="vlan broadcast" /></p>

<p>Note that 12 bit identifier limits the maximum number of vlans you can have to 4096. However with recent influx of virtualized environments, this is not enough and VLAN is not friendly enough for large number of multi tenant topologies. Hence the proposal for VXLAN.</p>

<h1 id="vxlan">VXLAN</h1>

<p>VXLAN stands for Virtual eXtensible Local Area Network, it’s a layer 2 in layer 3 overlay tunnel. More specifically an Ethernet in UDP tunnel. The idea of VXLAN is similar to VLAN, as in it provides logical separation of private networks.</p>

<p>Consider the network topology in the diagram below network A and B are part of the same private network separated by external network. A-P, B-Q use same subnet in the same physical network.
<img src="//gist.githubusercontent.com/goyalankit/df3686b62ac9bfd20f5eb292c02697bd/raw/72eaedd22c441ad96b5864555901f5f1fd6347bc/vxlan3.png" alt="" /></p>

<p>The requirement from underlay network (UDP) is that it should have ip connectivity between two networks, UDP port <a href="//www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=4789">4789</a> is reserved for VXLAN.</p>

<p>The endpoint of the tunnel, <strong>VTEP (Virtual Tunnel End Point)</strong> forms the control plane of VXLAN (overlay network). It maintains a mapping of internal MAC address to the outer IP address i.e., while sending a packet from A: 10.0.0.1 to B: 10.0.0.2. VTEP can learn</p>

<p>VMs in subnets (A and B in the above picture) are unaware of the VXLAN themselves, and they route the traffic as they normally would. Say, a VM (A, 10.0.0.1) wants to route traffic to another VM in same network (B, 10.0.0.2) located at a different physical server and network. It would send the MAC frame as it would if both were on the same physical network. VTEP on that host checks the MAC address of 10.0.0.2</p>

<p>Following packet trace shows the VXLAN packet wrapper in UDP outer packet. UDP forms the overlay network and inner nodes (with IP 10.0.0.1) don’t need to know about overlay network.</p>

<p class="image_size_600"><img src="//gist.githubusercontent.com/goyalankit/df3686b62ac9bfd20f5eb292c02697bd/raw/d1920b1f0cabfb35a5e350de61f1e4535d39cc7f/vxlan_packet_trace_1.png" alt="" /></p>

<p class="image_size_600"><img src="//gist.githubusercontent.com/goyalankit/df3686b62ac9bfd20f5eb292c02697bd/raw/d1920b1f0cabfb35a5e350de61f1e4535d39cc7f/vxlan_packet_trace_2.png" alt="" /></p>

<h1 id="conclusion">Conclusion</h1>
<p>VXLAN is an important piece of puzzle when it comes to scaling ipv4 and providing network abstractions. I hope you have a slightly better understanding of where VXLAN belongs in plethora of networking technologies.</p>

<h2 id="references">References:</h2>

<p>[1] <a href="//www.cloudshark.org/captures/670aeb7bad79">Packet trace for VXLAN - cloudshark</a><br />
[2] <a href="//tools.ietf.org/html/rfc7348">RFC-7348: Virtual eXtensible Local Area Network (VXLAN)</a></p>]]></content><author><name></name></author><category term="posts" /><category term="networks" /><category term="vxlan" /><summary type="html"><![CDATA[From the rfc7348 VXLAN is a Layer 2 overlay scheme on a Layer 3 network.]]></summary></entry><entry><title type="html">Linux device drivers</title><link href="https://goyalankit.com/blog/linux-device-drivers" rel="alternate" type="text/html" title="Linux device drivers" /><published>2017-05-12T22:07:00+00:00</published><updated>2017-05-12T22:07:00+00:00</updated><id>https://goyalankit.com/blog/linux-device-drivers</id><content type="html" xml:base="https://goyalankit.com/blog/linux-device-drivers"><![CDATA[<h1 id="environment-setup">Environment setup</h1>

<p>You need linux headers to be able to compile new module.</p>

<p>To make sure you have linux headers, see if the following path exists:</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nv">$ </span><span class="nb">ls</span> /usr/src/linux-headers-<span class="sb">`</span><span class="nb">uname</span> <span class="nt">-r</span><span class="sb">`</span>
<span class="nb">arch   </span>crypto         drivers   fs       init  Kbuild   kernel  
...</code></pre></figure>

<p>If the above directory is not present, you can install the headers using:</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nb">sudo </span>apt-get update <span class="o">&amp;&amp;</span> <span class="nb">sudo </span>apt-get <span class="nb">install </span>linux-headers-<span class="si">$(</span><span class="nb">uname</span> <span class="nt">-r</span><span class="si">)</span></code></pre></figure>

<p>Once the headers are installed you are good to go.</p>

<h1 id="writing-hello-module">Writing hello module</h1>

<p>To start off, create the following file in any directory.</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="c1">// hello.c</span>
<span class="cp">#include</span> <span class="cpf">&lt;linux/module.h&gt;</span><span class="cp">
</span>
<span class="n">MODULE_LICENSE</span><span class="p">(</span><span class="s">"Dual BSD/GPL"</span><span class="p">);</span></code></pre></figure>

<p>Create the following <strong>Makefile</strong> to compile your module.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">obj-m :<span class="o">=</span> hello.o
KDIR :<span class="o">=</span> /usr/src/linux-headers-3.2.0-23-generic

all:
	make <span class="nt">-C</span> <span class="si">$(</span>KDIR<span class="si">)</span> <span class="nv">M</span><span class="o">=</span><span class="si">$(</span>PWD<span class="si">)</span> modules
clean:
	make <span class="nt">-C</span> <span class="si">$(</span>KDIR<span class="si">)</span> <span class="nv">M</span><span class="o">=</span><span class="si">$(</span>PWD<span class="si">)</span> clean</code></pre></figure>

<p>After running <code class="language-plaintext highlighter-rouge">make</code>, you should see following:</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="c"># Check that *.ko is created.</span>
vagrant@precise64:~<span class="nv">$ </span><span class="nb">ls
</span>hello.c  hello.ko  hello.mod.c  hello.mod.o  hello.o  Makefile  
...
<span class="c"># Install the module.</span>
vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>insmod hello.ko
<span class="c"># Confirm the it's installed.</span>
vagrant@precise64:~<span class="nv">$ </span>lsmod | <span class="nb">grep </span>hello
hello                   8217  0
<span class="c"># Remove the module.</span>
vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>rmmod hello
<span class="c"># Confirm that it's removed.</span>
vagrant@precise64:~<span class="nv">$ </span>lsmod | <span class="nb">grep </span>hello</code></pre></figure>

<hr />

<p>Expanding further, each module needs to have an init and exit method.</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="cp">#include</span> <span class="cpf">&lt;linux/init.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;linux/module.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;linux/kernel.h&gt;</span><span class="cp">
</span>
<span class="n">MODULE_LICENSE</span><span class="p">(</span><span class="s">"Dual BSD/GPL"</span><span class="p">);</span>

<span class="k">static</span> <span class="kt">int</span> <span class="nf">hello_init</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">printk</span><span class="p">(</span><span class="s">"&lt;1&gt; Hola Mundo!</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
  <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">hello_exit</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">printk</span><span class="p">(</span><span class="s">"&lt;1&gt;Hasta Luego</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">module_init</span><span class="p">(</span><span class="n">hello_init</span><span class="p">);</span>
<span class="n">module_exit</span><span class="p">(</span><span class="n">hello_exit</span><span class="p">);</span></code></pre></figure>

<p>Again, use <code class="language-plaintext highlighter-rouge">inmod</code> to install the command and you should see <strong>Hola Mundo!</strong> in <code class="language-plaintext highlighter-rouge">dmesg</code>. <code class="language-plaintext highlighter-rouge">printk</code> is important here; as kernel will print it to the <code class="language-plaintext highlighter-rouge">syslog</code>. Pretty useful in understanding other drivers.</p>]]></content><author><name></name></author><category term="notes" /><category term="linux" /><category term="kernel" /><category term="modules" /><summary type="html"><![CDATA[Environment setup]]></summary></entry><entry><title type="html">Is the network device in promiscuous mode?</title><link href="https://goyalankit.com/blog/promiscuous-mode-detection" rel="alternate" type="text/html" title="Is the network device in promiscuous mode?" /><published>2017-05-08T22:40:00+00:00</published><updated>2017-05-08T22:40:00+00:00</updated><id>https://goyalankit.com/blog/promiscuous-mode-detection</id><content type="html" xml:base="https://goyalankit.com/blog/promiscuous-mode-detection"><![CDATA[<p>Wikipedia defines <a href="//en.wikipedia.org/wiki/Promiscuous_mode"><strong>promiscuous mode</strong></a> as a mode for a wired network interface controller (NIC) or wireless network interface controller (WNIC) that causes the controller to pass all traffic it receives to the central processing unit (CPU)rather than passing only the frames that the controller is intended to receive.</p>

<h1 id="how-do-i-tell-if-a-device-is-in-promiscuous-mode">How do I tell if a device is in promiscuous mode?</h1>

<p><strong>tl;dr</strong>: Kernel tracks promiscuous mode using flags on the device. For promiscuous mode, <a href="//elixir.free-electrons.com/linux/v3.10.105/source/include/uapi/linux/if.h#L39">IFF_PROMISC, 0x100</a> should be set.</p>

<p>For a given interface, check the flags to see if the promiscuous bit is set.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nv">$ </span><span class="nb">cat</span> /sys/devices/virtual/net/veth0/flags
0x1303  <span class="c"># 0001 001[1] 0000 0011   # device is in promiscuous mode.</span>

<span class="nv">$ </span><span class="nb">cat</span> /sys/devices/virtual/net/br0/flags
0x1003  <span class="c"># 0001 000[0] 0000 0011  # device is not in promiscuous mode.</span></code></pre></figure>

<p>Here’s a quick python script to test promiscuous mode for all interfaces:</p>

<script src="//gist.github.com/goyalankit/7ae7e967e68b1c2465646962e842ed2a.js"></script>

<hr />

<h1 id="problem-with-existing-tools">Problem with existing tools</h1>

<p>Figuring out if a given network device is in promiscuous mode using tools like <code class="language-plaintext highlighter-rouge">iproute2</code> or <code class="language-plaintext highlighter-rouge">netstat</code> can be trickier than you’d think.</p>

<p>At first glance, you’d think <code class="language-plaintext highlighter-rouge">iproute2</code> or <code class="language-plaintext highlighter-rouge">netstat -i</code> command should tell you if the device is in promiscuous mode but that’s not always the case.</p>

<p>We’ll consider two examples here, first to show the case where it works as expected and second to show where it doesn’t.</p>

<p><strong>A word on <code class="language-plaintext highlighter-rouge">netstat</code>:</strong></p>

<p>In netstat command, flag <strong><code class="language-plaintext highlighter-rouge">P</code></strong> is used to display if the interface is in promiscuous mode. However, <strong><code class="language-plaintext highlighter-rouge">P</code></strong> is also used for point to point connection. You can verify from the net-tools code <a href="//github.com/ecki/net-tools/blob/2617bbe4499749b93317cb41b2104278295eba81/lib/interface.c#L627-L634">here</a></p>

<h2 id="example-1-when-it-works">Example 1: When it works</h2>
<p>Following example, sets the promiscuous mode <strong>on</strong> using the <code class="language-plaintext highlighter-rouge">iproute2</code> and <code class="language-plaintext highlighter-rouge">netstat -i</code> command. You can verify it using the <code class="language-plaintext highlighter-rouge">iproute2</code> command and kernel logs.</p>

<p class="green_bold_text">Verify that promiscuous mode is not enabled.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>ip <span class="nb">link </span>show eth0
2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 
          qdisc pfifo_fast state UP qlen 1000
    <span class="nb">link</span>/ether 08:00:27:88:0c:a6 brd ff:ff:ff:ff:ff:ff

vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>netstat <span class="nt">-i</span> eth0
Kernel Interface table
Iface   MTU Met   RX-OK RX-ERR RX-DRP RX-OVR  TX-OK TX-ERR TX-DRP TX-OVR  Flg
eth0    1500 0    28880  0      0      0      17050    0      0      0   BMRU</code></pre></figure>

<p class="green_bold_text">Enable the promiscuous mode.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>ip <span class="nb">link set </span>eth0 promisc on</code></pre></figure>

<p class="green_bold_text">Check if promiscuous mode is enabled (see <code class="language-plaintext highlighter-rouge">PROMISC</code>) using <code class="language-plaintext highlighter-rouge">iproute2</code></p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>ip <span class="nb">link </span>show eth0
2: eth0: &lt;BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP&gt; mtu 1500 <span class="se">\</span>
          qdisc pfifo_fast state UP qlen 1000
    <span class="nb">link</span>/ether 08:00:27:88:0c:a6 brd ff:ff:ff:ff:ff:ff

vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>netstat <span class="nt">-i</span> eth0
Kernel Interface table
Iface   MTU Met   RX-OK RX-ERR RX-DRP RX-OVR  TX-OK TX-ERR TX-DRP TX-OVR  Flg
eth0    1500 0    28880  0      0      0      17050    0      0      0   BMPRU</code></pre></figure>

<p>Let’s check the kernel log messages, as logged in <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/core/dev.c#L4531">__dev_set_promiscuity</a> whenever a device is added/removed to/from promiscuous mode.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">vagrant@precise64:~<span class="nv">$ </span><span class="nb">grep</span> <span class="nt">-r</span> <span class="s1">'promiscuous'</span> /var/log/kern.log
precise64 kernel: <span class="o">[</span>44441.470885] device eth0 entered promiscuous mode</code></pre></figure>

<h2 id="example-2-when-it-doesnt-work">Example 2: When it doesn’t work</h2>

<p>Consider this for example, adding an interface to bridge; set the promiscuous mode on for that interface. Check out the post <a href="https://goyalankit.com/blog/linux-bridge">Linux Bridge - how it works</a> to learn more about bridge.</p>

<p>Following example creates a <strong>bridge</strong>, a <strong>veth</strong> pair and adds one end of the veth pair to bridge. According to <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_if.c#L355"><code class="language-plaintext highlighter-rouge">br_add_if</code></a>, promiscuous mode is turned on for the interface.</p>

<p class="green_bold_text">Create a new interface and add it to a bridge</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>brctl addbr br0
vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>ip <span class="nb">link </span>add veth0 <span class="nb">type </span>veth peer name vpeer0
vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>brctl addif br0 veth0</code></pre></figure>

<p class="green_bold_text">Check if promiscuous mode is enabled using <code class="language-plaintext highlighter-rouge">iproute2</code></p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>ip <span class="nt">-d</span> <span class="nb">link </span>show veth0
9: veth0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop master <span class="se">\</span>
 br0 state DOWN qlen 1000
    <span class="nb">link</span>/ether 12:30:e3:6b:42:2d brd ff:ff:ff:ff:ff:ff
    veth

vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>netstat <span class="nt">-ian</span> veth0
Kernel Interface table
Iface  MTU Met RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
veth0   1500 0   0      0      0 0             0      0      0      0 BM
vpeer0  1500 0   0      0      0 0             0      0      0      0 BM</code></pre></figure>

<p>It doesn’t show the device to be in promiscuous mode as <strong><code class="language-plaintext highlighter-rouge">PROMISC</code></strong> is not set and the flag <strong>P</strong> is not present in <strong>netstat</strong></p>

<p>Let’s check kernel logs and see if the device was actually put into promiscuous mode.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">vagrant@precise64:~<span class="nv">$ </span><span class="nb">grep</span> <span class="nt">-r</span> <span class="s1">'promiscuous'</span> /var/log/kern.log
precise64 kernel: <span class="o">[</span>43656.288050] device veth0 entered promiscuous mode</code></pre></figure>

<p>As expected, <strong>the device was in fact moved to promiscuous mode but <code class="language-plaintext highlighter-rouge">iproute2</code> doesn’t show it in promiscuous mode.</strong></p>

<h1 id="references">References</h1>

<ol>
  <li><a href="//lists.gt.net/linux/kernel/178148">https://lists.gt.net/linux/kernel/178148</a></li>
</ol>]]></content><author><name></name></author><category term="posts" /><category term="networks" /><category term="devices" /><summary type="html"><![CDATA[Wikipedia defines promiscuous mode as a mode for a wired network interface controller (NIC) or wireless network interface controller (WNIC) that causes the controller to pass all traffic it receives to the central processing unit (CPU)rather than passing only the frames that the controller is intended to receive.]]></summary></entry><entry><title type="html">Linux Bridge - how it works</title><link href="https://goyalankit.com/blog/linux-bridge" rel="alternate" type="text/html" title="Linux Bridge - how it works" /><published>2017-05-07T21:07:00+00:00</published><updated>2017-05-07T21:07:00+00:00</updated><id>https://goyalankit.com/blog/linux-bridge</id><content type="html" xml:base="https://goyalankit.com/blog/linux-bridge"><![CDATA[<p>Linux bridge is a layer 2 virtual device that on its own cannot receive or transmit anything unless you bind one or more real devices to it. <a href="//shop.oreilly.com/product/9780596002558.do">source</a>.</p>

<p>As <a href="https://wiki.aalto.fi/download/attachments/70789083/linux_bridging_final.pdf">Anatomy of a Linux bridge</a> puts it, bridge mainly consists of four major components:</p>

<ul>
  <li><strong>Set of network ports (or interfaces)</strong>: used to forward traffic between end switches to other hosts in the network.</li>
  <li><strong>A control plane</strong>: used to run Spanning Tree Protocol (STP) that calculates minimum spanning tree, preventing loops from crashing the network.</li>
  <li><strong>A forwarding plane</strong>: used to process incoming input frames from the ports, forward them to the network port by making a forwarding decision based on the MAC learning database.</li>
  <li><strong>MAC learning database</strong>: used to keep track of the host locations in the LAN.</li>
</ul>

<p>For each unicast mac address, bridge maintains a mac learning database to decide which ports to forward based on MAC addresses, and if it can’t find an entry for a given mac address, it will broadcast the frame to all ports except the one where it received the frame from.</p>

<p>There are three main configuration subsystems to do bridges:</p>
<ul>
  <li><strong>ioctl</strong>: This interface is used to create/destroy bridges and add/remove interfaces to/from a bridge.</li>
  <li><strong>sysfs</strong>: Management of bridge and port specific parameters.</li>
  <li><strong>netlink</strong>: Asynchronous queue based communication that uses <strong>AF_NETLINK</strong> address family, can also be used to interact with bridge.</li>
</ul>

<p>In this article, we only talk about <strong>ioctl</strong>.</p>

<h2 id="creating-a-bridge">Creating a bridge</h2>

<p>Bridge can be created using <code class="language-plaintext highlighter-rouge">ioctl</code> command <code class="language-plaintext highlighter-rouge">SIOCBRADDBR</code>; as can be seen by <code class="language-plaintext highlighter-rouge">brctl</code> utility provided by bridge-utils.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>strace brctl addbr br1
execve<span class="o">(</span><span class="s2">"/sbin/brctl"</span>, <span class="o">[</span><span class="s2">"brctl"</span>, <span class="s2">"addbr"</span>, <span class="s2">"br1"</span><span class="o">]</span>, <span class="o">[</span>/<span class="k">*</span> 16 vars <span class="k">*</span>/]<span class="o">)</span> <span class="o">=</span> 0
...
ioctl<span class="o">(</span>3, SIOCBRADDBR, 0x7fff2eae9966<span class="o">)</span>   <span class="o">=</span> 0
...</code></pre></figure>

<p>Note that there is no device at this point to handle the ioctl command, so the ioctl command is handled by a stub method: <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_ioctl.c#L351"><code class="language-plaintext highlighter-rouge">br_ioctl_deviceless_stub</code></a>, which in turn calls <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_if.c#L235"><code class="language-plaintext highlighter-rouge">br_add_bridge</code></a>. This method calls <code class="language-plaintext highlighter-rouge">alloc_netdev</code>, which is a macro that eventually calls <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/core/dev.c#L5660"><code class="language-plaintext highlighter-rouge">alloc_netdev_mqs</code></a>.</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="n">br_ioctl_deviceless_stub</span>
  <span class="o">|-</span> <span class="n">br_add_bridge</span>
      <span class="o">|-</span> <span class="n">alloc_netdev</span>
           <span class="o">|-</span> <span class="n">alloc_netdev_mqs</span>  <span class="c1">// creates the network device</span>
              <span class="o">|-</span> <span class="n">br_dev_setup</span> <span class="c1">// sets br_dev_ioctl handler               </span></code></pre></figure>

<p><code class="language-plaintext highlighter-rouge">alloc_netdev</code> also initializes the new netdevice using the <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_device.c#L335"><code class="language-plaintext highlighter-rouge">br_dev_setup</code></a>. This also includes setting up the bridge specific <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_ioctl.c#L379"><code class="language-plaintext highlighter-rouge">ioctl handler</code></a>. If you look at the handler code, it handles ioctl command to add/delete interfaces.</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="kt">int</span> <span class="nf">br_dev_ioctl</span><span class="p">(</span><span class="k">struct</span> <span class="n">net_device</span> <span class="o">*</span><span class="n">dev</span><span class="p">,</span> <span class="k">struct</span> <span class="n">ifreq</span> <span class="o">*</span><span class="n">rq</span><span class="p">,</span> <span class="kt">int</span> <span class="n">cmd</span><span class="p">)</span> <span class="p">{</span>
    <span class="p">...</span>
    <span class="k">switch</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">case</span> <span class="n">SIOCBRADDIF</span><span class="p">:</span>
	<span class="k">case</span> <span class="n">SIOCBRDELIF</span><span class="p">:</span>
		<span class="k">return</span> <span class="n">add_del_if</span><span class="p">(</span><span class="n">br</span><span class="p">,</span> <span class="n">rq</span><span class="o">-&gt;</span><span class="n">ifr_ifindex</span><span class="p">,</span> <span class="n">cmd</span> <span class="o">==</span> <span class="n">SIOCBRADDIF</span><span class="p">);</span>
    <span class="p">...</span>
    <span class="p">}</span>
    <span class="p">..</span>
<span class="p">}</span></code></pre></figure>

<h2 id="adding-an-interface">Adding an interface</h2>
<p>As it can be seen in <code class="language-plaintext highlighter-rouge">br_dev_ioctl</code>, bridge can be created using <code class="language-plaintext highlighter-rouge">ioctl</code> command <code class="language-plaintext highlighter-rouge">SIOCBRADDIF</code>. To confirm:</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">vagrant@precise64:~<span class="nv">$ </span><span class="nb">sudo </span>strace brctl addif br0 veth0
execve<span class="o">(</span><span class="s2">"/sbin/brctl"</span>, <span class="o">[</span><span class="s2">"brctl"</span>, <span class="s2">"addif"</span>, <span class="s2">"br0"</span>, <span class="s2">"veth0"</span><span class="o">]</span>, <span class="o">[</span>/<span class="k">*</span> 16 vars <span class="k">*</span>/]<span class="o">)</span> <span class="o">=</span> 0
...
<span class="c"># gets the index number of virtual ethernet device.</span>
ioctl<span class="o">(</span>4, SIOCGIFINDEX, <span class="o">{</span><span class="nv">ifr_name</span><span class="o">=</span><span class="s2">"veth0"</span>, <span class="nv">ifr_index</span><span class="o">=</span>5<span class="o">})</span> <span class="o">=</span> 0
close<span class="o">(</span>4<span class="o">)</span>
 <span class="c"># add the interface to bridge.</span>
ioctl<span class="o">(</span>3, SIOCBRADDIF, 0x7fff75bfe5f0<span class="o">)</span>   <span class="o">=</span> 0
...</code></pre></figure>

<p><strong><code class="language-plaintext highlighter-rouge">br_add_if</code></strong></p>

<p>The <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_if.c#L325"><code class="language-plaintext highlighter-rouge">br_add_if</code></a> method creates and sets up the new interface/port for the bridge by allocating a new <code class="language-plaintext highlighter-rouge">net_bridge_port</code> object. The object initialization is particularly interesting, as it sets the interface to receive all traffic, adds the network interface address for the new interface to the forwarding database as the local entry and attaches the interface as the slave to the bridge device.</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="cm">/* Truncated version */</span>
<span class="kt">int</span> <span class="nf">br_add_if</span><span class="p">(</span><span class="k">struct</span> <span class="n">net_bridge</span> <span class="o">*</span><span class="n">br</span><span class="p">,</span> <span class="k">struct</span> <span class="n">net_device</span> <span class="o">*</span><span class="n">dev</span><span class="p">)</span>
<span class="p">{</span>
	<span class="k">struct</span> <span class="n">net_bridge_port</span> <span class="o">*</span><span class="n">p</span><span class="p">;</span>
	<span class="cm">/* Don't allow bridging non-ethernet like devices */</span>
    <span class="p">...</span>
	<span class="cm">/* No bridging of bridges */</span>
    <span class="p">...</span>
	<span class="n">p</span> <span class="o">=</span> <span class="n">new_nbp</span><span class="p">(</span><span class="n">br</span><span class="p">,</span> <span class="n">dev</span><span class="p">);</span>
    <span class="p">...</span>
	<span class="n">call_netdevice_notifiers</span><span class="p">(</span><span class="n">NETDEV_JOIN</span><span class="p">,</span> <span class="n">dev</span><span class="p">);</span>
	<span class="n">err</span> <span class="o">=</span> <span class="n">dev_set_promiscuity</span><span class="p">(</span><span class="n">dev</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
	<span class="n">err</span> <span class="o">=</span> <span class="n">kobject_init_and_add</span><span class="p">(</span><span class="o">&amp;</span><span class="n">p</span><span class="o">-&gt;</span><span class="n">kobj</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">brport_ktype</span><span class="p">,</span> <span class="o">&amp;</span><span class="p">(</span><span class="n">dev</span><span class="o">-&gt;</span><span class="n">dev</span><span class="p">.</span><span class="n">kobj</span><span class="p">),</span>
				   <span class="n">SYSFS_BRIDGE_PORT_ATTR</span><span class="p">);</span>
    <span class="p">...</span>
	<span class="n">err</span> <span class="o">=</span> <span class="n">netdev_rx_handler_register</span><span class="p">(</span><span class="n">dev</span><span class="p">,</span> <span class="n">br_handle_frame</span><span class="p">,</span> <span class="n">p</span><span class="p">);</span>
    <span class="cm">/* Make entry in forwarding database*/</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">br_fdb_insert</span><span class="p">(</span><span class="n">br</span><span class="p">,</span> <span class="n">p</span><span class="p">,</span> <span class="n">dev</span><span class="o">-&gt;</span><span class="n">dev_addr</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span>
		<span class="p">...</span>
    <span class="p">...</span>
<span class="p">}</span></code></pre></figure>

<p>Some things worth noting in <code class="language-plaintext highlighter-rouge">br_add_if</code>:</p>

<ul>
  <li>Only ethernet like devices can be added to bridge, as bridge is a layer 2 device.</li>
  <li>Bridges cannot be added to a bridge.</li>
  <li>New interface is set to promiscuous mode: <code class="language-plaintext highlighter-rouge">dev_set_promiscuity(dev, 1)</code></li>
</ul>

<p>The promiscuous mode can be confirmed from kernel logs.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">vagrant@precise64:~<span class="nv">$ </span><span class="nb">grep</span> <span class="nt">-r</span> <span class="s1">'promiscuous'</span> /var/log/kern.log
precise64 kernel: <span class="o">[</span> 5185.751666] device veth0 entered promiscuous mode</code></pre></figure>

<p>Finally, <code class="language-plaintext highlighter-rouge">br_add_if</code> method calls <code class="language-plaintext highlighter-rouge">netdev_rx_handler_register</code>, that sets the <code class="language-plaintext highlighter-rouge">rx_handler</code> of the interface to <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_input.c#L153"><code class="language-plaintext highlighter-rouge">br_handle_frame</code></a></p>

<p>After this method finishes, you have an interface (or port) in bridge.</p>

<hr />

<h2 id="frame-processing">Frame Processing</h2>

<p class="image_size_600"><img src="https://gist.githubusercontent.com/goyalankit/6ea0ea8448ad1946e0791b308970a5d3/raw/cb59f23f7017edd47c075817aa2cdabb76bfa79d/frame_processing_bridge2.png" alt="bridge frame processing" />
Frame processing starts with device-independent network code, in <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/core/dev.c#L3579"><code class="language-plaintext highlighter-rouge">__netif_receive_skb</code></a> which calls the <code class="language-plaintext highlighter-rouge">rx_handler</code> of the interface, that was set to <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_input.c#L153"><code class="language-plaintext highlighter-rouge">br_handle_frame</code></a> at the time of adding the interface to bridge.</p>

<p>The <code class="language-plaintext highlighter-rouge">br_handle_frame</code> does the initial processing and any address with prefix <code class="language-plaintext highlighter-rouge">01-80-C2-00-00</code> is a control plane address, that may need special processing. From the comments in <code class="language-plaintext highlighter-rouge">br_handle_frame</code>:</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh">    /<span class="k">*</span>
    <span class="k">*</span> See IEEE 802.1D Table 7-10 Reserved addresses
    <span class="k">*</span>
    <span class="k">*</span> Assignment		 		Value
    <span class="k">*</span> Bridge Group Address		01-80-C2-00-00-00
    <span class="k">*</span> <span class="o">(</span>MAC Control<span class="o">)</span> 802.3		01-80-C2-00-00-01
    <span class="k">*</span> <span class="o">(</span>Link Aggregation<span class="o">)</span> 802.3	        01-80-C2-00-00-02
    <span class="k">*</span> 802.1X PAE address		01-80-C2-00-00-03
    <span class="k">*</span>
    <span class="k">*</span> 802.1AB LLDP 		01-80-C2-00-00-0E
    <span class="k">*</span>
    <span class="k">*</span> Others reserved <span class="k">for </span>future standardization
    <span class="k">*</span>/</code></pre></figure>

<p>In the method, note that stp messages are either passed to upper layers or forwarded if STP is enabled on the bridge or disabled respectively. Finally if a forwarding decision is made, the packet is passed to <code class="language-plaintext highlighter-rouge">br_handle_frame_finish</code>, where the actual forwarding happens.</p>

<p>Here’s the highly truncated version of <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_input.c#L60"><code class="language-plaintext highlighter-rouge">br_handle_frame_finish</code></a>:</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="cm">/* note: already called with rcu_read_lock */</span>
<span class="kt">int</span> <span class="nf">br_handle_frame_finish</span><span class="p">(</span><span class="k">struct</span> <span class="n">sk_buff</span> <span class="o">*</span><span class="n">skb</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">struct</span> <span class="n">net_bridge_port</span> <span class="o">*</span><span class="n">p</span> <span class="o">=</span> <span class="n">br_port_get_rcu</span><span class="p">(</span><span class="n">skb</span><span class="o">-&gt;</span><span class="n">dev</span><span class="p">);</span>
    <span class="p">...</span>
	<span class="cm">/* insert into forwarding database after filtering to avoid spoofing */</span>
	<span class="n">br</span> <span class="o">=</span> <span class="n">p</span><span class="o">-&gt;</span><span class="n">br</span><span class="p">;</span>
	<span class="n">br_fdb_update</span><span class="p">(</span><span class="n">br</span><span class="p">,</span> <span class="n">p</span><span class="p">,</span> <span class="n">eth_hdr</span><span class="p">(</span><span class="n">skb</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">h_source</span><span class="p">,</span> <span class="n">vid</span><span class="p">);</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">p</span><span class="o">-&gt;</span><span class="n">state</span> <span class="o">==</span> <span class="n">BR_STATE_LEARNING</span><span class="p">)</span>
		<span class="k">goto</span> <span class="n">drop</span><span class="p">;</span>
	<span class="cm">/* The packet skb2 goes to the local host (NULL to skip). */</span>
	<span class="n">skb2</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">br</span><span class="o">-&gt;</span><span class="n">dev</span><span class="o">-&gt;</span><span class="n">flags</span> <span class="o">&amp;</span> <span class="n">IFF_PROMISC</span><span class="p">)</span>
		<span class="n">skb2</span> <span class="o">=</span> <span class="n">skb</span><span class="p">;</span>
	<span class="n">dst</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">is_broadcast_ether_addr</span><span class="p">(</span><span class="n">dest</span><span class="p">))</span>
		<span class="n">skb2</span> <span class="o">=</span> <span class="n">skb</span><span class="p">;</span>
	<span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">is_multicast_ether_addr</span><span class="p">(</span><span class="n">dest</span><span class="p">))</span> <span class="p">{</span>
        <span class="p">...</span>
	<span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">((</span><span class="n">dst</span> <span class="o">=</span> <span class="n">__br_fdb_get</span><span class="p">(</span><span class="n">br</span><span class="p">,</span> <span class="n">dest</span><span class="p">,</span> <span class="n">vid</span><span class="p">))</span> <span class="o">&amp;&amp;</span>
			<span class="n">dst</span><span class="o">-&gt;</span><span class="n">is_local</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">skb2</span> <span class="o">=</span> <span class="n">skb</span><span class="p">;</span>
		<span class="cm">/* Do not forward the packet since it's local. */</span>
		<span class="n">skb</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">skb</span><span class="p">)</span> <span class="p">{</span>
		<span class="k">if</span> <span class="p">(</span><span class="n">dst</span><span class="p">)</span> <span class="p">{</span>
			<span class="n">br_forward</span><span class="p">(</span><span class="n">dst</span><span class="o">-&gt;</span><span class="n">dst</span><span class="p">,</span> <span class="n">skb</span><span class="p">,</span> <span class="n">skb2</span><span class="p">);</span>
		<span class="p">}</span> <span class="k">else</span>
			<span class="n">br_flood_forward</span><span class="p">(</span><span class="n">br</span><span class="p">,</span> <span class="n">skb</span><span class="p">,</span> <span class="n">skb2</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">skb2</span><span class="p">)</span>
		<span class="k">return</span> <span class="n">br_pass_frame_up</span><span class="p">(</span><span class="n">skb2</span><span class="p">);</span>
<span class="nl">out:</span>
	<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">...</span>
<span class="p">}</span></code></pre></figure>

<p>As you can see in above snippet of <code class="language-plaintext highlighter-rouge">br_handle_frame_finish</code>,</p>
<ul>
  <li>An entry in forwarding database is updated for the source of the frame.</li>
  <li>(not in the above snippet) If the destination address is a multicast address, and if the multicast is disabled, the packet is dropped. Or else message is received using <code class="language-plaintext highlighter-rouge">br_multicast_rcv</code></li>
  <li>Now if the promiscuous mode is on, packet will be delivered locally, irrespective of the destination.</li>
  <li>For a unicast address, we try to determine the port using the forwarding database (<code class="language-plaintext highlighter-rouge">__br_fdb_get</code>).</li>
  <li>If the destination is local, then <code class="language-plaintext highlighter-rouge">skb</code> is set to null i.e., packet will not be forwarded.</li>
  <li>If the destination is not local, then based on if we found an entry in forwarding database, either the frame is forwarded (<code class="language-plaintext highlighter-rouge">br_forward</code>) or flooded to all ports (<code class="language-plaintext highlighter-rouge">br_flood_forward</code>).</li>
  <li>Later, packet is delivered locally (<code class="language-plaintext highlighter-rouge">br_pass_frame_up</code>) if needed (based on either the current host being the destination or the net device being in promiscuous mode).</li>
</ul>

<p><a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_forward.c#L120"><code class="language-plaintext highlighter-rouge">br_forward</code></a> method either clones and then deliver (if it is also to be delivered locally, by calling <code class="language-plaintext highlighter-rouge">deliver_clone</code>), or directly forwards the message to the intended destination interface by calling <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_forward.c#L87"><code class="language-plaintext highlighter-rouge">__br_forward</code></a>.</p>

<p><a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_forward.c#L212"><code class="language-plaintext highlighter-rouge">bt_flood_forward</code></a> forwards the frame on each interface by iterating through the list in <a href="//elixir.free-electrons.com/linux/v3.10.105/source/net/bridge/br_forward.c#L212"><code class="language-plaintext highlighter-rouge">br_flood</code></a> method.</p>

<h1 id="conclusion">Conclusion</h1>

<p>Bridges can be used to create various different network topologies and it’s important to understand how they work. I have seen bridges being used with containers where they are used to provide networking in network namespaces along with <code class="language-plaintext highlighter-rouge">veth</code> devices. In fact the default networking in docker is provided using <a href="https://docs.docker.com/engine/userguide/networking/#default-networks">bridge</a>.</p>

<p>This is all for now, hopefully this was useful. This was mainly based on the excellent paper <a href="https://wiki.aalto.fi/download/attachments/70789083/linux_bridging_final.pdf">Anatomy of a Linux bridge</a> and my own reading of linux kernel code. I’d appreciate any feedback, or comments.</p>

<hr />

<p class="image_size_600">Ah, the wonderful world of bridges.</p>
<p><img src="https://gist.githubusercontent.com/goyalankit/6ea0ea8448ad1946e0791b308970a5d3/raw/bfa531da40571bdd72c21b752abb6768d346cb70/sf_bridge.jpg" alt="Bridge" /></p>

<hr />

<p><strong>Update July 12, 2017</strong>: Added third way to communicate with bridge. <em>Thanks to @vbernat</em> from comments.</p>

<h2 id="references">References:</h2>
<p><a href="//goyalankit.com/blog/linux-bridge-notes">Handwritten notes</a></p>

<p>[1] <a href="https://wiki.aalto.fi/download/attachments/70789083/linux_bridging_final.pdf">Anatomy of a Linux bridge</a><br />
[2] <a href="//shop.oreilly.com/product/9780596002558.do">Understanding Linux Networking Internals - Christian Benvenuti</a><br />
[3] <a href="//elixir.free-electrons.com/linux/v3.10.105/source">Linux kernel v3.10.105 source code</a></p>]]></content><author><name></name></author><category term="posts" /><category term="bridge" /><category term="networks" /><summary type="html"><![CDATA[Linux bridge is a layer 2 virtual device that on its own cannot receive or transmit anything unless you bind one or more real devices to it. source.]]></summary></entry><entry><title type="html">Linux Bridge</title><link href="https://goyalankit.com/blog/linux-bridge-notes" rel="alternate" type="text/html" title="Linux Bridge" /><published>2017-05-01T00:07:00+00:00</published><updated>2017-05-01T00:07:00+00:00</updated><id>https://goyalankit.com/blog/linux-bridge-notes</id><content type="html" xml:base="https://goyalankit.com/blog/linux-bridge-notes"><![CDATA[<p><a href="//goyalankit.com/blog/linux-bridge">Link to blog post for Linux Bridges</a></p>

<style>
.os_concepts_left img {
  width: 300px;
  float: left;
}
.os_concepts_right img {
  width: 300px;
}
.move_to_left  {
  clear: left;
  display:hidden;
}

</style>

<p class="os_concepts_left"><img src="https://gist.githubusercontent.com/goyalankit/638e39904d46ebe2ef81174a808e9ada/raw/8ac18d44ebaf4b1c6e38d1838c1d3778c42a140a/IMG_2186.JPG" alt="Linux Bridge 1" /></p>

<p class="os_concepts_right"><img src="https://gist.githubusercontent.com/goyalankit/638e39904d46ebe2ef81174a808e9ada/raw/8ac18d44ebaf4b1c6e38d1838c1d3778c42a140a/IMG_3258.JPG" alt="Linux Bridge 1" /></p>

<p class="os_concepts_left"><img src="https://gist.githubusercontent.com/goyalankit/638e39904d46ebe2ef81174a808e9ada/raw/8ac18d44ebaf4b1c6e38d1838c1d3778c42a140a/IMG_0057.JPG" alt="Linux Bridge 1" /></p>

<p class="os_concepts_right"><img src="https://gist.githubusercontent.com/goyalankit/638e39904d46ebe2ef81174a808e9ada/raw/7a63612e4720fda2ef9cbf60c184a5bb16f6333c/IMG_9286.JPG" alt="Linux Bridge 1" /></p>

<div class="move_to_left"> </div>

<h2 id="references">References:</h2>
<p>[1] <a href="https://wiki.aalto.fi/download/attachments/70789083/linux_bridging_final.pdf">Anatomy of a Linux bridge</a><br />
[2] <a href="//shop.oreilly.com/product/9780596002558.do">Understanding Linux Networking Internals - Christian Benvenuti</a><br /></p>]]></content><author><name></name></author><category term="notes" /><category term="bridge" /><category term="networks" /><summary type="html"><![CDATA[Link to blog post for Linux Bridges]]></summary></entry><entry><title type="html">IPTables - You Shall Not Pass</title><link href="https://goyalankit.com/blog/iptables" rel="alternate" type="text/html" title="IPTables - You Shall Not Pass" /><published>2017-04-30T23:11:00+00:00</published><updated>2017-04-30T23:11:00+00:00</updated><id>https://goyalankit.com/blog/iptables</id><content type="html" xml:base="https://goyalankit.com/blog/iptables"><![CDATA[<p>I have been lately working with networks and had to use <strong>iptables</strong> at several instances. During the process I dug into iptables a little bit and so documenting my learnings here.</p>

<h1 id="netfilter-and-iptables">Netfilter and IPTables</h1>

<p><strong>Netfilter</strong> provides a set of hooks inside Linux kernel networking stack which allows one to take action at different phases in routing process.</p>

<p><code class="language-plaintext highlighter-rouge">iptables</code> is a userland tool to interact with netfilter in kernel. <em>IPTables</em> and <em>netfilter</em> together facilitate in enforcing different packet-filtering rules and setting up a basic firewall.</p>

<p>To check if you have iptables enabled:</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>lsmod | <span class="nb">grep </span>ip_tables
ip_tables              27115  3 iptable_filter,iptable_mangle,iptable_nat</code></pre></figure>

<h2 id="packet-traversal-tables-and-chains">Packet Traversal, Tables and Chains</h2>

<p>A packet goes through several different routines and there are hooks available that allow you to modify the behavior.</p>

<p>There are primarily 3 different tables which allow you to define rules and where those rules get applied depends on the chain.</p>

<p>Consider the following diagram showing packet traversal flow:</p>

<p><img src="https://cloud.githubusercontent.com/assets/1711674/8742358/87ee94aa-2c32-11e5-84b7-4819a676129a.gif" alt="Packet Traversal Diagram" /></p>

<p>According to above diagram, a packet goes through following chains:</p>
<ol>
  <li><strong>PREROUTING</strong>: A packet goes through this chain before any routing decision is made.</li>
  <li><strong>FORWARD</strong>: If the message is not intended for local machine; it can be either dropped or routed to other machine based on the policy.</li>
  <li><strong>INPUT</strong>: If the packet is targeted for local machine, it passes through the INPUT chain.</li>
  <li><strong>OUTPUT</strong>: Any packet originating from local machine goes through the OUTPUT chain.</li>
  <li><strong>POSTROUTING</strong>: All outbound packet goes through post routing state.</li>
</ol>

<p><br />
Let’s go though the packet flow. When a new packet arrives:</p>
<ul>
  <li>It first goes through the <strong>PREROUTING</strong> chain; where you can change the TCP headers, setup nat rules, etc; before passing it on for the Routing. Routing decides if the packet is targeted for current system based on IP headers.</li>
  <li>If it is not meant for the current system, packet passes through the <strong>FORWARD</strong> chain where one can decide to either forward the packet or drop it based on the set policy. If the forwarding is allowed, the packet is passed on to the <strong>POSTROUTING</strong> chain, otherwise it’s dropped or rejected.</li>
  <li>If the packet is meant for the current system, it is passed to the <strong>INPUT</strong> chain where packets can be filtered, header can be modified, etc and once rules in this chain are applied, packet is passed on to the local machine for processing.</li>
  <li>Any new packet from local machine passes through <strong>OUTPUT</strong> chain, where packet can be modified before the routing decision is made. Once the routing decision is made, packet goes through the <strong>POSTROUTING</strong> chain.</li>
  <li><strong>POSTROUTING</strong> change can modify the headers, filter the packets before packet is finally put onto the network.</li>
</ul>

<style>
.imptnt strong {
  color: rgba(189, 117, 1, 0.89);
}
</style>

<p>I loosely talked about filters, modifying headers during the traversal flow of packet. To make it more concrete, let’s look at what tables are available and how can they be used. As mentioned above, there are primarily 3 tables available where you can define you rules and these rules are executed at different chains. <em class="imptnt">These three tables are: <strong>nat</strong>, <strong>mangle</strong> and <strong>filter</strong>.</em></p>

<p>Not all tables are available in all chains, as you can see <strong>filter</strong> table is not available in <strong>PREROUTING</strong> and <strong>POSTROUTING</strong> chains.</p>

<p>So a rules needs to have a chain and a table. For example, an iptable rule looks something like this:</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nb">sudo </span>iptables <span class="nt">-t</span> nat <span class="nt">-A</span> PREROUTING <span class="nt">-p</span> tcp <span class="nt">--dport</span> 22 <span class="nt">-j</span> DNAT <span class="se">\</span>
    <span class="nt">--to-destination</span> 172.22.212.30:222</code></pre></figure>

<p>where table is <strong>nat</strong> and chain is <strong>PREROUTING</strong>.</p>

<p>The 3 tables (<code class="language-plaintext highlighter-rouge">-t</code> flag) and their functions:</p>
<ol>
  <li><strong>filter</strong>: This table, as the name suggests, filters the packets based on the rules. This is the default table and will be used if a table is not specified. It is only present in INPUT, OUTPUT and FORWARD chain. Based on target value, packets can be dropped, accepted, and returned.</li>
  <li><strong>nat</strong>: This allows altering the packets destination or source address. This is normally used to modify source and destination headers to allow internal ips to talk to outside networks. Normal use-case involves changing the destination IP address of the packet, also known as DNAT, in the PREROUTING chain to that of local machine so that routing can route it to INPUT chain. And conversely, changing the source address of the packet, also known as SNAT, in the POSTROUTING chain to that of the router or an ip known to destination routing.</li>
  <li><strong>mangle</strong>: Mangle can be used to modify certain properties of the packet like TOS, TTL, MTU, etc.</li>
</ol>

<h1 id="targets-and-jumps--j-flag">Targets and Jumps (<code class="language-plaintext highlighter-rouge">-j</code> flag)</h1>
<p>Each rule has a predicate and a possible action which is called a target. For a detailed list of possible target and jump values, refer: <a href="//www.iptables.info/en/iptables-targets-and-jumps.html">iptables - targets and Jumps</a>. Some of the most common ones are:</p>

<ol>
  <li><strong>ACCEPT</strong>: This can be used to allow traffic based on the other specifications in the rule.</li>
  <li><strong>DROP</strong>: This can be used to drop the messages. No further processing is done; and no further information will be sent back to sender.</li>
  <li><strong>REJECT</strong>: This is similar to DROP but it also sends back an error message back to host sending the packet.</li>
  <li><strong>SNAT</strong>: It allows one to modify source ip address in the packet. It’s only valid in POSTROUTING chain and nat table.</li>
  <li><strong>DNAT</strong>: It allows one to modify destination ip address in the packet. It’s only valid in PREROUTING chaing and nat table.</li>
  <li><strong>MASQUERADE</strong>: This is same as SNAT but it doesn’t require a <code class="language-plaintext highlighter-rouge">--to-source</code> option. I.e., this is preferable if you don’t know the external ip during the rule creation, as it may be retrieved later using DHCP.</li>
</ol>

<h2 id="some-cli-examples">Some CLI examples</h2>
<p>This post was an attempt to understand basics of <code class="language-plaintext highlighter-rouge">iptables</code>, rather than a specific example. There are some good examples online like this <a href="https://www.karlrupp.net/en/computer/nat_tutorial">nat tutorial</a></p>

<p><strong>To drop traffic from a certain IP address</strong></p>

<p>We can use filter table (which is the default) and add the rule in the INPUT chain.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="c"># To create the rule</span>
<span class="nv">$ </span><span class="nb">sudo </span>iptables <span class="nt">-A</span> INPUT <span class="nt">-s</span> 192.12.33.12 <span class="nt">-j</span> REJECT

<span class="c"># To list the rule</span>
<span class="nv">$ </span><span class="nb">sudo </span>iptables <span class="nt">-L</span> OUTPUT <span class="nt">-n</span>
Chain OUTPUT <span class="o">(</span>policy ACCEPT<span class="o">)</span>
num  target     prot opt <span class="nb">source               </span>destination
1    REJECT     all  <span class="nt">--</span>  0.0.0.0/0            192.12.33.12       <span class="se">\</span>
                                     reject-with icmp-port-unreachable

<span class="c"># To delete the rule</span>
<span class="nv">$ </span><span class="nb">sudo </span>iptables <span class="nt">-D</span> OUTPUT <span class="nt">-1</span></code></pre></figure>

<p><strong>To drop all traffic to a certain IP address</strong></p>

<p>We can use filter table (which is the default) and add the following rule in the OUTPUT chain.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nb">sudo </span>iptables <span class="nt">-t</span> filter <span class="nt">-d</span> 192.12.33.12 <span class="nt">-A</span> OUTPUT <span class="nt">-j</span> REJECT</code></pre></figure>

<p>For more complex examples, please refer to this: <a href="https://www.karlrupp.net/en/computer/nat_tutorial">nat tutorial</a></p>

<p>This is it for now.</p>

<h2 id="references">References:</h2>
<p>[1] <a href="//www.linuxjournal.com/article/4815">Taming the Wild Netfilter - linuxjournal.com</a><br />
[2] <a href="https://www.netfilter.org/documentation/HOWTO/netfilter-hacking-HOWTO-3.html">Netfilter Architecture - netfilter.org</a><br /></p>]]></content><author><name></name></author><category term="posts" /><category term="iptables" /><category term="networks" /><summary type="html"><![CDATA[I have been lately working with networks and had to use iptables at several instances. During the process I dug into iptables a little bit and so documenting my learnings here.]]></summary></entry><entry><title type="html">XINETD - Extended Internet Daemon</title><link href="https://goyalankit.com/blog/xinetd" rel="alternate" type="text/html" title="XINETD - Extended Internet Daemon" /><published>2017-04-16T20:25:00+00:00</published><updated>2017-04-16T20:25:00+00:00</updated><id>https://goyalankit.com/blog/xinetd</id><content type="html" xml:base="https://goyalankit.com/blog/xinetd"><![CDATA[<p>I was recently looking into ways to provide ssh access inside linux network namespaces and
came across <strong>xinetd</strong>. So I decided to dig more into it. Noting it down here so that I can
refer it back.</p>

<h1 id="xinetd">XINETD</h1>

<p>It’s basically a daemon that listens for network requests and services them by spawning more processes.</p>

<p>The master configuration for <strong>xinetd</strong> lives in <code class="language-plaintext highlighter-rouge">/etc/xinetd.conf</code>. Each service managed
by xinetd has a configuration file in <code class="language-plaintext highlighter-rouge">/etc/xinetd.d/</code>.</p>

<p>Each network service is listed in <code class="language-plaintext highlighter-rouge">/etc/services</code> that xinetd could potentially manage.</p>

<p>Let’s look at an example from one of the services in <code class="language-plaintext highlighter-rouge">/etc/xinetd.d/</code> to see how it works:</p>

<h1 id="an-echo-service">An echo service</h1>

<p>This was a default service that was present on my RHEL6 box. There were lots of
settings in this file which were basically commented out. Most of them are self
explanatory, so I have omitted them for brevity.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nv">$ </span><span class="nb">sudo cat</span> /etc/xinetd.d/echo-stream
<span class="c"># This is the configuration for the tcp/stream echo service.</span>

service <span class="nb">echo</span>
<span class="o">{</span>
<span class="c"># This is for quick on or off of the service</span>
	disable		<span class="o">=</span> <span class="nb">yes</span>

<span class="c"># The next attributes are mandatory for all services</span>
	<span class="nb">id</span>		<span class="o">=</span> echo-stream
	<span class="nb">type</span>		<span class="o">=</span> INTERNAL
	<span class="nb">wait</span>		<span class="o">=</span> no
	socket_type	<span class="o">=</span> stream
<span class="c">#	protocol	=  socket type is usually enough</span>

<span class="o">}</span></code></pre></figure>

<p><strong>echo</strong> service simply provides an echo service (duh). But what port does it listen to?
The port can be checked in <code class="language-plaintext highlighter-rouge">/etc/services</code> file, search for echo in file, and on my machine
it had an entry that looked like this:</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nv">$ </span><span class="nb">sudo cat</span> /etc/services | <span class="nb">grep echo
echo            </span>7/tcp</code></pre></figure>

<p>If you try to connect to this port; the connection will fail since the <strong>disabled</strong> flag
is set to <strong>yes</strong> in the above configuration file.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nv">$ </span>telnet 172.22.210.126 7
Trying 172.22.210.126...
telnet: connect to address 172.22.210.126: Connection refused
telnet: Unable to connect to remote host</code></pre></figure>

<p>Let’s enable the service by setting <code class="language-plaintext highlighter-rouge">disable = no</code> in <code class="language-plaintext highlighter-rouge">/etc/xinetd.d/echo-stream</code>.
In addition, you’d need to restart the xinetd service.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nv">$ </span><span class="nb">sudo </span>service xinetd restart
Stopping xinetd:                                           <span class="o">[</span>  OK  <span class="o">]</span>
Starting xinetd:                                           <span class="o">[</span>  OK  <span class="o">]</span></code></pre></figure>

<p>Now again, let’s try to connect to service.</p>

<figure class="highlight"><pre><code class="language-sh" data-lang="sh"><span class="nv">$ </span>telnet 172.22.210.126 7
Trying 172.22.210.126...
Connected to angoyal-ld2.linkedin.biz.
Escape character is <span class="s1">'^]'</span><span class="nb">.</span>
hola  &lt;<span class="nt">----</span> I said hola to Server.
hola  <span class="nt">----</span><span class="o">&gt;</span> Server said hola back.
^]
telnet&gt; q
Connection closed.</code></pre></figure>

<p>Sweet.</p>

<p>You can use xinetd to run your own network service and have full control. I have some ideas which
I’ll document if they work.</p>

<p>So long.</p>]]></content><author><name></name></author><category term="posts" /><category term="xinetd" /><category term="networks" /><summary type="html"><![CDATA[I was recently looking into ways to provide ssh access inside linux network namespaces and came across xinetd. So I decided to dig more into it. Noting it down here so that I can refer it back.]]></summary></entry></feed>