<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Andrei Kazialetski — AVKSoft</title>
    <subtitle>Software engineering, product development, and technical writing by Andrei Kazialetski.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://iamavk.info/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://iamavk.info"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-10-15T00:00:00+00:00</updated>
    <id>https://iamavk.info/atom.xml</id>
    <entry xml:lang="en">
        <title>Spec-Driven AI Development: Building bgpd-simple with Spec Kit and Local AI Coding Agents</title>
        <published>2026-10-15T00:00:00+00:00</published>
        <updated>2026-10-15T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/"/>
        <id>https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/</id>
        
        <content type="html" xml:base="https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/">&lt;p&gt;AI coding becomes much more useful when the model is not asked to invent both the requirements and the implementation at the same time.&lt;/p&gt;
&lt;p&gt;That was one of the main lessons from working on &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/bgpd-simple&quot;&gt;&lt;code&gt;bgpd-simple&lt;/code&gt;&lt;/a&gt;, a compact routing daemon written in C with BGP, OSPF, and MPLS/LDP support. The project is small enough to understand, but protocol work is unforgiving: a change can look perfectly reasonable in C and still be wrong because it violates one sentence in an RFC, serializes one field incorrectly, or breaks a state transition that only appears during reconnect or withdrawal.&lt;/p&gt;
&lt;p&gt;For this project I used &lt;strong&gt;Spec Kit&lt;/strong&gt; as the workflow layer, &lt;strong&gt;OpenCode&lt;/strong&gt; as the coding agent, and &lt;strong&gt;local models served by llama.cpp&lt;/strong&gt;. I started with &lt;strong&gt;Qwen 3.5&lt;/strong&gt; and &lt;strong&gt;GLM-4.7&lt;/strong&gt;, then experimented with newer models, including &lt;strong&gt;Qwen 3.6 27B&lt;/strong&gt;. The important part was not the particular model. The important part was moving as much engineering truth as possible out of the prompt and into versioned artifacts: specifications, RFC references, plans, task lists, tests, compliance matrices, and verification reports.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/sdd-workflow.svg&quot; alt=&quot;Spec-driven development loop&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;why-a-routing-daemon-is-a-good-sdd-test-case&quot;&gt;Why a routing daemon is a good SDD test case&lt;/h2&gt;
&lt;p&gt;A routing daemon is almost the opposite of a “vibe coding” application.&lt;/p&gt;
&lt;p&gt;The external behavior is already specified. BGP is defined by documents such as &lt;a rel=&quot;external&quot; href=&quot;https://datatracker.ietf.org/doc/rfc4271/&quot;&gt;RFC 4271&lt;/a&gt;, multiprotocol BGP by &lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc4760.html&quot;&gt;RFC 4760&lt;/a&gt;, OSPFv2 by &lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc2328.html&quot;&gt;RFC 2328&lt;/a&gt;, OSPFv3 by &lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc5340.html&quot;&gt;RFC 5340&lt;/a&gt;, and LDP by &lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc5036.html&quot;&gt;RFC 5036&lt;/a&gt;. The implementation may be original, but the observable protocol behavior cannot be arbitrary.&lt;/p&gt;
&lt;p&gt;That makes the project a useful environment for spec-driven development because there are several independent sources of truth:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;protocol RFCs;&lt;/li&gt;
&lt;li&gt;the project configuration and CLI vocabulary;&lt;/li&gt;
&lt;li&gt;internal architecture and invariants;&lt;/li&gt;
&lt;li&gt;regression tests;&lt;/li&gt;
&lt;li&gt;interoperability behavior;&lt;/li&gt;
&lt;li&gt;explicit release/compliance gates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The repository reflects this. Documentation is split into protocol domains and lifecycle-oriented material: BGP, OSPF, MPLS/LDP, shared architecture, troubleshooting, compliance matrices, and project reports. The release documentation also requires compliance claims to point to executable evidence rather than to prose alone.&lt;/p&gt;
&lt;p&gt;In other words, the specification is not one large Markdown file. It is a graph of constraints.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/evidence-loop.svg&quot; alt=&quot;Specification and evidence graph&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-stack-spec-kit-opencode-or-claude-code-llama-cpp-or-lm-studio&quot;&gt;The stack: Spec Kit, OpenCode or Claude Code, llama.cpp or LM Studio&lt;/h2&gt;
&lt;p&gt;The roles were deliberately separated. The primary configuration below is &lt;strong&gt;OpenCode + llama.cpp&lt;/strong&gt;; &lt;strong&gt;Claude Code&lt;/strong&gt; is an alternative coding agent, while &lt;strong&gt;LM Studio&lt;/strong&gt; is an alternative local inference server. These are interchangeable components, not a claim that every combination was used for every bgpd-simple commit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spec Kit&lt;/strong&gt; provided the structured development workflow. It turns an idea into artifacts such as a specification, technical plan, task list, and implementation/convergence steps. &lt;strong&gt;OpenCode&lt;/strong&gt; acted as the coding driver: reading the repository, editing files, running builds and tests, and iterating on failures. &lt;strong&gt;llama.cpp&lt;/strong&gt; supplied an OpenAI-compatible local HTTP endpoint. &lt;strong&gt;Qwen 3.5&lt;/strong&gt; and &lt;strong&gt;GLM-4.7&lt;/strong&gt; were my starting points, followed by experiments with newer models, including &lt;strong&gt;Qwen 3.6 27B&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/tooling-stack.svg&quot; alt=&quot;Interchangeable agents and local inference servers&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This distinction matters. Spec Kit is not the model and it is not the coding agent. It is the process layer. OpenCode &lt;strong&gt;or Claude Code&lt;/strong&gt; drives repository edits and tests; llama.cpp &lt;strong&gt;or LM Studio&lt;/strong&gt; serves a model locally. The model is replaceable as long as it can follow the process, reason over the repository, and use tools reliably.&lt;/p&gt;
&lt;p&gt;Current Spec Kit releases list &lt;code&gt;opencode&lt;/code&gt; as a supported integration and install OpenCode command files under &lt;code&gt;.opencode/commands&lt;/code&gt;. Current documentation often shows skill names such as &lt;code&gt;/speckit-specify&lt;/code&gt;, &lt;code&gt;/speckit-plan&lt;/code&gt;, and &lt;code&gt;/speckit-tasks&lt;/code&gt;. During this work I used the dotted command form commonly seen in earlier Spec Kit workflows — &lt;code&gt;/speckit.specify&lt;/code&gt;, &lt;code&gt;/speckit.plan&lt;/code&gt;, and &lt;code&gt;/speckit.tasks&lt;/code&gt;. The exact slash-command spelling is version/integration dependent; the workflow boundary is the important part.&lt;/p&gt;
&lt;h2 id=&quot;from-qwen-3-5-and-glm-4-7-to-newer-local-models&quot;&gt;From Qwen 3.5 and GLM-4.7 to newer local models&lt;/h2&gt;
&lt;p&gt;I did not start this project with Qwen 3.6 27B. My early experiments used &lt;strong&gt;Qwen 3.5&lt;/strong&gt; and &lt;strong&gt;GLM-4.7&lt;/strong&gt;. As newer model releases became available, I tried them with the same general setup: a local &lt;code&gt;llama.cpp&lt;/code&gt; inference server exposed through OpenCode, with Spec Kit supplying the development workflow. &lt;strong&gt;Qwen 3.6 27B&lt;/strong&gt; is one of the newer configurations discussed below, not the only model used over the lifetime of the project.&lt;/p&gt;
&lt;p&gt;This model progression is useful context for the SDD approach. Rather than tie protocol requirements to a particular model or chat history, I could keep the RFC references, plans, test commands, and acceptance checks in the repository while changing the inference backend. I am not presenting controlled model benchmarks here: the important observation is the portability of the &lt;em&gt;workflow&lt;/em&gt;, not an unmeasured claim that one model outperformed another.&lt;/p&gt;
&lt;h3 id=&quot;the-hardware-behind-the-local-experiments&quot;&gt;The hardware behind the local experiments&lt;/h3&gt;
&lt;p&gt;My &lt;strong&gt;main setup&lt;/strong&gt; was a server/workstation built around a &lt;strong&gt;Supermicro H11SSL-C motherboard&lt;/strong&gt;, an &lt;strong&gt;AMD EPYC 7551 processor&lt;/strong&gt;, and &lt;strong&gt;256 GB RAM&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For an &lt;strong&gt;alternative setup&lt;/strong&gt;, I used a &lt;strong&gt;Mougol X99&lt;/strong&gt; motherboard and later moved to a &lt;strong&gt;Machinist X99 D8 Max&lt;/strong&gt; board, with &lt;strong&gt;two Intel Xeon E5-2680 processors&lt;/strong&gt; and &lt;strong&gt;64 GB RAM&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The GPUs available for the experiments were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PNY GeForce RTX 5060 Ti OC — 16 GB VRAM&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gainward GeForce RTX 5070 Ti Phoenix-S — 16 GB VRAM&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/local-ai-hardware.svg&quot; alt=&quot;Local AI development hardware: main EPYC server, alternative dual-Xeon X99 machine, and GPUs&quot; /&gt;&lt;/p&gt;
&lt;p&gt;On the &lt;strong&gt;Machinist X99 D8 Max&lt;/strong&gt;, PCIe/NUMA topology matters when distributing inference work across GPUs. PCIe slots may be attached to different CPU sockets, and communication between GPUs on opposite sockets can cross the CPU-to-CPU interconnect. In my setup, that became a bottleneck. &lt;strong&gt;For multi-GPU workloads with frequent cross-GPU transfers, I place both cards in PCIe slots attached to the same CPU&lt;/strong&gt; whenever the board&#39;s slot wiring, physical clearance, and available PCIe lanes permit it. This avoids the extra inter-socket hop; it does not eliminate normal PCIe transfer costs. Check the actual mapping with &lt;code&gt;lspci -tv&lt;/code&gt; and &lt;code&gt;nvidia-smi topo -m&lt;/code&gt; rather than assuming that adjacent slots share a CPU. The best choice still depends on the model split and the transfer pattern.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/dual-socket-gpu-topology.svg&quot; alt=&quot;Dual-socket PCIe topology: GPU placement across CPUs versus within one CPU&amp;#39;s PCIe domain&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;H11SSL-C&#39;s revision supports EPYC 7002 processors&lt;/strong&gt;, so an &lt;strong&gt;EPYC 7502&lt;/strong&gt; upgrade would also be possible. That upgrade deserves a separate article; the experiments described here used the &lt;strong&gt;EPYC 7551&lt;/strong&gt;. See &lt;a rel=&quot;external&quot; href=&quot;https://www.supermicro.com/en/support/faqs/faq.php?faq=30897&quot;&gt;Supermicro&#39;s official EPYC 7002 support FAQ&lt;/a&gt; for the revision requirement.&lt;/p&gt;
&lt;p&gt;For models that do not fit entirely into a single 16 GB VRAM allocation, &lt;code&gt;llama.cpp&lt;/code&gt; can use quantized GGUF weights and appropriate CPU/GPU offloading; the practical fit and speed depend on the quantization, available RAM, context length, and the actual GPU configuration. The hardware list above describes my test equipment, not a claim that both GPUs were always installed together or that every model ran fully in VRAM.&lt;/p&gt;
&lt;p&gt;This is why a local OpenAI-compatible endpoint was so valuable: OpenCode&#39;s connection remained broadly the same even as I changed machines, models, and inference settings.&lt;/p&gt;
&lt;h2 id=&quot;adopting-spec-kit-in-an-existing-repository&quot;&gt;Adopting Spec Kit in an existing repository&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;bgpd-simple&lt;/code&gt; was not a greenfield “generate an app” exercise. For an existing repository, I prefer to initialize Spec Kit in place from a clean, reviewable Git state:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;uv tool install specify-cli

cd bgpd-simple
git status

specify init --here --force --integration opencode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a repository that already matters, &lt;code&gt;--force&lt;/code&gt; should not mean “overwrite without looking.” It means the generated integration files are applied to an existing tree, so I want the pre-init state committed or stashed and I review the diff immediately afterward.&lt;/p&gt;
&lt;p&gt;The resulting separation is useful:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;.specify/              shared Spec Kit workflow data
.opencode/commands/    OpenCode integration commands
specs/                 feature specifications / plans / tasks
src/, include/         implementation
tests/                 executable behavior
docs/                  architecture, RFC compliance, reports
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For protocol work I also keep project-level rules explicit. The repository&#39;s &lt;code&gt;docs/meta/WO.md&lt;/code&gt; captures several of them: plan first for non-trivial work, minimize the impact of changes, do not accept temporary fixes when the root cause is knowable, and never mark a task done without verification. Those rules are exactly the kind of constraints I want an agent to inherit before it starts editing C.&lt;/p&gt;
&lt;h2 id=&quot;a-practical-spec-kit-workflow&quot;&gt;A practical Spec Kit workflow&lt;/h2&gt;
&lt;p&gt;For a non-trivial protocol change, my workflow is roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;constitution / project rules
        ↓
specify the externally visible behavior
        ↓
clarify ambiguous protocol or compatibility points
        ↓
plan the implementation boundaries
        ↓
turn the plan into checkable tasks
        ↓
analyze the plan against the specification
        ↓
implement with OpenCode
        ↓
run targeted tests + full regression gate
        ↓
update compliance/evidence documents
        ↓
converge only when code, tests and docs agree
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In command form, depending on the Spec Kit version:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;/speckit.constitution   or /speckit-constitution
/speckit.specify        or /speckit-specify
/speckit.clarify        or /speckit-clarify
/speckit.plan           or /speckit-plan
/speckit.tasks          or /speckit-tasks
/speckit.analyze        or /speckit-analyze
/speckit.implement      or /speckit-implement
/speckit.converge       or /speckit-converge
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I do not necessarily use every phase for a tiny bug. The value comes from making the amount of process proportional to the risk.&lt;/p&gt;
&lt;p&gt;For example, changing a CLI string is not the same as changing BGP UPDATE encoding. For the second case I want the plan to mention the exact RFC section, affected parser/encoder path, malformed-input behavior, regression test, and release evidence update before the agent starts coding.&lt;/p&gt;
&lt;h2 id=&quot;rfcs-as-executable-requirements&quot;&gt;RFCs as executable requirements&lt;/h2&gt;
&lt;p&gt;The repository&#39;s compliance summary is more useful to an AI agent than a generic instruction such as “implement BGP correctly.” It maps protocol areas to RFCs, scope, gate status, and required evidence.&lt;/p&gt;
&lt;p&gt;Some of the core references used by the project are:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Domain&lt;/th&gt;&lt;th&gt;RFC&lt;/th&gt;&lt;th&gt;Purpose in the project&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;BGP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://datatracker.ietf.org/doc/rfc4271/&quot;&gt;RFC 4271&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Core BGP-4 protocol&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;BGP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc4760.html&quot;&gt;RFC 4760&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Multiprotocol extensions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;BGP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc6793.html&quot;&gt;RFC 6793&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Four-octet AS numbers&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;BGP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc4456.html&quot;&gt;RFC 4456&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Route reflection&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;BGP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc5065.html&quot;&gt;RFC 5065&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Confederations&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;BGP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc8654.html&quot;&gt;RFC 8654&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Extended messages&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;BGP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc9774.html&quot;&gt;RFC 9774&lt;/a&gt;&lt;/td&gt;&lt;td&gt;AS_PATH segment handling updates&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OSPF&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc2328.html&quot;&gt;RFC 2328&lt;/a&gt;&lt;/td&gt;&lt;td&gt;OSPFv2&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OSPF&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc5340.html&quot;&gt;RFC 5340&lt;/a&gt;&lt;/td&gt;&lt;td&gt;OSPF for IPv6&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OSPF&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc3623.html&quot;&gt;RFC 3623&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Graceful restart&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OSPF&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc5187.html&quot;&gt;RFC 5187&lt;/a&gt;&lt;/td&gt;&lt;td&gt;OSPFv3 graceful restart&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OSPF&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc5709.html&quot;&gt;RFC 5709&lt;/a&gt;&lt;/td&gt;&lt;td&gt;HMAC-SHA authentication&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OSPF&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc4552.html&quot;&gt;RFC 4552&lt;/a&gt;&lt;/td&gt;&lt;td&gt;OSPFv3 authentication/confidentiality&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MPLS/LDP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc5036.html&quot;&gt;RFC 5036&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Label Distribution Protocol&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MPLS&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc3032.html&quot;&gt;RFC 3032&lt;/a&gt;&lt;/td&gt;&lt;td&gt;MPLS label stack encoding&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;LDP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc6720.html&quot;&gt;RFC 6720&lt;/a&gt;&lt;/td&gt;&lt;td&gt;LDP capability negotiation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;LDP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc5918.html&quot;&gt;RFC 5918&lt;/a&gt;&lt;/td&gt;&lt;td&gt;LDP IPv6 extensions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;MPLS PW&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc4447.html&quot;&gt;RFC 4447&lt;/a&gt;&lt;/td&gt;&lt;td&gt;Pseudowire signaling&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;LDP/IGP&lt;/td&gt;&lt;td&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.rfc-editor.org/rfc/rfc7552.html&quot;&gt;RFC 7552&lt;/a&gt;&lt;/td&gt;&lt;td&gt;LDP/IGP synchronization&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The useful pattern is not “put RFC links in a README.” It is to turn them into acceptance criteria.&lt;/p&gt;
&lt;p&gt;A specification for an encoder change might say:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;Requirement:
- UPDATE encoding must follow RFC 4271 section 4.3.
- Empty-path behavior must be deterministic and interoperable.
- Malformed input must be rejected without corrupting peer/session state.
- Existing IPv4/IPv6 and AS4 tests must remain green.
- Add a focused regression reproducing the previously observed failure.
- Update the BGP compliance matrix if the supported behavior changes.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is far harder for an agent to “interpret creatively” than “fix AS_PATH handling.”&lt;/p&gt;
&lt;h2 id=&quot;what-the-commit-history-shows&quot;&gt;What the commit history shows&lt;/h2&gt;
&lt;p&gt;The repository&#39;s own implementation ledger preserves a useful sequence of phase checkpoints from the March 7, 2026 stabilization work.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/commit-timeline.svg&quot; alt=&quot;Commit and evidence timeline&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The sequence is interesting because implementation, tests, and documentation are interleaved rather than treated as separate afterthoughts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;8c64c0c&lt;/code&gt; hardened OSPF authentication writer fallback behavior and added regression coverage.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;3d6dc52&lt;/code&gt; added an MPLS VPN data-plane regression for transport-label resolution upgrades.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;f4ed696&lt;/code&gt; fixed VPNv4 serialization so &lt;code&gt;send-community extended&lt;/code&gt; survives without forcing AF activation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;6b33ecd&lt;/code&gt; added VRF forwarding route-ownership transition coverage.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ede88bc&lt;/code&gt; / &lt;code&gt;38f5bb1&lt;/code&gt; aligned LDP discovery CLI behavior with runtime loopback suppression and added a regression.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;894dccb&lt;/code&gt; fixed RD-safe VPN import cache deletion and reconnect-time &lt;code&gt;update-source &amp;lt;ifname&amp;gt;&lt;/code&gt; resolution, while cleaning up AF-scoped parser behavior.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;225ca52&lt;/code&gt; refreshed the implementation and project status documentation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;f04ef1b&lt;/code&gt;, &lt;code&gt;399d04a&lt;/code&gt;, and &lt;code&gt;3a3fca4&lt;/code&gt; added parser/CLI regressions around VPNv4 shorthand and mixed-order VRF AF mutation.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b15e645&lt;/code&gt; and &lt;code&gt;5493e3d&lt;/code&gt; consolidated the checkpoint/evidence documentation and removed duplicated readiness text.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The phase handoff recorded a full regression result of &lt;strong&gt;152/152 passing tests&lt;/strong&gt; at that checkpoint. The current repository README reports a different current test inventory, so I treat the numbers in the reports as historical evidence tied to that phase, not as an eternal project metric.&lt;/p&gt;
&lt;p&gt;This is exactly the behavior I want from AI-assisted engineering: a code change should leave behind enough evidence that a later session — human or AI — can reconstruct why it exists.&lt;/p&gt;
&lt;h2 id=&quot;the-runtime-architecture-the-agent-had-to-preserve&quot;&gt;The runtime architecture the agent had to preserve&lt;/h2&gt;
&lt;p&gt;The project is organized into focused C modules for infrastructure, platform integration, encoding, RIB, policy, routing, protocol state machines, OSPF, MPLS, configuration, and CLI.&lt;/p&gt;
&lt;p&gt;A simplified data path looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/bgpd-architecture.svg&quot; alt=&quot;bgpd-simple architecture&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That architecture is another form of specification. If a task says “fix parsing under &lt;code&gt;address-family vpnv4&lt;/code&gt;,” it should not casually mutate best-path or FIB code. If an LDP CLI discrepancy is really a presentation/runtime parity issue, the plan should say exactly where the source of truth lives.&lt;/p&gt;
&lt;p&gt;A good agent plan therefore contains not only &lt;em&gt;what&lt;/em&gt; to change, but also &lt;em&gt;what must not move&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;running-qwen-3-6-27b-locally-with-llama-cpp&quot;&gt;Running Qwen 3.6 27B locally with llama.cpp&lt;/h2&gt;
&lt;p&gt;For these experiments, model inference ran locally. That was useful for two reasons: repository context stayed under my control, and I could iterate without tying the workflow to a hosted model provider.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;llama.cpp&lt;/code&gt; provides an OpenAI-compatible HTTP server, which makes it a convenient boundary between a coding agent and a local GGUF model.&lt;/p&gt;
&lt;p&gt;A typical text-only coding setup looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;llama-server \
  -m /models/Qwen3.6-27B.gguf \
  --alias qwen3.6-27b \
  --host 127.0.0.1 \
  --port 8080 \
  --ctx-size 65536 \
  --n-gpu-layers 99
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact GGUF quantization, context size, and GPU offload depend on the machine. I prefer to set an explicit &lt;code&gt;--alias&lt;/code&gt;; otherwise the model ID exposed by &lt;code&gt;/v1/models&lt;/code&gt; may be based on the model path, which is awkward to reference from OpenCode.&lt;/p&gt;
&lt;p&gt;Before involving the coding agent, I verify the serving boundary directly:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;curl http://127.0.0.1:8080/v1/models
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and, if needed, a minimal chat request:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;curl http://127.0.0.1:8080/v1/chat/completions \
  -H &amp;#39;Content-Type: application/json&amp;#39; \
  -d &amp;#39;{
    &amp;quot;model&amp;quot;: &amp;quot;qwen3.6-27b&amp;quot;,
    &amp;quot;messages&amp;quot;: [
      {&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: &amp;quot;Return only: ready&amp;quot;}
    ]
  }&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This isolates model-serving problems from agent problems.&lt;/p&gt;
&lt;h2 id=&quot;connecting-opencode-directly-to-llama-cpp&quot;&gt;Connecting OpenCode directly to llama.cpp&lt;/h2&gt;
&lt;p&gt;OpenCode supports OpenAI-compatible custom providers, and its current provider documentation includes &lt;code&gt;llama.cpp&lt;/code&gt; explicitly.&lt;/p&gt;
&lt;p&gt;A minimal project &lt;code&gt;opencode.json&lt;/code&gt; can look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;json&quot;&gt;{
  &amp;quot;$schema&amp;quot;: &amp;quot;https://opencode.ai/config.json&amp;quot;,
  &amp;quot;model&amp;quot;: &amp;quot;llama.cpp/qwen3.6-27b&amp;quot;,
  &amp;quot;provider&amp;quot;: {
    &amp;quot;llama.cpp&amp;quot;: {
      &amp;quot;npm&amp;quot;: &amp;quot;@ai-sdk/openai-compatible&amp;quot;,
      &amp;quot;name&amp;quot;: &amp;quot;llama-server (local)&amp;quot;,
      &amp;quot;options&amp;quot;: {
        &amp;quot;baseURL&amp;quot;: &amp;quot;http://127.0.0.1:8080/v1&amp;quot;
      },
      &amp;quot;models&amp;quot;: {
        &amp;quot;qwen3.6-27b&amp;quot;: {
          &amp;quot;name&amp;quot;: &amp;quot;Qwen 3.6 27B (local)&amp;quot;,
          &amp;quot;limit&amp;quot;: {
            &amp;quot;context&amp;quot;: 65536,
            &amp;quot;output&amp;quot;: 16384
          }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then I start OpenCode from the repository root:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;opencode
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The basic local connection is simple:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/local-connection.svg&quot; alt=&quot;Local OpenCode to llama.cpp connection&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The coding agent sees an OpenAI-compatible model provider. It does not need to know how Qwen is loaded, quantized, or placed across GPUs.&lt;/p&gt;
&lt;h2 id=&quot;alternative-opencode-with-lm-studio&quot;&gt;Alternative: OpenCode with LM Studio&lt;/h2&gt;
&lt;p&gt;LM Studio can replace &lt;code&gt;llama-server&lt;/code&gt; without changing the Spec Kit specification, task list, or acceptance criteria. Load a model in LM Studio and enable its local server (default &lt;code&gt;127.0.0.1:1234&lt;/code&gt;). You can start the server with:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;lms server start --port 1234
curl http://127.0.0.1:1234/v1/models
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model ID must match the ID returned by your server. For example, add an LM Studio provider to &lt;code&gt;opencode.json&lt;/code&gt; and select it instead of the &lt;code&gt;llama.cpp&lt;/code&gt; provider:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;json&quot;&gt;{
  &amp;quot;$schema&amp;quot;: &amp;quot;https://opencode.ai/config.json&amp;quot;,
  &amp;quot;model&amp;quot;: &amp;quot;lmstudio/my-local-model&amp;quot;,
  &amp;quot;provider&amp;quot;: {
    &amp;quot;lmstudio&amp;quot;: {
      &amp;quot;npm&amp;quot;: &amp;quot;@ai-sdk/openai-compatible&amp;quot;,
      &amp;quot;name&amp;quot;: &amp;quot;LM Studio (local)&amp;quot;,
      &amp;quot;options&amp;quot;: { &amp;quot;baseURL&amp;quot;: &amp;quot;http://127.0.0.1:1234/v1&amp;quot; },
      &amp;quot;models&amp;quot;: {
        &amp;quot;my-local-model&amp;quot;: { &amp;quot;name&amp;quot;: &amp;quot;Local model (replace ID)&amp;quot; }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;my-local-model&lt;/code&gt; is a placeholder; replace it in both locations with a real ID from &lt;code&gt;/v1/models&lt;/code&gt;. This JSON illustrates an alternative configuration, not a second &lt;code&gt;opencode.json&lt;/code&gt; to place alongside the original. LM Studio and llama.cpp expose similar OpenAI-style interfaces, but sampling, tool-call parsing, and context handling can differ by runtime and model.&lt;/p&gt;
&lt;h2 id=&quot;alternative-coding-agent-claude-code&quot;&gt;Alternative coding agent: Claude Code&lt;/h2&gt;
&lt;p&gt;Spec Kit also supports &lt;strong&gt;Claude Code&lt;/strong&gt; directly. In current Spec Kit versions, a fresh project can be initialized with &lt;code&gt;--integration claude&lt;/code&gt; rather than &lt;code&gt;--integration opencode&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;specify init --here --force --integration claude
claude
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For an &lt;strong&gt;existing Spec Kit project&lt;/strong&gt;, avoid reinitializing unnecessarily. To install Claude alongside OpenCode (in Spec Kit versions supporting multiple integrations), use:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;specify integration install claude
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your installed version reports an integration conflict, inspect &lt;code&gt;specify integration list&lt;/code&gt; and the relevant &lt;code&gt;switch&lt;/code&gt;/&lt;code&gt;--force&lt;/code&gt; guidance before changing existing integration files. Claude Code&#39;s Spec Kit skills live under &lt;code&gt;.claude/skills/&lt;/code&gt;, while OpenCode uses its own integration directory. The shared &lt;code&gt;.specify/&lt;/code&gt; artifacts remain the workflow&#39;s common source of truth.&lt;/p&gt;
&lt;p&gt;Claude Code normally uses hosted Anthropic models. It can &lt;strong&gt;also act as a local-model client&lt;/strong&gt; when the inference server supports the Anthropic Messages API. That does &lt;em&gt;not&lt;/em&gt; turn Qwen into an Anthropic Claude model: Claude Code is the CLI agent, and the locally served model is still Qwen (or another loaded model).&lt;/p&gt;
&lt;h3 id=&quot;claude-code-lm-studio&quot;&gt;Claude Code + LM Studio&lt;/h3&gt;
&lt;p&gt;LM Studio exposes &lt;code&gt;POST /v1/messages&lt;/code&gt; as an Anthropic-compatible endpoint. With the model already loaded:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;lms server start --port 1234
export ANTHROPIC_BASE_URL=http://127.0.0.1:1234
export ANTHROPIC_AUTH_TOKEN=lmstudio
claude --model YOUR_LM_STUDIO_MODEL_ID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use the actual model identifier from LM Studio. The placeholder token is sufficient only when server authentication is disabled; if enabled, use a real LM Studio API token. Keep these environment variables scoped to the terminal/session using the local server so that subsequent Claude Code sessions are not accidentally redirected.&lt;/p&gt;
&lt;h3 id=&quot;claude-code-llama-cpp&quot;&gt;Claude Code + llama.cpp&lt;/h3&gt;
&lt;p&gt;Recent &lt;code&gt;llama-server&lt;/code&gt; builds also expose Anthropic-compatible &lt;code&gt;/v1/messages&lt;/code&gt;. Start a tool-capable model with a compatible chat template and enable &lt;code&gt;--jinja&lt;/code&gt; for tool calls:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;llama-server \
  -m /models/YOUR-MODEL.gguf \
  --alias local-coder \
  --host 127.0.0.1 --port 8080 \
  --ctx-size 65536 --jinja

export ANTHROPIC_BASE_URL=http://127.0.0.1:8080
export ANTHROPIC_AUTH_TOKEN=local
claude --model local-coder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt; deliberately has &lt;strong&gt;no &lt;code&gt;/v1&lt;/code&gt; suffix&lt;/strong&gt; in these Claude Code commands; the client appends the Messages API route. As with LM Studio, tool-use behavior and streaming compatibility depend on the server build, template, context size, and model. Verify a small edit plus tool execution before starting a large RFC-driven task.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/agent-inference-matrix.svg&quot; alt=&quot;Agent and server connection matrix&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The practical combinations are:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Coding agent&lt;/th&gt;&lt;th&gt;llama.cpp&lt;/th&gt;&lt;th&gt;LM Studio&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;OpenCode&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;OpenAI-compatible &lt;code&gt;:8080/v1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;OpenAI-compatible &lt;code&gt;:1234/v1&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Claude Code&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Anthropic-compatible &lt;code&gt;:8080/v1/messages&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Anthropic-compatible &lt;code&gt;:1234/v1/messages&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;In this architecture, &lt;strong&gt;the Spec Kit plan and the repository tests stay the same even if the agent CLI or serving engine changes&lt;/strong&gt;. Switching the coding agent may still require its matching Spec Kit integration files; switching the inference server does not.&lt;/p&gt;
&lt;h2 id=&quot;other-connection-patterns&quot;&gt;Other connection patterns&lt;/h2&gt;
&lt;h3 id=&quot;1-llama-cpp-on-another-workstation&quot;&gt;1. llama.cpp on another workstation&lt;/h3&gt;
&lt;p&gt;If the GPU machine is on a trusted private network, &lt;code&gt;llama-server&lt;/code&gt; can listen on a non-loopback address:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;llama-server \
  -m /models/Qwen3.6-27B.gguf \
  --alias qwen3.6-27b \
  --host 0.0.0.0 \
  --port 8080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and OpenCode can point to:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;json&quot;&gt;&amp;quot;baseURL&amp;quot;: &amp;quot;http://gpu-workstation:8080/v1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would not expose this unauthenticated endpoint directly to the public Internet.&lt;/p&gt;
&lt;h3 id=&quot;2-remote-gpu-machine-through-an-ssh-tunnel&quot;&gt;2. Remote GPU machine through an SSH tunnel&lt;/h3&gt;
&lt;p&gt;For a machine outside the local trusted network, an SSH tunnel keeps the OpenCode configuration unchanged:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;ssh -L 8080:127.0.0.1:8080 user@gpu-host
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;llama-server&lt;/code&gt; still binds to &lt;code&gt;127.0.0.1&lt;/code&gt; on the remote machine, while OpenCode still connects to &lt;code&gt;http://127.0.0.1:8080/v1&lt;/code&gt; locally.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/spec-driven-ai-development-bgpd-simple/remote-connection.svg&quot; alt=&quot;Remote model through SSH tunnel&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is my preferred remote pattern because the inference endpoint itself remains loopback-only.&lt;/p&gt;
&lt;h3 id=&quot;3-llama-cpp-in-docker&quot;&gt;3. llama.cpp in Docker&lt;/h3&gt;
&lt;p&gt;The same boundary also works when the model server is containerized:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;docker run --rm \
  --gpus all \
  -p 127.0.0.1:8080:8080 \
  -v /models:/models \
  ghcr.io/ggml-org/llama.cpp:server-cuda \
  -m /models/Qwen3.6-27B.gguf \
  --alias qwen3.6-27b \
  --host 0.0.0.0 \
  --port 8080 \
  --n-gpu-layers 99
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OpenCode does not change. That is another advantage of keeping an HTTP compatibility boundary between the agent and inference runtime.&lt;/p&gt;
&lt;h2 id=&quot;how-i-use-the-agent-during-implementation&quot;&gt;How I use the agent during implementation&lt;/h2&gt;
&lt;p&gt;A local model is still an agentic coding model, not an oracle. I get better results when the work is segmented around evidence.&lt;/p&gt;
&lt;p&gt;For a protocol change I normally ask OpenCode to work through a bounded task such as:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;Implement task T04 only.

Read the feature spec and plan first.
Use RFC 4271 and the project&amp;#39;s BGP compliance document as authority.
Do not change unrelated CLI syntax.
Add the focused regression before considering the task complete.
Run the focused test, then the full relevant BGP gate.
Summarize changed files and verification evidence.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The useful part is that most of those instructions should already exist in project artifacts. The prompt becomes a selector and an execution boundary, not the sole memory of the project.&lt;/p&gt;
&lt;p&gt;If the model starts making broad changes, that is usually a signal that the plan is too vague. If it repeatedly fails the same test, the project&#39;s workflow rules say to stop and re-plan rather than continue patching symptoms. This prevents the “agent keeps editing until the test happens to pass” failure mode.&lt;/p&gt;
&lt;h2 id=&quot;verification-is-part-of-implementation&quot;&gt;Verification is part of implementation&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;bgpd-simple&lt;/code&gt; has dedicated compliance and soak tooling in addition to normal unit/regression tests. The project reports reference commands such as:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;ctest --test-dir build --output-on-failure
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and targeted protocol slices, plus scripts such as:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;tools/run_bgp_mpls_compliance_gate.sh
tools/protocol_soak_evidence.sh
tools/mpls_ldp_soak_gate.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is where spec-driven AI development becomes materially different from “AI generated the patch.”&lt;/p&gt;
&lt;p&gt;The implementation is not done when the agent says it is done. It is done when:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the requested behavior is present;&lt;/li&gt;
&lt;li&gt;the focused regression passes;&lt;/li&gt;
&lt;li&gt;the relevant protocol gate passes;&lt;/li&gt;
&lt;li&gt;unrelated regressions do not appear;&lt;/li&gt;
&lt;li&gt;compliance documentation still matches reality;&lt;/li&gt;
&lt;li&gt;the commit explains the behavioral boundary.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The repository&#39;s release rules capture the same idea: no in-scope &lt;code&gt;FAIL&lt;/code&gt; items, explicit constraints for partial support, and compliance claims backed by both implementation references and executable evidence.&lt;/p&gt;
&lt;h2 id=&quot;where-local-models-work-well-and-where-they-do-not&quot;&gt;Where local models work well — and where they do not&lt;/h2&gt;
&lt;p&gt;In my use, Qwen 3.6 27B was useful for repository-level work, especially when the task had a narrow spec and strong tests. But a local model benefits even more than a frontier hosted model from reducing ambiguity.&lt;/p&gt;
&lt;p&gt;The highest-value uses were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;tracing behavior across parser, protocol, RIB, CLI, and tests;&lt;/li&gt;
&lt;li&gt;generating focused regression tests from a concrete failure;&lt;/li&gt;
&lt;li&gt;implementing bounded changes after the plan was accepted;&lt;/li&gt;
&lt;li&gt;keeping documentation and test evidence synchronized;&lt;/li&gt;
&lt;li&gt;checking repetitive RFC-related conditions across multiple code paths.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The weaker pattern is giving the model a broad request such as “make OSPF fully compliant” and hoping it will discover scope, architecture, protocol semantics, test strategy, and release criteria by itself.&lt;/p&gt;
&lt;p&gt;That is not a model-size problem. It is a requirements problem.&lt;/p&gt;
&lt;h2 id=&quot;what-spec-driven-development-changed-for-me&quot;&gt;What Spec-Driven Development changed for me&lt;/h2&gt;
&lt;p&gt;The biggest change was not code generation speed. It was &lt;strong&gt;context durability&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A chat session is temporary. A specification, compliance matrix, regression test, and commit are durable. When the next task begins, OpenCode and the local model can re-read the same artifacts that a human maintainer would use.&lt;/p&gt;
&lt;p&gt;The workflow also makes model replacement much less disruptive. I can use Qwen locally today, another local model tomorrow, or a hosted model for a difficult task. The process does not disappear with the model because the important state lives in the repository.&lt;/p&gt;
&lt;p&gt;For protocol software, that is the direction I want AI development to move:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;requirements as versioned artifacts, implementation as a constrained transformation, and verification as executable evidence.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The model is a powerful participant in that loop, but it is not the loop itself.&lt;/p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;p&gt;Project:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/bgpd-simple&quot;&gt;&lt;code&gt;avkghost/bgpd-simple&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/bgpd-simple/blob/main/docs/meta/WO.md&quot;&gt;&lt;code&gt;docs/meta/WO.md&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/bgpd-simple/blob/main/docs/reports/IMPLEMENTATION_STATUS.md&quot;&gt;&lt;code&gt;docs/reports/IMPLEMENTATION_STATUS.md&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/bgpd-simple/blob/main/docs/reports/PHASE_HANDOFF_2026-03-07.md&quot;&gt;&lt;code&gt;docs/reports/PHASE_HANDOFF_2026-03-07.md&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/bgpd-simple/blob/main/docs/reports/RFC_COMPLIANCE_SUMMARY.md&quot;&gt;&lt;code&gt;docs/reports/RFC_COMPLIANCE_SUMMARY.md&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tooling:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/github/spec-kit&quot;&gt;GitHub Spec Kit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.github.com/spec-kit/reference/integrations.html&quot;&gt;Spec Kit: supported integrations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/github/spec-kit/blob/main/docs/guides/existing-projects.md&quot;&gt;Spec Kit: adopting an existing project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://opencode.ai/docs/providers/&quot;&gt;OpenCode providers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://dev.opencode.ai/docs/providers&quot;&gt;LM Studio: OpenCode-compatible provider configuration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://lmstudio.ai/docs/integrations/claude-code&quot;&gt;LM Studio: Claude Code integration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://lmstudio.ai/docs/developer/anthropic-compat&quot;&gt;LM Studio: Anthropic-compatible API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.github.io/spec-kit/reference/integrations.html&quot;&gt;Spec Kit: managing multiple agent integrations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md&quot;&gt;llama.cpp server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://huggingface.co/Qwen/Qwen3.6-27B&quot;&gt;Qwen3.6-27B model card&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Libella — Privacy Policy</title>
        <published>2026-10-14T00:00:00+00:00</published>
        <updated>2026-10-14T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://iamavk.info/apps/libella/privacy/"/>
        <id>https://iamavk.info/apps/libella/privacy/</id>
        
        <content type="html" xml:base="https://iamavk.info/apps/libella/privacy/">&lt;p&gt;&lt;strong&gt;Effective date:&lt;/strong&gt; 14 October 2026&lt;/p&gt;
&lt;p&gt;This policy explains how Libella handles information. Libella is provided by &lt;strong&gt;ANDREI KAZIALETSKI, trading as AVKSoft&lt;/strong&gt;.
Privacy questions can be sent to &lt;a href=&quot;mailto:libella@iamavk.info&quot;&gt;libella@iamavk.info&lt;/a&gt;. No postal address is required to contact us.&lt;/p&gt;
&lt;h2 id=&quot;documents-and-local-data&quot;&gt;Documents and local data&lt;/h2&gt;
&lt;p&gt;Libella is designed as an offline PDF/DjVu reader. Imported documents are copied into app-managed local storage and
processed locally. Documents, library metadata, bookmarks and reading positions are not uploaded to AVKSoft servers or
AVKSoft cloud services. Libella has no AVKSoft account system and no proprietary cloud synchronization or backend.&lt;/p&gt;
&lt;p&gt;Apple or Google operating-system or cloud backups may include application data according to the user&#39;s platform and
account settings. Those backups are outside AVKSoft&#39;s direct control.&lt;/p&gt;
&lt;h2 id=&quot;analytics-advertising-and-tracking&quot;&gt;Analytics, advertising and tracking&lt;/h2&gt;
&lt;p&gt;For Libella specifically, there are no ads and no AVKSoft analytics or tracking used to profile reading behavior.
Document and reading activity is not used for advertising. AVKSoft does not sell personal data.&lt;/p&gt;
&lt;h2 id=&quot;store-purchases&quot;&gt;Store purchases&lt;/h2&gt;
&lt;p&gt;Libella may communicate with the Apple App Store or Google Play for product information, localized pricing, purchase
initiation, purchase restoration or recovery, and determining whether a Pro entitlement is active. Apple and Google
process payments under their own terms and privacy policies. AVKSoft does not receive or store payment card details.
Apple App Store and Google Play purchases are separate, and Libella has no account or backend that transfers purchases
between stores. Store availability and recovery behavior should be confirmed with the relevant store setup.&lt;/p&gt;
&lt;h2 id=&quot;support-communications&quot;&gt;Support communications&lt;/h2&gt;
&lt;p&gt;The app does not require personal contact details. If you voluntarily email support, AVKSoft may receive your email
address, message contents, device/OS/app-version information, screenshots, and files or documents you attach. This
information is used to respond to support requests and diagnose reported problems, not for advertising. Please avoid
sending confidential or sensitive documents unless necessary. Support correspondence is kept for as long as reasonably
necessary to handle the request, maintain relevant support records, or comply with legal obligations. You may request
deletion at &lt;a href=&quot;mailto:libella@iamavk.info&quot;&gt;libella@iamavk.info&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;sharing&quot;&gt;Sharing&lt;/h2&gt;
&lt;p&gt;AVKSoft does not sell personal data or share documents or reading activity with advertisers or data brokers. Apple and
Google may process information in connection with their store, operating-system, payment and backup services. Email
infrastructure providers necessarily process support correspondence. Information may also be disclosed when required by law.&lt;/p&gt;
&lt;h2 id=&quot;retention-and-deletion&quot;&gt;Retention and deletion&lt;/h2&gt;
&lt;p&gt;Local app-managed data remains on the device or in app storage. Deleting a document removes the app-managed copy
according to the app&#39;s behavior. Uninstalling normally removes Libella&#39;s local app data, but original files may remain
at their source location and operating-system or cloud backups may have their own retention.&lt;/p&gt;
&lt;h2 id=&quot;privacy-rights&quot;&gt;Privacy rights&lt;/h2&gt;
&lt;p&gt;Depending on applicable law, you may request access, correction, deletion, or restriction or objection where applicable,
and may complain to a supervisory authority. In Poland, this includes the &lt;strong&gt;Prezes Urzędu Ochrony Danych Osobowych
(UODO)&lt;/strong&gt;. Contact &lt;a href=&quot;mailto:libella@iamavk.info&quot;&gt;libella@iamavk.info&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;children-s-privacy&quot;&gt;Children&#39;s privacy&lt;/h2&gt;
&lt;p&gt;Libella has no account, advertising, or behavioral analytics/profile system. This policy makes no unsupported age-certification claim.&lt;/p&gt;
&lt;h2 id=&quot;security&quot;&gt;Security&lt;/h2&gt;
&lt;p&gt;AVKSoft uses reasonable measures appropriate to the information handled, but no method of storage or transmission can
be guaranteed completely secure.&lt;/p&gt;
&lt;h2 id=&quot;changes-to-this-policy&quot;&gt;Changes to this policy&lt;/h2&gt;
&lt;p&gt;If this policy changes, the new version will be published on this page with a new effective date. For questions, contact
&lt;a href=&quot;mailto:libella@iamavk.info&quot;&gt;libella@iamavk.info&lt;/a&gt; or see the &lt;a href=&quot;https://iamavk.info/apps/libella/support/&quot;&gt;Libella Support page&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Chasing a PCIe Ghost on Raspberry Pi 5</title>
        <published>2026-10-14T00:00:00+00:00</published>
        <updated>2026-10-14T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://iamavk.info/blog/raspberry-pi-5-pcie-debugging-story/"/>
        <id>https://iamavk.info/blog/raspberry-pi-5-pcie-debugging-story/</id>
        
        <content type="html" xml:base="https://iamavk.info/blog/raspberry-pi-5-pcie-debugging-story/">&lt;p&gt;&lt;em&gt;How a simple expansion experiment turned into a deep dive through link training, oscilloscopes, kernel patches, and a plan to build my own PCB.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-setup&quot;&gt;The setup&lt;/h2&gt;
&lt;p&gt;I wanted to connect standard PCIe cards to a Raspberry Pi 5. I used the &lt;strong&gt;52Pi P02 (EP-0219)&lt;/strong&gt; adapter: a full-size PCIe slot connected to the Pi&#39;s external PCIe FFC connector. The Pi exposes one lane, so even physically wider cards operate at x1.&lt;/p&gt;
&lt;p&gt;My test hardware included an &lt;strong&gt;AXAGON PCEE-G25 RTL8125B&lt;/strong&gt; 2.5GbE NIC, a &lt;strong&gt;10Gtek four-port RTL8125&lt;/strong&gt; NIC behind an &lt;strong&gt;ASMedia ASM1806&lt;/strong&gt; switch, a standalone &lt;strong&gt;ASM1184E&lt;/strong&gt; switch card, and a &lt;strong&gt;Broadcom/LSI SAS9300-8i (SAS3008)&lt;/strong&gt; HBA.&lt;/p&gt;
&lt;p&gt;The results were not what I expected.&lt;/p&gt;
&lt;h2 id=&quot;a-puzzling-compatibility-matrix&quot;&gt;A puzzling compatibility matrix&lt;/h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Device&lt;/th&gt;&lt;th&gt;Raspberry Pi 5 + P02 result&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;AXAGON RTL8125B&lt;/td&gt;&lt;td&gt;No link; ends in &lt;code&gt;Polling.Compliance&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ASM1184E&lt;/td&gt;&lt;td&gt;No usable link; sometimes reaches &lt;code&gt;Polling.Configuration&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;10Gtek ASM1806&lt;/td&gt;&lt;td&gt;Upstream link works; switch and downstream bridges enumerate&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;10Gtek 4× RTL8125&lt;/td&gt;&lt;td&gt;No NIC endpoints enumerate&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;SAS3008&lt;/td&gt;&lt;td&gt;Works, including &lt;strong&gt;Gen3 8.0 GT/s x1&lt;/strong&gt;, with no reported AER errors&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The SAS3008 was a particularly useful control. It demonstrated that the same Pi, FFC cable, and P02 could carry a working Gen3 link. But that did not prove every electrical and timing characteristic of the adapter was correct for every endpoint.&lt;/p&gt;
&lt;h2 id=&quot;watching-pcie-link-training&quot;&gt;Watching PCIe link training&lt;/h2&gt;
&lt;p&gt;I enabled the Raspberry Pi Broadcom driver&#39;s LTSSM tracing:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;pcie-brcmstb.trace_ltssm=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the AXAGON, the result was repeatable across Gen1, Gen2, and Gen3 settings:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;Detect.Active
Polling.Active
~24 ms later: Polling.Compliance
link down
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ASM1184E behaved differently. At Gen2/Gen3 it could reach &lt;code&gt;Polling.Configuration&lt;/code&gt;, but still never established a usable link. Working cards advanced through &lt;code&gt;Configuration.Link&lt;/code&gt;, the other configuration substates, and ultimately &lt;code&gt;L0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;These are different failure stages, not one universal symptom.&lt;/p&gt;
&lt;h2 id=&quot;ruling-out-the-obvious&quot;&gt;Ruling out the obvious&lt;/h2&gt;
&lt;p&gt;I tested Raspberry Pi kernel &lt;strong&gt;6.12&lt;/strong&gt; and &lt;strong&gt;6.18&lt;/strong&gt;; the behavior persisted. I forced Gen1/Gen2/Gen3, tried a &lt;strong&gt;500 ms&lt;/strong&gt; delay before PERST# release, changed the P02&#39;s power arrangement, and tested with CLKREQ held low. I checked the 5 V supply and PCIe slot&#39;s 3.3 V rails. None of these changes fixed the failing cards.&lt;/p&gt;
&lt;p&gt;With a &lt;strong&gt;FNIRSI DPOX180H&lt;/strong&gt; oscilloscope, I verified that PERST# was released during the link-training window and reasserted after failure. I also observed an approximately &lt;strong&gt;100 MHz REFCLK&lt;/strong&gt; during training on the AXAGON and ASM1184E. So the reset and clock signals were not simply absent.&lt;/p&gt;
&lt;p&gt;The scope cannot resolve a PCIe 2.5 GT/s eye diagram, but it can reveal coarse activity. The AXAGON showed activity from the card toward the Pi; comparable Pi-to-card training activity was not clearly visible. The ASM1184E showed Pi-to-card activity and progressed farther in the LTSSM.&lt;/p&gt;
&lt;p&gt;An approximately &lt;strong&gt;208 mV&lt;/strong&gt; receiver-detect-looking event was visible with an empty slot, and a similar event appeared with the working 10Gtek. I did not see an equally clear event with the AXAGON. This was an interesting comparison, &lt;strong&gt;not a compliance-grade measurement or proof of a particular receiver-detection failure&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Eventually, probing the tiny FFC connector with ordinary multimeter probes became more risky than useful. Adjacent contacts were too easy to bridge, and continuity readings could be misleading.&lt;/p&gt;
&lt;h2 id=&quot;a-kernel-patch-retry-without-perst&quot;&gt;A kernel patch: retry without PERST#&lt;/h2&gt;
&lt;p&gt;One hypothesis was that the RTL8125 needed more time after startup. I patched the &lt;strong&gt;Raspberry Pi 6.18 &lt;code&gt;pcie-brcmstb&lt;/code&gt; driver&lt;/strong&gt; to toggle the root port&#39;s &lt;strong&gt;Link Disable&lt;/strong&gt; bit after the initial failure, restarting link training &lt;strong&gt;without reasserting PERST#&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The first attempt produced:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;Detect.Active → Polling.Active → Polling.Compliance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The retry produced the &lt;strong&gt;same sequence&lt;/strong&gt;. The second capture reported zero timestamps because tracing began after the restart, so those timestamps were not useful for timing analysis. The meaningful result was that &lt;strong&gt;the warm LTSSM retry did not recover the AXAGON&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This weakened the simple “endpoint was not ready yet” explanation, although it did not reveal the underlying cause.&lt;/p&gt;
&lt;h2 id=&quot;the-x86-cross-check-changed-everything&quot;&gt;The x86 cross-check changed everything&lt;/h2&gt;
&lt;p&gt;Later I tested the cards in an x86-64 PC. &lt;strong&gt;Every card worked.&lt;/strong&gt; The ASM1184E exposed its four ports; the 10Gtek enumerated its &lt;strong&gt;ASM1806 switch and all four RTL8125 NICs&lt;/strong&gt;; and the AXAGON worked. A separate PCIe x1-to-x16 riser/extender also successfully detected an RTL8127.&lt;/p&gt;
&lt;p&gt;This ruled out treating the endpoint cards as generally defective.&lt;/p&gt;
&lt;p&gt;It also exposed a problem with the early “RTL8125 versus BCM2712” theory: on the 10Gtek, the four RTL8125 controllers are &lt;strong&gt;behind the ASM1806 switch&lt;/strong&gt;, not directly connected to the Raspberry Pi PHY. A direct BCM2712-to-RTL8125 electrical mismatch alone cannot explain every observation.&lt;/p&gt;
&lt;p&gt;The common failure environment is the &lt;strong&gt;Pi 5 + FFC cable + 52Pi P02 combination&lt;/strong&gt;. Plausible causes still include P02 signal integrity or power/reset implementation, BCM2712 external PCIe interoperability, or an interaction between them. Successful HATs such as Hailo setups show that the Pi&#39;s external PCIe can work, but they do not isolate the P02 because they replace the adapter path entirely.&lt;/p&gt;
&lt;h2 id=&quot;the-adapter-became-the-unknown&quot;&gt;The adapter became the unknown&lt;/h2&gt;
&lt;p&gt;I consulted the &lt;a rel=&quot;external&quot; href=&quot;https://wiki.52pi.com/index.php?title=EP-0219&quot;&gt;52Pi EP-0219 wiki&lt;/a&gt; and the &lt;a rel=&quot;external&quot; href=&quot;https://pip-assets.raspberrypi.com/categories/892-raspberry-pi-5/documents/RP-008298-DS-1-pcie-connector-standard.pdf&quot;&gt;official Raspberry Pi 5 PCIe connector specification&lt;/a&gt;. The official J20 signal assignment includes:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: right&quot;&gt;J20 pin&lt;/th&gt;&lt;th&gt;Signal&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;4 / 5&lt;/td&gt;&lt;td&gt;PCIe reference clock P / N&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;7 / 8&lt;/td&gt;&lt;td&gt;PCIe RX P / N&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;10 / 11&lt;/td&gt;&lt;td&gt;PCIe TX P / N&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;13&lt;/td&gt;&lt;td&gt;PCIe power enable&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;14&lt;/td&gt;&lt;td&gt;PCIe detect / wake&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;15&lt;/td&gt;&lt;td&gt;PCIe CLKREQ#&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: right&quot;&gt;16&lt;/td&gt;&lt;td&gt;PCIe reset&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Without a full, verified P02 schematic and layout—or another equivalent PCIe slot adapter for A/B testing—I could not confidently separate the board&#39;s behavior from the Pi&#39;s own PCIe behavior. Basic continuity measurements were not going to tell me enough about AC coupling, controlled impedance, clock routing, and reset or power sequencing.&lt;/p&gt;
&lt;h2 id=&quot;next-designing-my-own-pcie-pcb&quot;&gt;Next: designing my own PCIe PCB&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;I&#39;m going to design and build a custom PCB with all required onboard power converters and a full-size PCIe connector, replacing the 52Pi P02 adapter in my test setup.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The design will include a Pi 5-compatible FFC interface, regulated &lt;strong&gt;12 V and 3.3 V slot supplies&lt;/strong&gt; sized for the intended cards, controlled-impedance PCIe TX/RX and REFCLK routing, correct reset and sideband handling, and accessible test points for power and low-speed signals. The slot will accept wider physical cards, while the Pi connection remains &lt;strong&gt;one PCIe lane&lt;/strong&gt;. I&#39;ll also make the schematic, component choices, and power sequencing fully inspectable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I have not proven that the P02 is defective.&lt;/strong&gt; The purpose of the custom PCB is to remove an opaque adapter from the experiment and make the entire signal and power path understandable and measurable.&lt;/p&gt;
&lt;p&gt;If the same cards fail on the new PCB, the evidence will shift toward Pi 5 / BCM2712 behavior or a shared design assumption. If they work, the P02 implementation or its connection will become the much stronger suspect.&lt;/p&gt;
&lt;p&gt;Either outcome is progress. The next chapter will be about designing, assembling, and testing the replacement board.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://wiki.52pi.com/index.php?title=EP-0219&quot;&gt;52Pi P02 / EP-0219 wiki&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://pip-assets.raspberrypi.com/categories/892-raspberry-pi-5/documents/RP-008298-DS-1-pcie-connector-standard.pdf&quot;&gt;Raspberry Pi 5 PCIe connector standard (PDF)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/raspberrypi/linux/tree/rpi-6.18.y&quot;&gt;Raspberry Pi Linux kernel &lt;code&gt;rpi-6.18.y&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Welcome to the AVKSoft blog</title>
        <published>2026-10-14T00:00:00+00:00</published>
        <updated>2026-10-14T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://iamavk.info/blog/welcome/"/>
        <id>https://iamavk.info/blog/welcome/</id>
        
        <summary type="html">&lt;p&gt;AVKSoft is an independent software developer. We build mobile applications for the Apple App Store and Google Play.&lt;/p&gt;
&lt;p&gt;Our first applications are still in development. When they are ready, we will publish their store links on this site and announce them here.&lt;/p&gt;</summary>
        
    </entry>
    <entry xml:lang="en">
        <title>From BlueSCSI Ultra to DaynaPORT: Connecting Legacy SCSI Devices to Linux and Windows</title>
        <published>2026-08-16T00:00:00+00:00</published>
        <updated>2026-08-16T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://iamavk.info/blog/bluescsi-daynaport-linux-windows/"/>
        <id>https://iamavk.info/blog/bluescsi-daynaport-linux-windows/</id>
        
        <content type="html" xml:base="https://iamavk.info/blog/bluescsi-daynaport-linux-windows/">&lt;p&gt;The project did not start with networking. It started with a much more practical problem: &lt;strong&gt;I wanted to use my old SCSI devices again.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I had devices such as a &lt;strong&gt;Sony SMO-F551&lt;/strong&gt; magneto-optical drive and &lt;strong&gt;Iomega Jaz 1 GB / 2 GB&lt;/strong&gt; drives. The drives were still useful to me, but connecting them to a modern computer was the difficult part. At the time, I could not find a suitable old SCSI controller.&lt;/p&gt;
&lt;p&gt;Instead, I used a &lt;strong&gt;BlueSCSI Ultra&lt;/strong&gt; and began exploring how far its bridge capabilities could be taken. My original goal was straightforward: give a modern Linux or Windows host access to physical legacy SCSI peripherals, without first having to source a period-correct SCSI host adapter.&lt;/p&gt;
&lt;p&gt;That distinction matters. BlueSCSI is widely known for &lt;em&gt;emulating&lt;/em&gt; SCSI peripherals, such as hard disks backed by SD-card images. My problem was the other way around: I already had real SCSI devices and wanted to communicate with them.&lt;/p&gt;
&lt;h3 id=&quot;how-networking-entered-the-picture&quot;&gt;How networking entered the picture&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/legacy-scsi-bridge.svg&quot; alt=&quot;Modern computer connected to physical Sony and Iomega SCSI drives through BlueSCSI Ultra&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The original goal: access real legacy SCSI hardware without a vintage host adapter.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Working on the SCSI bridge exposed another possibility. BlueSCSI also has DaynaPORT-related functionality, based on the &lt;strong&gt;DaynaPORT SCSI/Link&lt;/strong&gt; network adapter protocol from the 1990s. It was worth exploring whether the bridge and a host-side driver could expose that interface to Linux and Windows as an ordinary network adapter.&lt;/p&gt;
&lt;p&gt;That led to the work described here: BlueSCSI bridge changes, a Linux DaynaPORT driver, and a Windows DaynaPORT driver family. The network interface was an extension of the original SCSI-access project, &lt;strong&gt;not the reason I bought or started using BlueSCSI Ultra&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The work now spans three repositories:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BlueSCSI-v2&lt;/code&gt;, with my experimental &lt;code&gt;avkghost/birdge-with-daynaport&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;&lt;code&gt;daynaport-scsilink-linux-driver&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;daynaport-scsilink-windows-driver&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The interesting part is not only that the driver works like a network card. It is that the same basic DaynaPORT protocol has to survive several very different environments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;native SCSI,&lt;/li&gt;
&lt;li&gt;BlueSCSI&#39;s USB bridge,&lt;/li&gt;
&lt;li&gt;Linux kernels from 2.0 to 7.x,&lt;/li&gt;
&lt;li&gt;Windows 9x,&lt;/li&gt;
&lt;li&gt;Windows NT 4.0,&lt;/li&gt;
&lt;li&gt;Windows 2000/XP,&lt;/li&gt;
&lt;li&gt;and Windows 10/11.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is a useful example of what happens when a packet protocol from the early 1990s meets several decades of operating-system driver architecture.&lt;/p&gt;
&lt;h2 id=&quot;daynaport-is-not-a-disk&quot;&gt;DaynaPORT is not a disk&lt;/h2&gt;
&lt;p&gt;A DaynaPORT SCSI/Link appears on the SCSI bus as a &lt;strong&gt;Processor device&lt;/strong&gt; (&lt;code&gt;type 0x03&lt;/code&gt;), not as a Direct Access device.&lt;/p&gt;
&lt;p&gt;The host discovers it with a normal SCSI &lt;code&gt;INQUIRY&lt;/code&gt;, looking for something equivalent to:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;Vendor:  Dayna
Product: SCSI/Link
Type:    Processor (0x03)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From there, Ethernet frames are moved with ordinary-looking six-byte SCSI commands that have Dayna-specific semantics.&lt;/p&gt;
&lt;p&gt;The core operations are surprisingly small:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;INQUIRY          0x12   identify the device
READ(6)          0x08   receive Ethernet frames
WRITE(6)         0x0A   transmit Ethernet frames
RETRIEVE STATS   0x09   retrieve MAC address and counters
ENABLE/DISABLE   0x0E   bring the interface up or down
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A Linux or Windows driver therefore sits between two completely different subsystems:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/network-stack.svg&quot; alt=&quot;DaynaPORT network data path&quot; /&gt;&lt;/p&gt;
&lt;p&gt;BlueSCSI itself does not run DHCP for the host and does not own the host&#39;s IP configuration. In this mode it behaves much more like an Ethernet bridge: the operating system still owns the network stack.&lt;/p&gt;
&lt;h2 id=&quot;the-first-important-limitation-there-is-no-rx-interrupt&quot;&gt;The first important limitation: there is no RX interrupt&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/daynaport-data-path.svg&quot; alt=&quot;DaynaPORT network stack and SCSI data path&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The operating system handles networking; the DaynaPORT driver converts Ethernet traffic to SCSI commands.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A normal Ethernet controller raises an interrupt when a packet arrives.&lt;/p&gt;
&lt;p&gt;DaynaPORT does not.&lt;/p&gt;
&lt;p&gt;The only way to discover incoming traffic is to issue &lt;code&gt;READ(6)&lt;/code&gt; repeatedly.&lt;/p&gt;
&lt;p&gt;That creates one of the central design problems in the entire driver.&lt;/p&gt;
&lt;p&gt;Poll too slowly and latency becomes terrible or the device&#39;s RX queue fills.&lt;/p&gt;
&lt;p&gt;Poll too aggressively and the driver wastes CPU time and occupies the SCSI bus.&lt;/p&gt;
&lt;p&gt;There is another constraint: effectively there is one command path. RX polling and TX must be arbitrated. If receive traffic monopolizes the SCSI transport, transmit suffers. If transmit is allowed to dominate, the receive queue may overflow.&lt;/p&gt;
&lt;p&gt;This becomes much more important once BlueSCSI is placed behind another bridge.&lt;/p&gt;
&lt;h2 id=&quot;native-scsi-was-the-easy-case&quot;&gt;Native SCSI was the easy case&lt;/h2&gt;
&lt;p&gt;With a real SCSI HBA, one Ethernet frame can be transferred in one SCSI operation.&lt;/p&gt;
&lt;p&gt;A typical receive record looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/rx-record.svg&quot; alt=&quot;DaynaPORT receive record&quot; /&gt;&lt;/p&gt;
&lt;p&gt;BlueSCSI can also return several records in one &lt;code&gt;READ(6)&lt;/code&gt; when the host uses the multi-packet form.&lt;/p&gt;
&lt;p&gt;The driver therefore cannot assume that one SCSI read equals one Ethernet frame. It has to walk the returned buffer record by record.&lt;/p&gt;
&lt;p&gt;For transmit, the simplest path is even cleaner: one raw Ethernet frame becomes one &lt;code&gt;WRITE(6)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is close to how the original hardware was intended to be used.&lt;/p&gt;
&lt;p&gt;Then came the USB bridge.&lt;/p&gt;
&lt;h2 id=&quot;the-512-byte-problem&quot;&gt;The 512-byte problem&lt;/h2&gt;
&lt;p&gt;My BlueSCSI bridge work introduced a very different transport constraint.&lt;/p&gt;
&lt;p&gt;The early USB Mass Storage bridge path moved SCSI data through a &lt;strong&gt;512-byte buffer&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That is perfectly natural for block storage.&lt;/p&gt;
&lt;p&gt;It is not natural for Ethernet.&lt;/p&gt;
&lt;p&gt;A normal Ethernet frame can be roughly 1.5 KB, so a packet that was previously transferred in one SCSI operation suddenly no longer fits.&lt;/p&gt;
&lt;p&gt;The transport now looks more like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/bridge-512.svg&quot; alt=&quot;512-byte SCSI bridge transfer&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is where the driver stopped being only a DaynaPORT driver and started becoming a DaynaPORT-over-bridge driver.&lt;/p&gt;
&lt;h2 id=&quot;adding-chunking-without-breaking-daynaport&quot;&gt;Adding chunking without breaking DaynaPORT&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/bridge-chunking.svg&quot; alt=&quot;An Ethernet frame split into continuation chunks and reassembled&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Bridge-specific fragmentation preserves the Ethernet frame despite a smaller transport limit.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The bridge protocol uses a continuation flag (&lt;code&gt;0x20&lt;/code&gt;) so that one Ethernet frame can be spread across multiple SCSI transactions.&lt;/p&gt;
&lt;p&gt;On receive, the driver collects chunks until it sees the final record and then reconstructs the Ethernet frame.&lt;/p&gt;
&lt;p&gt;On transmit, a large frame is split into several &lt;code&gt;WRITE(6)&lt;/code&gt; commands and reassembled on the BlueSCSI side.&lt;/p&gt;
&lt;p&gt;For the USB bridge path, TX chunks are limited to 506 bytes because some bytes are consumed by protocol framing.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/continuation.svg&quot; alt=&quot;DaynaPORT continuation flag&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A zero-length write can be used to abort an incomplete transmit chain.&lt;/p&gt;
&lt;p&gt;The important architectural decision was to keep this extension outside the normal DaynaPORT semantics as much as possible. Native SCSI should continue to behave like native SCSI. Chunking is transport-specific.&lt;/p&gt;
&lt;p&gt;That distinction appears throughout the Linux and Windows implementations.&lt;/p&gt;
&lt;h2 id=&quot;the-linux-driver-became-three-drivers&quot;&gt;The Linux driver became three drivers&lt;/h2&gt;
&lt;p&gt;The original Linux driver work comes from Jeff Flitton. My fork adds the bridge/chunking support, diagnostics, shared protocol work, and subsequent improvements.&lt;/p&gt;
&lt;p&gt;One source file cannot realistically cover the Linux kernel API from 2.0 through 7.x, so the repository now contains three separate driver implementations:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;linux-2.0/
linux-2.4/
linux-7.0/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What can be shared lives in:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;lib/daynaport.h
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That file deliberately contains protocol logic rather than Linux kernel API calls.&lt;/p&gt;
&lt;p&gt;It is C89-clean so that the same protocol core can be used by very old compilers and modern toolchains.&lt;/p&gt;
&lt;p&gt;This turned out to be one of the most valuable design choices in the project, because the same core could later be reused by the Windows ports.&lt;/p&gt;
&lt;h2 id=&quot;discovery-instead-of-hard-coded-scsi-ids&quot;&gt;Discovery instead of hard-coded SCSI IDs&lt;/h2&gt;
&lt;p&gt;Another small but important rule is that the driver must not assume a fixed SCSI target.&lt;/p&gt;
&lt;p&gt;BlueSCSI lets the user choose the SCSI ID of the emulated DaynaPORT.&lt;/p&gt;
&lt;p&gt;The driver therefore follows normal SCSI discovery and binds to a device matching the Dayna identity:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;Peripheral type: 0x03
Vendor:          Dayna
Product:         SCSI/Link
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sounds obvious, but it keeps the driver compatible with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;real DaynaPORT hardware,&lt;/li&gt;
&lt;li&gt;BlueSCSI,&lt;/li&gt;
&lt;li&gt;ZuluSCSI,&lt;/li&gt;
&lt;li&gt;PiSCSI,&lt;/li&gt;
&lt;li&gt;and potentially more than one adapter on a bus.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;another-historical-detail-wait-after-enable&quot;&gt;Another historical detail: wait after ENABLE&lt;/h2&gt;
&lt;p&gt;One of the less obvious pieces of the protocol is the delay after enabling the interface.&lt;/p&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;0E 00 00 00 00 80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the host should wait about 500 ms before issuing further commands.&lt;/p&gt;
&lt;p&gt;That behavior is visible in other DaynaPORT driver implementations as well.&lt;/p&gt;
&lt;p&gt;It is exactly the sort of detail that disappears if you only implement the obvious packet path and do not compare multiple implementations and the original command documentation.&lt;/p&gt;
&lt;p&gt;The protocol-reference work in the Linux repository became almost as important as the driver itself.&lt;/p&gt;
&lt;h2 id=&quot;polling-was-the-real-performance-problem&quot;&gt;Polling was the real performance problem&lt;/h2&gt;
&lt;p&gt;At first it was tempting to assume that the 512-byte bridge buffer would be the main bottleneck.&lt;/p&gt;
&lt;p&gt;The benchmarks showed something more interesting.&lt;/p&gt;
&lt;p&gt;The Linux test notes record roughly:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/polling-baseline.svg&quot; alt=&quot;Fixed-interval receive polling&quot; /&gt;&lt;/p&gt;
&lt;p&gt;During active receive traffic, the driver reached roughly 70–85 SCSI reads per second.&lt;/p&gt;
&lt;p&gt;That was close to the device round-trip limit in the tested configuration.&lt;/p&gt;
&lt;p&gt;The polling logic was changed so that after a productive read the driver could continue polling aggressively for a short fast window rather than immediately sleeping for the normal poll interval.&lt;/p&gt;
&lt;p&gt;When the link becomes idle, it falls back to the slower polling cadence.&lt;/p&gt;
&lt;p&gt;This is important because a fixed polling interval creates a nasty feedback loop:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/polling-adaptive.svg&quot; alt=&quot;Adaptive receive polling&quot; /&gt;&lt;/p&gt;
&lt;p&gt;With back-to-back packets, every artificial sleep directly reduces throughput.&lt;/p&gt;
&lt;p&gt;The optimized path instead behaves approximately like:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;packet arrives
      |
productive READ
      |
immediate READ
      |
productive READ
      |
immediate READ
      |
...
      |
empty READs
      |
return to idle cadence
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The cost is temporarily higher CPU usage, so the driver exposes parameters such as &lt;code&gt;poll_ms&lt;/code&gt;, &lt;code&gt;poll0_ms&lt;/code&gt;, &lt;code&gt;fast_hold&lt;/code&gt;, and &lt;code&gt;tx_burst&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;measuring-the-bridge-separately-changed-the-diagnosis&quot;&gt;Measuring the bridge separately changed the diagnosis&lt;/h2&gt;
&lt;p&gt;The most useful benchmark was not an Ethernet benchmark.&lt;/p&gt;
&lt;p&gt;It was block I/O through the same bridge.&lt;/p&gt;
&lt;p&gt;Using the raw bridge with a SCSI Jaz drive produced approximately:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;block read:   ~2.7 Mbit/s
block write:  ~2.6 Mbit/s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That was several times faster than the DaynaPORT network download result.&lt;/p&gt;
&lt;p&gt;This immediately showed that the USB bridge itself had significant headroom.&lt;/p&gt;
&lt;p&gt;The network RX path was being limited somewhere else.&lt;/p&gt;
&lt;p&gt;That is exactly why end-to-end benchmarking alone can be misleading. If I had only measured &lt;code&gt;iperf3&lt;/code&gt;, it would have been easy to blame USB chunking for everything.&lt;/p&gt;
&lt;h2 id=&quot;increasing-the-usb-buffer-to-4096-bytes&quot;&gt;Increasing the USB buffer to 4096 bytes&lt;/h2&gt;
&lt;p&gt;A later experiment increased the TinyUSB MSC buffer from 512 bytes to 4096 bytes.&lt;/p&gt;
&lt;p&gt;That changed block-read performance significantly.&lt;/p&gt;
&lt;p&gt;The benchmark notes show approximately:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;512-byte era:
    block read  ~2.7 Mbit/s

4096-byte buffer:
    block read  ~5.35 Mbit/s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A full Ethernet frame could now fit into one bridge read instead of being split across up to three 512-byte transactions.&lt;/p&gt;
&lt;p&gt;But something else was revealing:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/windows-modern.svg&quot; alt=&quot;Windows 10/11 driver layers&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The Jaz drive itself was now the write-side limit.&lt;/p&gt;
&lt;p&gt;And the expected DaynaPORT network improvement was small, because the Wi-Fi inbound path was already the tighter bottleneck.&lt;/p&gt;
&lt;p&gt;That is the kind of result I like in low-level projects: changing a parameter makes one subsystem dramatically faster while proving that another subsystem was never limited by that parameter in the first place.&lt;/p&gt;
&lt;h2 id=&quot;from-linux-to-windows&quot;&gt;From Linux to Windows&lt;/h2&gt;
&lt;p&gt;Once the DaynaPORT protocol logic had been separated from the Linux kernel glue, porting the concept to Windows became much more realistic.&lt;/p&gt;
&lt;p&gt;But &quot;Windows driver&quot; does not mean one architecture.&lt;/p&gt;
&lt;p&gt;The repository currently targets four eras.&lt;/p&gt;
&lt;h3 id=&quot;windows-10-11&quot;&gt;Windows 10 / 11&lt;/h3&gt;
&lt;p&gt;Modern Windows uses a two-driver split:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;daynaportfn.sys
    KMDF companion driver
    owns the SCSI-side communication

daynaportnd.sys
    NDIS 6 miniport
    exposes the network adapter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps SCSI access and NDIS networking separated.&lt;/p&gt;
&lt;h3 id=&quot;windows-2000-xp&quot;&gt;Windows 2000 / XP&lt;/h3&gt;
&lt;p&gt;The 2000/XP version uses a single NDIS 5.1 miniport.&lt;/p&gt;
&lt;p&gt;It talks to the SCSI device through:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/windows-9x.svg&quot; alt=&quot;Windows 9x driver layers&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;windows-nt-4-0&quot;&gt;Windows NT 4.0&lt;/h3&gt;
&lt;p&gt;NT 4 follows the same broad idea but targets NDIS 4.0 and the NT-era driver environment.&lt;/p&gt;
&lt;h3 id=&quot;windows-95-98-me&quot;&gt;Windows 95 / 98 / ME&lt;/h3&gt;
&lt;p&gt;Windows 9x is different again.&lt;/p&gt;
&lt;p&gt;The design uses:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/common-core.svg&quot; alt=&quot;Shared protocol across operating systems&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The amusing part is that the network protocol itself did not fundamentally change.&lt;/p&gt;
&lt;p&gt;The operating-system plumbing changed completely.&lt;/p&gt;
&lt;h2 id=&quot;one-protocol-core-decades-of-driver-apis&quot;&gt;One protocol core, decades of driver APIs&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/cross-platform-drivers.svg&quot; alt=&quot;Linux and Windows driver implementations using a common DaynaPORT protocol core&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Platform-specific driver plumbing stays separate from the protocol core.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The Windows repository vendors the shared DaynaPORT protocol header derived from the Linux project.&lt;/p&gt;
&lt;p&gt;That means the project can be viewed as two layers:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;+-------------------------------------------+
| OS-specific driver                       |
|                                           |
| Linux 2.0 / 2.4 / 7.x                    |
| Win9x / NT4 / 2K-XP / Win10-11           |
+--------------------+----------------------+
                     |
                     v
+-------------------------------------------+
| shared DaynaPORT protocol core            |
| framing, opcodes, RX parsing, chunking    |
+--------------------+----------------------+
                     |
                     v
+-------------------------------------------+
| SCSI transport / BlueSCSI                 |
+-------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is probably the most reusable part of the whole exercise.&lt;/p&gt;
&lt;p&gt;The protocol is old.&lt;/p&gt;
&lt;p&gt;The driver APIs are not.&lt;/p&gt;
&lt;p&gt;Keeping those two facts separate makes the code much easier to reason about.&lt;/p&gt;
&lt;h2 id=&quot;bridge-options-on-windows&quot;&gt;Bridge options on Windows&lt;/h2&gt;
&lt;p&gt;The Windows side follows the same split between normal DaynaPORT traffic and bridge-specific transport.&lt;/p&gt;
&lt;p&gt;The legacy configuration includes tunables for:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/project-timeline.svg&quot; alt=&quot;Evolution of the project&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;UsbBridge&lt;/code&gt; selects the USB MSC chunked path.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RawBridge&lt;/code&gt; selects the BlueSCSI raw DaynaPORT bridge path, where both directions must respect the bridge transfer limit.&lt;/p&gt;
&lt;p&gt;The important point is that the network stack should not care.&lt;/p&gt;
&lt;p&gt;It still sends and receives Ethernet frames.&lt;/p&gt;
&lt;p&gt;Only the SCSI transport layer knows whether a frame was moved in one command or reconstructed from several.&lt;/p&gt;
&lt;h2 id=&quot;what-the-repository-history-shows&quot;&gt;What the repository history shows&lt;/h2&gt;
&lt;p&gt;Looking across the repositories, the project did not evolve as one giant &quot;write a driver&quot; commit.&lt;/p&gt;
&lt;p&gt;It evolved in layers:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;1. Understand the DaynaPORT command set
2. Build a Linux driver around normal SCSI
3. Separate reusable protocol logic
4. Add multi-kernel Linux implementations
5. Add USB-bridge chunking
6. Add diagnostics and benchmark tooling
7. Tune the polling loop
8. Benchmark storage and network paths independently
9. Increase the MSC buffer and measure again
10. Reuse the protocol core for Windows
11. Split Windows support by driver era
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This progression matters because several design decisions only became obvious after measurement.&lt;/p&gt;
&lt;p&gt;For example, the 512-byte bridge limitation looked like the obvious performance problem.&lt;/p&gt;
&lt;p&gt;It was a real limitation — but not the dominant limitation for Wi-Fi receive.&lt;/p&gt;
&lt;p&gt;Likewise, making the poll loop faster improved behavior, but only until the device round-trip became the next ceiling.&lt;/p&gt;
&lt;h2 id=&quot;current-status&quot;&gt;Current status&lt;/h2&gt;
&lt;p&gt;The Linux repository is the most mature part of the project.&lt;/p&gt;
&lt;p&gt;It contains:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux 2.0.x support&lt;/li&gt;
&lt;li&gt;Linux 2.4.x support&lt;/li&gt;
&lt;li&gt;Linux 7.x support&lt;/li&gt;
&lt;li&gt;shared DaynaPORT protocol code&lt;/li&gt;
&lt;li&gt;USB bridge chunking&lt;/li&gt;
&lt;li&gt;diagnostics&lt;/li&gt;
&lt;li&gt;a benchmark runbook&lt;/li&gt;
&lt;li&gt;protocol documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Windows repository currently contains implementations for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows 10/11&lt;/li&gt;
&lt;li&gt;Windows 2000/XP&lt;/li&gt;
&lt;li&gt;Windows NT 4.0&lt;/li&gt;
&lt;li&gt;Windows 9x&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The Windows 10/11 and 2000/XP code paths are structurally complete in the repository, while the NT4 and 9x trees are currently best treated as completed scaffolding.&lt;/p&gt;
&lt;p&gt;Most importantly, the Windows repository still needs real validation with the corresponding Microsoft toolchains and hardware/emulator combinations.&lt;/p&gt;
&lt;p&gt;I consider that distinction important: source code that looks complete is not the same thing as a validated driver.&lt;/p&gt;
&lt;h2 id=&quot;what-i-learned&quot;&gt;What I learned&lt;/h2&gt;
&lt;p&gt;A few lessons from this project are useful beyond BlueSCSI.&lt;/p&gt;
&lt;h3 id=&quot;storage-bridges-and-packet-protocols-have-different-assumptions&quot;&gt;Storage bridges and packet protocols have different assumptions&lt;/h3&gt;
&lt;p&gt;A 512-byte transfer buffer is perfectly reasonable for a storage bridge.&lt;/p&gt;
&lt;p&gt;It becomes a protocol-design problem when the payload is a 1500-byte Ethernet frame.&lt;/p&gt;
&lt;h3 id=&quot;polling-behavior-can-dominate-throughput&quot;&gt;Polling behavior can dominate throughput&lt;/h3&gt;
&lt;p&gt;When there is no receive interrupt, a few milliseconds of unnecessary sleep can matter more than the raw bus bandwidth.&lt;/p&gt;
&lt;h3 id=&quot;measure-layers-independently&quot;&gt;Measure layers independently&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;iperf3&lt;/code&gt; told me the network was slow.&lt;/p&gt;
&lt;p&gt;Block I/O through the same bridge showed that USB itself was much faster.&lt;/p&gt;
&lt;p&gt;That changed where I looked for the bottleneck.&lt;/p&gt;
&lt;h3 id=&quot;keep-protocol-code-separate-from-os-apis&quot;&gt;Keep protocol code separate from OS APIs&lt;/h3&gt;
&lt;p&gt;The shared C89 DaynaPORT core made it possible to support Linux kernels separated by decades and then reuse the same logic in Windows drivers with completely different architectures.&lt;/p&gt;
&lt;h3 id=&quot;old-protocols-are-often-better-documented-in-code-than-in-manuals&quot;&gt;Old protocols are often better documented in code than in manuals&lt;/h3&gt;
&lt;p&gt;The useful details came from several places:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the original Dayna command reference,&lt;/li&gt;
&lt;li&gt;BlueSCSI/ZuluSCSI firmware,&lt;/li&gt;
&lt;li&gt;PiSCSI,&lt;/li&gt;
&lt;li&gt;existing BSD driver behavior,&lt;/li&gt;
&lt;li&gt;and real benchmark results.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No single source tells the whole story.&lt;/p&gt;
&lt;h2 id=&quot;final-result&quot;&gt;Final result&lt;/h2&gt;
&lt;p&gt;My original motivation was to connect physical SCSI hardware — including the Sony SMO-F551 and Iomega Jaz 1 GB / 2 GB drives — when a suitable vintage SCSI controller was not available. BlueSCSI Ultra provided the starting point for that experiment.&lt;/p&gt;
&lt;p&gt;The original DaynaPORT was a SCSI-to-Ethernet adapter designed for computers of the early 1990s.&lt;/p&gt;
&lt;p&gt;BlueSCSI already proved that this protocol can be emulated.&lt;/p&gt;
&lt;p&gt;The next step was to turn that emulation around and make Linux and Windows machines treat it as a network interface — including when the SCSI traffic itself has to cross another 512-byte USB bridge.&lt;/p&gt;
&lt;p&gt;That required more than wrapping &lt;code&gt;READ(6)&lt;/code&gt; and &lt;code&gt;WRITE(6)&lt;/code&gt; in a network driver.&lt;/p&gt;
&lt;p&gt;It required:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;protocol reconstruction,&lt;/li&gt;
&lt;li&gt;SCSI discovery,&lt;/li&gt;
&lt;li&gt;polling and arbitration,&lt;/li&gt;
&lt;li&gt;packet chunking and reassembly,&lt;/li&gt;
&lt;li&gt;cross-kernel Linux work,&lt;/li&gt;
&lt;li&gt;several generations of Windows driver architecture,&lt;/li&gt;
&lt;li&gt;and a lot of benchmarking to determine which bottleneck was actually real.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is what made the project interesting.&lt;/p&gt;
&lt;p&gt;The network protocol is more than thirty years old.&lt;/p&gt;
&lt;p&gt;The engineering problems are not.&lt;/p&gt;
&lt;h2 id=&quot;repositories&quot;&gt;Repositories&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;BlueSCSI v2 experimental branch: &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/BlueSCSI-v2/tree/avkghost/birdge-with-daynaport&quot;&gt;https://github.com/avkghost/BlueSCSI-v2/tree/avkghost/birdge-with-daynaport&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Linux DaynaPORT driver: &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/daynaport-scsilink-linux-driver&quot;&gt;https://github.com/avkghost/daynaport-scsilink-linux-driver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Linux bridge branch: &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/daynaport-scsilink-linux-driver/tree/avkghost/birdge-with-daynaport&quot;&gt;https://github.com/avkghost/daynaport-scsilink-linux-driver/tree/avkghost/birdge-with-daynaport&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Windows DaynaPORT drivers: &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/daynaport-scsilink-windows-driver&quot;&gt;https://github.com/avkghost/daynaport-scsilink-windows-driver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Linux DaynaPORT protocol notes: &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/daynaport-scsilink-linux-driver/blob/main/reference/daynaport.md&quot;&gt;https://github.com/avkghost/daynaport-scsilink-linux-driver/blob/main/reference/daynaport.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Linux bridge benchmark notes: &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/daynaport-scsilink-linux-driver/blob/main/reference/benchmark.md&quot;&gt;https://github.com/avkghost/daynaport-scsilink-linux-driver/blob/main/reference/benchmark.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Recovering Iomega Media in 2026: Building iomega_tools for Jaz, Zip, and QIC-122</title>
        <published>2026-08-09T00:00:00+00:00</published>
        <updated>2026-08-09T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://iamavk.info/blog/iomega-tools/"/>
        <id>https://iamavk.info/blog/iomega-tools/</id>
        
        <content type="html" xml:base="https://iamavk.info/blog/iomega-tools/">&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; This post was created on August 9, 2026 and later expanded as the tooling and hardware tests evolved during August.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The story started with a simple problem: I still had useful SCSI hardware, but no convenient way to connect it to a modern machine.&lt;/p&gt;
&lt;p&gt;That problem eventually led me to experiment with a BlueSCSI Ultra as a bridge to physical SCSI devices. I described that part of the project in &lt;a href=&quot;/blog/bluescsi-daynaport-linux-windows/&quot;&gt;From BlueSCSI Ultra to DaynaPORT: Connecting Legacy SCSI Devices to Linux and Windows&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Among the devices I was testing were Iomega Jaz drives.&lt;/p&gt;
&lt;p&gt;Once the transport problem was mostly solved, another question appeared:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What can I actually do with old Iomega media once Linux can see the drive?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Mounting a filesystem is only part of the answer.&lt;/p&gt;
&lt;p&gt;Old removable storage has its own failure modes, protection states, formatting behavior, vendor commands, damaged sectors, strange bridge interactions, and—in the case of old Iomega backup software—archive formats that modern systems do not understand.&lt;/p&gt;
&lt;p&gt;That became the reason for &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/iomega_tools&quot;&gt;iomega_tools&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;why-iomega-media-is-still-interesting&quot;&gt;Why Iomega media is still interesting&lt;/h2&gt;
&lt;p&gt;Zip and Jaz are good examples of a period when removable storage was not just a generic USB block device.&lt;/p&gt;
&lt;p&gt;At the host level, the drives may look familiar:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/scsi-stack.svg&quot; alt=&quot;Scsi stack&quot; /&gt;&lt;/p&gt;
&lt;p&gt;But there is more going on below the filesystem.&lt;/p&gt;
&lt;p&gt;A drive can have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;media protection modes,&lt;/li&gt;
&lt;li&gt;password-based locking,&lt;/li&gt;
&lt;li&gt;vendor-specific status pages,&lt;/li&gt;
&lt;li&gt;mechanical eject and load behavior,&lt;/li&gt;
&lt;li&gt;FORMAT UNIT behavior,&lt;/li&gt;
&lt;li&gt;firmware-specific command restrictions,&lt;/li&gt;
&lt;li&gt;bad sectors that stall a bridge instead of returning a clean SCSI error,&lt;/li&gt;
&lt;li&gt;and media layouts created by old Iomega software.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A modern Linux kernel can handle the ordinary block-device part.&lt;/p&gt;
&lt;p&gt;For recovery work, however, I wanted to see and control the layer underneath it.&lt;/p&gt;
&lt;h2 id=&quot;the-project&quot;&gt;The project&lt;/h2&gt;
&lt;p&gt;The repository currently builds three independent tools:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/utilities.svg&quot; alt=&quot;Utilities&quot; /&gt;&lt;/p&gt;
&lt;p&gt;There was also an SG_IO tracing path during the reverse-engineering work.&lt;/p&gt;
&lt;p&gt;The code is intentionally implemented as normal Linux userspace tooling. There is no custom kernel driver for Jaz or Zip. Commands are sent through the Linux SCSI generic interface using &lt;code&gt;SG_IO&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That makes the project useful with more than one transport:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/transport.svg&quot; alt=&quot;Transport&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This was important for my setup because BlueSCSI Ultra was already being used as a path from a modern host to physical legacy SCSI hardware.&lt;/p&gt;
&lt;h2 id=&quot;starting-with-scsi-passthrough&quot;&gt;Starting with SCSI passthrough&lt;/h2&gt;
&lt;p&gt;The first layer is deliberately boring: identify the device, send commands, decode sense data, and avoid hiding failures.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./scsi_probe identify /dev/sdX
sudo ./scsi_probe explore /dev/sdX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;scsi_probe&lt;/code&gt; is read-only. I use it when I want to understand what a device accepts before adding behavior to the main tool.&lt;/p&gt;
&lt;p&gt;This became useful beyond Iomega itself. The probe code eventually grew better MMC decoding and DVD structure reporting as I kept using it against other SCSI-class devices.&lt;/p&gt;
&lt;p&gt;But the original purpose was much simpler: find out what the Iomega hardware actually does.&lt;/p&gt;
&lt;h2 id=&quot;reading-files-without-trusting-the-host-mount-path&quot;&gt;Reading files without trusting the host mount path&lt;/h2&gt;
&lt;p&gt;For FAT-family media, &lt;code&gt;iomega_tools&lt;/code&gt; can access the filesystem directly through FatFs.&lt;/p&gt;
&lt;p&gt;That allows operations such as:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./iomega_tools list /dev/sdX -r
sudo ./iomega_tools get /dev/sdX README.TXT
sudo ./iomega_tools get /dev/sdX --all -o recovered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;get&lt;/code&gt; path supports FAT12, FAT16, FAT32 and exFAT directly.&lt;/p&gt;
&lt;p&gt;Long filenames and subdirectories are supported, and filesystem detection does not assume that the filesystem starts at one hard-coded sector.&lt;/p&gt;
&lt;p&gt;This matters because Iomega media can contain an MBR layout rather than a filesystem beginning at LBA 0.&lt;/p&gt;
&lt;p&gt;The code recognizes both:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/filesystem-layout.svg&quot; alt=&quot;Filesystem layout&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The traditional Iomega layout prefers partition table slot 4, but the tools do not require it.&lt;/p&gt;
&lt;h2 id=&quot;recover-first-repair-later&quot;&gt;Recover first, repair later&lt;/h2&gt;
&lt;p&gt;One of the main design rules is that inspection should be read-only unless I explicitly ask the program to modify the cartridge.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./iomega_tools verify /dev/sdX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;verify&lt;/code&gt; combines several checks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;drive and protection-state probing,&lt;/li&gt;
&lt;li&gt;filesystem consistency checking where a safe read-only checker exists,&lt;/li&gt;
&lt;li&gt;and a full read-only surface scan unless it is disabled.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Likewise:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./iomega_tools surface /dev/sdX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is read-only by default.&lt;/p&gt;
&lt;p&gt;This is the behavior I want from a recovery tool. Before changing anything on old media, I want to know what can still be read.&lt;/p&gt;
&lt;h2 id=&quot;when-a-bad-sector-is-not-just-a-bad-sector&quot;&gt;When a bad sector is not just a bad sector&lt;/h2&gt;
&lt;p&gt;Old hardware behind a modern bridge produces failure modes that are easy to miss if software assumes a normal SATA or USB disk.&lt;/p&gt;
&lt;p&gt;A large read may fail even though most of the requested sectors are fine.&lt;/p&gt;
&lt;p&gt;The surface scanner therefore works in stages:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/surface-scan.svg&quot; alt=&quot;Surface scan&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After a chunk failure, the scanner can fall back to single-sector reads for the remaining media.&lt;/p&gt;
&lt;p&gt;This turned out to matter with USB-to-SCSI/IDE paths. One observed bridge configuration rejected large &lt;code&gt;READ(10)&lt;/code&gt; transfers at roughly 240 sectors.&lt;/p&gt;
&lt;p&gt;The tooling therefore cannot simply assume that a failed transfer means every sector inside that transfer is bad.&lt;/p&gt;
&lt;h2 id=&quot;recovering-from-a-stalled-bridge&quot;&gt;Recovering from a stalled bridge&lt;/h2&gt;
&lt;p&gt;There is an even less pleasant case: a failing data phase can leave the transport itself in a bad state.&lt;/p&gt;
&lt;p&gt;Instead of receiving a nice SCSI CHECK CONDITION, the host may get an ioctl-level transport error.&lt;/p&gt;
&lt;p&gt;The recovery path became:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/bridge-recovery.svg&quot; alt=&quot;Bridge recovery&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For USB storage, the reset can become a USB device reset so the bridge toggles the underlying drive reset line.&lt;/p&gt;
&lt;p&gt;If the device node actually disappears with &lt;code&gt;ENODEV&lt;/code&gt;, the scanner can force a USB re-enumeration and resume.&lt;/p&gt;
&lt;p&gt;That sounds excessive for ordinary storage.&lt;/p&gt;
&lt;p&gt;For recovering aging removable media through an adapter stack, it is extremely useful.&lt;/p&gt;
&lt;h2 id=&quot;capacity-without-read-capacity&quot;&gt;Capacity without READ CAPACITY&lt;/h2&gt;
&lt;p&gt;Another interesting hardware detail appeared while working with Zip.&lt;/p&gt;
&lt;p&gt;Normally, a disk utility would ask a SCSI disk for its size using &lt;code&gt;READ CAPACITY&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That assumption does not always work here.&lt;/p&gt;
&lt;p&gt;The Zip drive I tested can reject &lt;code&gt;READ CAPACITY&lt;/code&gt; with an illegal-request sense response.&lt;/p&gt;
&lt;p&gt;So when the user does not specify the scan length, &lt;code&gt;surface&lt;/code&gt; can determine the readable limit by probing the address space instead of depending entirely on the normal capacity command.&lt;/p&gt;
&lt;p&gt;It is a small feature, but it captures an important theme of the project:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;old hardware often works correctly according to its own assumptions, not ours.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;bad-sector-registries-instead-of-a-private-database&quot;&gt;Bad-sector registries instead of a private database&lt;/h2&gt;
&lt;p&gt;Finding damaged sectors is useful.&lt;/p&gt;
&lt;p&gt;Preventing a filesystem from reusing them can be even more useful.&lt;/p&gt;
&lt;p&gt;With an explicit:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./iomega_tools surface /dev/sdX --mark-bad
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the tool can write bad locations into the filesystem&#39;s own bad-block mechanism.&lt;/p&gt;
&lt;p&gt;The implementation is filesystem-specific:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/bad-block-mapping.svg&quot; alt=&quot;Bad block mapping&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Metadata areas and already allocated data are protected from this operation.&lt;/p&gt;
&lt;p&gt;Formats without an appropriate bad-block registry are refused.&lt;/p&gt;
&lt;p&gt;This is intentionally not the default. &lt;code&gt;--mark-bad&lt;/code&gt; modifies the media and requires write access.&lt;/p&gt;
&lt;h2 id=&quot;iomega-protection-modes&quot;&gt;Iomega protection modes&lt;/h2&gt;
&lt;p&gt;Another part of the project came from reverse-engineering IomegaWare behavior.&lt;/p&gt;
&lt;p&gt;The drives support more than a simple writable/read-only switch.&lt;/p&gt;
&lt;p&gt;The tool implements protection operations including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;normal read/write,&lt;/li&gt;
&lt;li&gt;write protection,&lt;/li&gt;
&lt;li&gt;password-protected write access,&lt;/li&gt;
&lt;li&gt;password-based read/write lock,&lt;/li&gt;
&lt;li&gt;temporary unprotect,&lt;/li&gt;
&lt;li&gt;password removal.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./iomega_tools password /dev/sdX -w
sudo ./iomega_tools password /dev/sdX -rw -pw secret
sudo ./iomega_tools password /dev/sdX -u -pw secret
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The password is carried in the vendor command parameter list, matching the behavior reconstructed from Iomega&#39;s old Linux tooling.&lt;/p&gt;
&lt;p&gt;The same research also informed &lt;code&gt;mount&lt;/code&gt;, &lt;code&gt;umount&lt;/code&gt;, &lt;code&gt;eject&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and &lt;code&gt;info&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;formatting-was-more-complicated-than-expected&quot;&gt;Formatting was more complicated than expected&lt;/h2&gt;
&lt;p&gt;At first glance, formatting a Jaz or Zip cartridge sounds like:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/logical-format.svg&quot; alt=&quot;Logical format&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That is only the logical side.&lt;/p&gt;
&lt;p&gt;The tool can create an Iomega-style MBR layout and invoke the appropriate Linux &lt;code&gt;mkfs&lt;/code&gt; implementation:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./iomega_tools format /dev/sdX --fs fat16
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Supported filesystem paths include FAT variants, exFAT, NTFS and ext filesystems.&lt;/p&gt;
&lt;p&gt;But IomegaWare also has a drive-level long-format path.&lt;/p&gt;
&lt;p&gt;That path sends a SCSI &lt;code&gt;FORMAT UNIT&lt;/code&gt; command and then monitors the drive while the physical format is in progress.&lt;/p&gt;
&lt;p&gt;Reverse-engineering the old Linux IomegaWare binaries exposed the command form and the progress calculation used by the original software.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/long-format.svg&quot; alt=&quot;Long format&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The distinction between logical formatting and a real drive-level format turned out to be important.&lt;/p&gt;
&lt;h2 id=&quot;a-bluescsi-specific-surprise-sense-data-can-disappear&quot;&gt;A BlueSCSI-specific surprise: sense data can disappear&lt;/h2&gt;
&lt;p&gt;This was one of the more interesting interactions between the Iomega project and my BlueSCSI work.&lt;/p&gt;
&lt;p&gt;During a long format, the drive reports:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/format-sense.svg&quot; alt=&quot;Format sense&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The format progress value lives in bytes 16-17 of fixed-format sense data.&lt;/p&gt;
&lt;p&gt;The confusing observation was that BlueSCSI debug output could show the expected &lt;code&gt;04/04&lt;/code&gt; sense data, while a later host-side &lt;code&gt;REQUEST SENSE&lt;/code&gt; returned &lt;code&gt;00/00/00&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The repository notes explain why this is plausible.&lt;/p&gt;
&lt;p&gt;In initiator mode, BlueSCSI can issue &lt;code&gt;REQUEST SENSE&lt;/code&gt; internally after a command returns CHECK CONDITION. The bridge consumes and acts on that sense data, but it does not necessarily preserve it for a later &lt;code&gt;REQUEST SENSE&lt;/code&gt; from the Linux host.&lt;/p&gt;
&lt;p&gt;So the path can look like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/consumed-sense.svg&quot; alt=&quot;Consumed sense&quot; /&gt;&lt;/p&gt;
&lt;p&gt;That changed the implementation.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TEST UNIT READY&lt;/code&gt; became the reliable completion condition, while &lt;code&gt;REQUEST SENSE&lt;/code&gt; is used for progress information when it is available.&lt;/p&gt;
&lt;p&gt;This is exactly the kind of issue that appears only when several old and new protocol layers are stacked together.&lt;/p&gt;
&lt;h2 id=&quot;what-iomegaware-was-still-useful-for&quot;&gt;What IomegaWare was still useful for&lt;/h2&gt;
&lt;p&gt;I did not want to copy the original tooling.&lt;/p&gt;
&lt;p&gt;I wanted to recover protocol facts.&lt;/p&gt;
&lt;p&gt;The old IomegaWare for Linux binaries were therefore used as reverse-engineering reference material.&lt;/p&gt;
&lt;p&gt;The work exposed details such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the drive-level &lt;code&gt;FORMAT UNIT&lt;/code&gt; command,&lt;/li&gt;
&lt;li&gt;the 4-byte format parameter list,&lt;/li&gt;
&lt;li&gt;progress calculation from REQUEST SENSE,&lt;/li&gt;
&lt;li&gt;which drive classes are allowed to perform a long format,&lt;/li&gt;
&lt;li&gt;protection state encoding,&lt;/li&gt;
&lt;li&gt;the vendor protection command,&lt;/li&gt;
&lt;li&gt;mount/unmount/eject sequencing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The implementation in this repository is original C/C++ code built from those observed protocol behaviors.&lt;/p&gt;
&lt;p&gt;This distinction matters both technically and legally: old proprietary binaries are reference material, not project dependencies.&lt;/p&gt;
&lt;h2 id=&quot;qic-extract-recovering-old-iomega-backup-archives&quot;&gt;&lt;code&gt;qic_extract&lt;/code&gt;: recovering old Iomega backup archives&lt;/h2&gt;
&lt;p&gt;The third program in the repository solves a different problem.&lt;/p&gt;
&lt;p&gt;Even if the disk itself is readable, old backup software may have stored the useful data inside a QIC-122 archive.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;qic_extract&lt;/code&gt; operates on an image or archive file rather than directly on the SCSI device:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;./qic_extract analyze backup.qic
./qic_extract list backup.qic
./qic_extract extract backup.qic -o recovered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The reverse-engineered format contains structures such as:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/qic-structure.svg&quot; alt=&quot;Qic structure&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The compressed path includes a custom bitstream decoder, segment-chain handling, and UTF-16 filename recovery.&lt;/p&gt;
&lt;p&gt;I find this part of the project particularly interesting because it shows that recovering an old cartridge can involve several independent layers:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/recovery-layers.svg&quot; alt=&quot;Recovery layers&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Being able to read the disk is not the same as being able to recover the files that a user originally stored.&lt;/p&gt;
&lt;h2 id=&quot;the-commit-history-tells-the-real-story&quot;&gt;The commit history tells the real story&lt;/h2&gt;
&lt;p&gt;The repository was created on August 9, 2026 with the three main tools already separated:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/initial-tools.svg&quot; alt=&quot;Initial tools&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The first commit also included the SCSI passthrough layer, filesystem work, QIC extraction, tests, and the SG_IO tracing infrastructure.&lt;/p&gt;
&lt;p&gt;Later the same day the project gained more practical recovery features: image/benchmark streaming, wildcard file recovery, additional filesystem formatting support, and ATA SMART probing through SAT.&lt;/p&gt;
&lt;p&gt;The next rounds of work came directly from hardware behavior:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/timeline.svg&quot; alt=&quot;Timeline&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is one reason I like keeping experimental work in git.&lt;/p&gt;
&lt;p&gt;The interesting history is not just &quot;feature A was added.&quot;&lt;/p&gt;
&lt;p&gt;It shows which assumptions failed when the code met real hardware.&lt;/p&gt;
&lt;h2 id=&quot;the-relationship-with-the-bluescsi-project&quot;&gt;The relationship with the BlueSCSI project&lt;/h2&gt;
&lt;p&gt;The BlueSCSI work and &lt;code&gt;iomega_tools&lt;/code&gt; solve different halves of the same problem.&lt;/p&gt;
&lt;p&gt;BlueSCSI Ultra gave me a practical way to connect physical legacy SCSI hardware when I did not have a suitable vintage host adapter.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;iomega_tools&lt;/code&gt; deals with what happens after that connection exists.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/bluescsi-connection.svg&quot; alt=&quot;Bluescsi connection&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The DaynaPORT project pushed the bridge in another direction: transporting Ethernet frames over SCSI.&lt;/p&gt;
&lt;p&gt;The Iomega project pushes it toward storage recovery and hardware diagnostics.&lt;/p&gt;
&lt;p&gt;Together they turned BlueSCSI Ultra from something I originally associated with SCSI emulation into a much more interesting laboratory for real legacy hardware.&lt;/p&gt;
&lt;h2 id=&quot;current-workflow&quot;&gt;Current workflow&lt;/h2&gt;
&lt;p&gt;For an unknown old cartridge, my preferred workflow is conservative.&lt;/p&gt;
&lt;p&gt;First identify the drive:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./scsi_probe identify /dev/sdX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then inspect the media:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./iomega_tools info /dev/sdX
sudo ./iomega_tools list /dev/sdX -r
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the filesystem is readable, recover the files:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./iomega_tools get /dev/sdX --all -o recovered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then perform a read-only check:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ./iomega_tools verify /dev/sdX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For questionable media, perform an explicit surface scan or image it before attempting repair.&lt;/p&gt;
&lt;p&gt;Only after recovery would I consider write operations such as formatting or marking bad blocks.&lt;/p&gt;
&lt;p&gt;That ordering is intentional:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/iomega-tools/recovery-workflow.svg&quot; alt=&quot;Recovery workflow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Old removable media is not the place for optimistic automation.&lt;/p&gt;
&lt;h2 id=&quot;what-i-learned&quot;&gt;What I learned&lt;/h2&gt;
&lt;p&gt;A few conclusions from the project are broader than Iomega.&lt;/p&gt;
&lt;p&gt;First, &lt;strong&gt;SCSI passthrough is still extremely useful&lt;/strong&gt;. A modern kernel provides the infrastructure needed to investigate devices that were designed decades ago.&lt;/p&gt;
&lt;p&gt;Second, &lt;strong&gt;a bridge is part of the protocol path&lt;/strong&gt;. It can impose transfer limits, transform errors, consume sense data, reset hardware, or disappear during a failed command.&lt;/p&gt;
&lt;p&gt;Third, &lt;strong&gt;filesystem recovery and device recovery are different problems&lt;/strong&gt;. A perfectly valid FAT parser cannot help if the transport stalls on one physical sector.&lt;/p&gt;
&lt;p&gt;Fourth, &lt;strong&gt;old vendor tools are valuable documentation&lt;/strong&gt; when the real protocol documentation is incomplete or difficult to find.&lt;/p&gt;
&lt;p&gt;And finally, &lt;strong&gt;the safest recovery feature is usually the one that does not write anything&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That is why &lt;code&gt;iomega_tools&lt;/code&gt; started as a collection of low-level experiments and gradually became a recovery toolkit.&lt;/p&gt;
&lt;p&gt;The code is available in the &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/iomega_tools&quot;&gt;iomega_tools repository&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Building Cisco-Like Loopback Interfaces on Linux</title>
        <published>2026-01-12T00:00:00+00:00</published>
        <updated>2026-01-12T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://iamavk.info/blog/linux-cisco-like-loopback/"/>
        <id>https://iamavk.info/blog/linux-cisco-like-loopback/</id>
        
        <content type="html" xml:base="https://iamavk.info/blog/linux-cisco-like-loopback/">&lt;p&gt;Cisco routers use loopback interfaces as stable Layer 3 identities. They are independent of physical ports, making them useful for router IDs, routing-protocol endpoints, management addresses, and service IPs. Linux can provide nearly the same functionality without a custom kernel module: use a &lt;strong&gt;dummy&lt;/strong&gt; network interface.&lt;/p&gt;
&lt;p&gt;This article explains how dummy interfaces behave, how to route to them, how to use them with policy routing and FRRouting, and how to create one from C using Netlink. It also covers anycast, VRRP, and the cases where a custom kernel driver or a TUN device is actually necessary.&lt;/p&gt;
&lt;h2 id=&quot;1-dummy-versus-loopback-what-is-actually-routable&quot;&gt;1. Dummy versus loopback: what is actually routable?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/linux-cisco-like-loopback/dummy-routing.svg&quot; alt=&quot;Dummy Routing&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Create a dummy device and assign a host address:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ip link add dummy0 type dummy
sudo ip addr add 10.255.0.1/32 dev dummy0
sudo ip link set dummy0 up
ip addr show dummy0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Linux installs a &lt;strong&gt;local&lt;/strong&gt; route for the address, normally visible in &lt;code&gt;table local&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;ip route show table local
# local 10.255.0.1 dev dummy0 proto kernel scope host src 10.255.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ping 10.255.0.1&lt;/code&gt; succeeds on the host. This does &lt;strong&gt;not&lt;/strong&gt; mean that packets are physically transmitted through &lt;code&gt;dummy0&lt;/code&gt;: packets destined to a local address are delivered by the IP stack. The dummy driver&#39;s transmit path discards packets sent &lt;em&gt;out&lt;/em&gt; through it.&lt;/p&gt;
&lt;p&gt;To reach the address from another machine, that machine or its upstream routers must know a path to the Linux host. For example, if the Linux host has &lt;code&gt;eth0=192.168.1.1&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;# On a second machine on 192.168.1.0/24:
sudo ip route add 10.255.0.1/32 via 192.168.1.1
ping 10.255.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The packet arrives on &lt;code&gt;eth0&lt;/code&gt; and is delivered locally because &lt;code&gt;10.255.0.1&lt;/code&gt; belongs to the host. &lt;strong&gt;IPv4 forwarding is not required&lt;/strong&gt; for this traffic. Forwarding is required only for packets whose destinations are &lt;em&gt;not&lt;/em&gt; local and which must traverse the router.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A dummy interface provides a routable local identity, not a virtual wire. Adding &lt;code&gt;ip route add 10.200.0.0/24 dev dummy0&lt;/code&gt; does not make &lt;code&gt;10.200.0.0/24&lt;/code&gt; reachable: packets routed out of the dummy device are dropped.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;2-source-based-policy-routing&quot;&gt;2. Source-based policy routing&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/linux-cisco-like-loopback/policy-routing.svg&quot; alt=&quot;Policy Routing&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Suppose a router has these addresses:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Device&lt;/th&gt;&lt;th&gt;Address&lt;/th&gt;&lt;th&gt;Role&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;dummy0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;10.255.0.1/32&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Stable identity&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;eth0&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;192.168.1.1/24&lt;/code&gt;&lt;/td&gt;&lt;td&gt;LAN&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;eth1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;172.16.0.2/24&lt;/code&gt;&lt;/td&gt;&lt;td&gt;ISP 1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;eth2&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;172.17.0.2/24&lt;/code&gt;&lt;/td&gt;&lt;td&gt;ISP 2&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;To route traffic &lt;em&gt;sourced from&lt;/em&gt; the dummy address through ISP 2:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ip route add default via 172.17.0.1 dev eth2 table 200
sudo ip rule add from 10.255.0.1/32 lookup 200 priority 1000
ip rule show
ip route get 8.8.8.8 from 10.255.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The expected lookup selects &lt;code&gt;eth2&lt;/code&gt;, with source &lt;code&gt;10.255.0.1&lt;/code&gt;. This only works end-to-end if the upstream network accepts the source address and a return route exists. On multihomed hosts, reverse-path filtering (&lt;code&gt;rp_filter&lt;/code&gt;) can also interfere with asymmetric paths.&lt;/p&gt;
&lt;p&gt;Linux checks the local-routing rule before ordinary user policy rules, so traffic destined to a locally owned dummy address is ordinarily delivered locally.&lt;/p&gt;
&lt;h2 id=&quot;3-stable-bgp-endpoints-with-frrouting&quot;&gt;3. Stable BGP endpoints with FRRouting&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/linux-cisco-like-loopback/bgp-peering.svg&quot; alt=&quot;Bgp Peering&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Loopback addresses make good BGP peering endpoints because the session can survive a change in physical path &lt;em&gt;provided another routed path remains available&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Example topology:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Router A&lt;/th&gt;&lt;th&gt;Router B&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Physical IP&lt;/td&gt;&lt;td&gt;&lt;code&gt;192.168.10.1&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;192.168.10.2&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dummy IP&lt;/td&gt;&lt;td&gt;&lt;code&gt;10.255.0.1/32&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;10.255.0.2/32&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;ASN&lt;/td&gt;&lt;td&gt;&lt;code&gt;65001&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;65002&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;On each router, create the appropriate dummy interface. Before starting the BGP session, provide underlay reachability:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;# Router A
sudo ip route add 10.255.0.2/32 via 192.168.10.2

# Router B
sudo ip route add 10.255.0.1/32 via 192.168.10.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Install FRRouting (&lt;code&gt;sudo apt install frr&lt;/code&gt; on Debian/Ubuntu), enable &lt;code&gt;bgpd&lt;/code&gt; in &lt;code&gt;/etc/frr/daemons&lt;/code&gt;, and configure it with &lt;code&gt;vtysh&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Router A:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;configure terminal
router bgp 65001
 bgp router-id 10.255.0.1
 neighbor 10.255.0.2 remote-as 65002
 neighbor 10.255.0.2 update-source 10.255.0.1
 neighbor 10.255.0.2 ebgp-multihop 2
 address-family ipv4 unicast
  neighbor 10.255.0.2 activate
 exit-address-family
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Router B:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;configure terminal
router bgp 65002
 bgp router-id 10.255.0.2
 neighbor 10.255.0.1 remote-as 65001
 neighbor 10.255.0.1 update-source 10.255.0.2
 neighbor 10.255.0.1 ebgp-multihop 2
 address-family ipv4 unicast
  neighbor 10.255.0.1 activate
 exit-address-family
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check the session with:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo vtysh -c &amp;#39;show bgp summary&amp;#39;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A BGP router ID is only a 32-bit identifier: it need not be assigned to an interface or reachable. Using the loopback address as the router ID is a convention, not a requirement.&lt;/p&gt;
&lt;h2 id=&quot;4-anycast-service-addresses&quot;&gt;4. Anycast service addresses&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/linux-cisco-like-loopback/anycast.svg&quot; alt=&quot;Anycast&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Anycast uses the &lt;strong&gt;same service IP on multiple hosts&lt;/strong&gt;, with the routing system selecting which host receives traffic. For example, three DNS servers might all own &lt;code&gt;10.100.0.53/32&lt;/code&gt; on &lt;code&gt;dummy0&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo ip link add dummy0 type dummy
sudo ip addr add 10.100.0.53/32 dev dummy0
sudo ip link set dummy0 up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each server advertises the prefix via its routing daemon. In FRRouting, the advertisement can include:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;router bgp 65001
 address-family ipv4 unicast
  network 10.100.0.53/32
 exit-address-family
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This assumes BGP neighbors are configured and the prefix exists in the relevant routing table. Routing preference is determined by routing policies and metrics; it is &lt;strong&gt;not necessarily geographic proximity&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A key failure mode is that the dummy interface stays up even when the DNS application fails. A production deployment must link route advertisement or withdrawal to service health. Anycast is particularly useful for stateless services such as DNS; stateful TCP sessions can break if routing shifts an active flow to another node.&lt;/p&gt;
&lt;h2 id=&quot;5-vrrp-and-high-availability&quot;&gt;5. VRRP and high availability&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/linux-cisco-like-loopback/vrrp.svg&quot; alt=&quot;Vrrp&quot; /&gt;&lt;/p&gt;
&lt;p&gt;For a virtual IP on the same Ethernet subnet as clients, Keepalived/VRRP is often a better fit than an always-present dummy address. Consider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server A: &lt;code&gt;192.168.1.10&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Server B: &lt;code&gt;192.168.1.11&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Floating VIP: &lt;code&gt;192.168.1.100&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example Keepalived configuration for Server A:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;vrrp_instance VI_1 {
    state BACKUP
    interface eth0
    virtual_router_id 51
    priority 150
    advert_int 1

    virtual_ipaddress {
        192.168.1.100/24 dev eth0
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Server B uses compatible settings with a lower priority, for example &lt;code&gt;100&lt;/code&gt;. For a VIP on a directly attached subnet, assigning it to &lt;code&gt;eth0&lt;/code&gt; supports normal neighbor discovery. For &lt;strong&gt;routed&lt;/strong&gt; service IPs, a dummy interface plus dynamic routing may be preferable. Keepalived configurations also need appropriate peer communication, health checks, and security controls for production use.&lt;/p&gt;
&lt;h2 id=&quot;6-create-a-dummy-interface-from-c-with-libnl&quot;&gt;6. Create a dummy interface from C with libnl&lt;/h2&gt;
&lt;p&gt;The preferred implementation is a userspace management program using &lt;strong&gt;rtnetlink&lt;/strong&gt;, not a new kernel network driver. Netlink messages include:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Task&lt;/th&gt;&lt;th&gt;Message&lt;/th&gt;&lt;th&gt;Key attributes&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Create interface&lt;/td&gt;&lt;td&gt;&lt;code&gt;RTM_NEWLINK&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;IFLA_IFNAME&lt;/code&gt;, &lt;code&gt;IFLA_LINKINFO&lt;/code&gt;, &lt;code&gt;IFLA_INFO_KIND=dummy&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Bring up&lt;/td&gt;&lt;td&gt;&lt;code&gt;RTM_NEWLINK&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;IFF_UP&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Add IP address&lt;/td&gt;&lt;td&gt;&lt;code&gt;RTM_NEWADDR&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;IFA_LOCAL&lt;/code&gt;, &lt;code&gt;IFA_ADDRESS&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Add route&lt;/td&gt;&lt;td&gt;&lt;code&gt;RTM_NEWROUTE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;RTA_DST&lt;/code&gt;, &lt;code&gt;RTA_GATEWAY&lt;/code&gt;, &lt;code&gt;RTA_OIF&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Add policy rule&lt;/td&gt;&lt;td&gt;&lt;code&gt;RTM_NEWRULE&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;FRA_SRC&lt;/code&gt;, &lt;code&gt;FRA_TABLE&lt;/code&gt;, &lt;code&gt;FRA_PRIORITY&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Delete interface&lt;/td&gt;&lt;td&gt;&lt;code&gt;RTM_DELLINK&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Interface identifier&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Install development packages:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;sudo apt install libnl-3-dev libnl-route-3-dev pkg-config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Minimal implementation (&lt;code&gt;cloop.c&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;c&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;net/if.h&amp;gt;
#include &amp;lt;sys/socket.h&amp;gt;
#include &amp;lt;netlink/netlink.h&amp;gt;
#include &amp;lt;netlink/socket.h&amp;gt;
#include &amp;lt;netlink/route/link.h&amp;gt;
#include &amp;lt;netlink/route/link/dummy.h&amp;gt;
#include &amp;lt;netlink/route/addr.h&amp;gt;
#include &amp;lt;netlink/addr.h&amp;gt;

int main(void)
{
    struct nl_sock *sock = nl_socket_alloc();
    struct rtnl_link *link = NULL, *change = NULL;
    struct rtnl_addr *addr = NULL;
    struct nl_addr *ip = NULL;
    int err = -1, index;

    if (!sock) return 1;
    err = nl_connect(sock, NETLINK_ROUTE);
    if (err &amp;lt; 0) goto out;

    link = rtnl_link_dummy_alloc();
    if (!link) { err = -NLE_NOMEM; goto out; }
    rtnl_link_set_name(link, &amp;quot;cloop0&amp;quot;);
    err = rtnl_link_add(sock, link, NLM_F_CREATE | NLM_F_EXCL);
    if (err &amp;lt; 0) goto out;

    index = rtnl_link_name2i(sock, &amp;quot;cloop0&amp;quot;);
    if (index &amp;lt;= 0) { err = -NLE_OBJ_NOTFOUND; goto out; }

    addr = rtnl_addr_alloc();
    if (!addr) { err = -NLE_NOMEM; goto out; }
    err = nl_addr_parse(&amp;quot;10.255.0.1/32&amp;quot;, AF_INET, &amp;amp;ip);
    if (err &amp;lt; 0) goto out;
    rtnl_addr_set_ifindex(addr, index);
    err = rtnl_addr_set_local(addr, ip);
    if (err &amp;lt; 0) goto out;
    err = rtnl_addr_add(sock, addr, 0);
    if (err &amp;lt; 0) goto out;

    change = rtnl_link_alloc();
    if (!change) { err = -NLE_NOMEM; goto out; }
    rtnl_link_set_flags(change, IFF_UP);
    err = rtnl_link_change(sock, link, change, 0);
    if (err &amp;lt; 0) goto out;

    puts(&amp;quot;Created cloop0: 10.255.0.1/32&amp;quot;);
out:
    if (err &amp;lt; 0) fprintf(stderr, &amp;quot;Netlink error: %s\n&amp;quot;, nl_geterror(err));
    if (change) rtnl_link_put(change);
    if (ip) nl_addr_put(ip);
    if (addr) rtnl_addr_put(addr);
    if (link) rtnl_link_put(link);
    nl_socket_free(sock);
    return err &amp;lt; 0 ? 1 : 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compile and run:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;gcc cloop.c -o cloop $(pkg-config --cflags --libs libnl-3.0 libnl-route-3.0)
sudo ./cloop
ip -d link show cloop0
ip addr show cloop0
ping 10.255.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The program requires &lt;code&gt;CAP_NET_ADMIN&lt;/code&gt; in its network namespace. It intentionally uses &lt;code&gt;NLM_F_EXCL&lt;/code&gt;, so rerunning it while &lt;code&gt;cloop0&lt;/code&gt; exists returns an error. This is a minimal demonstration: a production daemon should reconcile desired state, handle Netlink notifications, validate inputs, and roll back partially applied changes.&lt;/p&gt;
&lt;h2 id=&quot;7-do-you-need-a-kernel-module&quot;&gt;7. Do you need a kernel module?&lt;/h2&gt;
&lt;p&gt;Usually not. A kernel module using &lt;code&gt;alloc_netdev()&lt;/code&gt; and &lt;code&gt;register_netdev()&lt;/code&gt; can expose a new device, but it introduces kernel API compatibility and lifecycle concerns. A device built with &lt;code&gt;ether_setup()&lt;/code&gt; is Ethernet-like, not an exact clone of Linux &lt;code&gt;lo&lt;/code&gt; (&lt;code&gt;ARPHRD_LOOPBACK&lt;/code&gt; with loopback-specific semantics).&lt;/p&gt;
&lt;p&gt;Use existing device types unless you truly need new transmit/receive behavior:&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Requirement&lt;/th&gt;&lt;th&gt;Recommended mechanism&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Stable router IP / Cisco-style Loopback&lt;/td&gt;&lt;td&gt;&lt;code&gt;dummy&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Custom source-based routing&lt;/td&gt;&lt;td&gt;&lt;code&gt;ip rule&lt;/code&gt; / rtnetlink&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;BGP/OSPF control plane&lt;/td&gt;&lt;td&gt;FRRouting&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Service anycast&lt;/td&gt;&lt;td&gt;&lt;code&gt;dummy&lt;/code&gt; + BGP + health checks&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Active/passive floating LAN VIP&lt;/td&gt;&lt;td&gt;Keepalived / VRRP&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Userspace processing of IP packets&lt;/td&gt;&lt;td&gt;TUN&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Virtual Ethernet frames in userspace&lt;/td&gt;&lt;td&gt;TAP&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Routed path between hosts&lt;/td&gt;&lt;td&gt;GRE, IPIP, WireGuard, etc.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Novel device-specific kernel TX/RX semantics&lt;/td&gt;&lt;td&gt;Custom &lt;code&gt;net_device&lt;/code&gt; driver&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;8-suggested-architecture-for-a-router-like-product&quot;&gt;8. Suggested architecture for a router-like product&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/linux-cisco-like-loopback/c-architecture.svg&quot; alt=&quot;C Architecture&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A clean Cisco-like management architecture has three layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;C management daemon:&lt;/strong&gt; parses commands such as &lt;code&gt;interface Loopback0&lt;/code&gt;, stores desired configuration, creates dummy interfaces, assigns addresses, and watches rtnetlink notifications.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linux kernel:&lt;/strong&gt; owns the interfaces, local and forwarding routing tables, policy rules, and packet forwarding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FRRouting:&lt;/strong&gt; implements BGP/OSPF, route advertisements, and convergence.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example, a CLI command &lt;code&gt;interface Loopback0&lt;/code&gt; could map to &lt;code&gt;dummy&lt;/code&gt; device &lt;code&gt;lo0&lt;/code&gt; or &lt;code&gt;Loopback0&lt;/code&gt;; &lt;code&gt;ip address 10.255.0.1/32&lt;/code&gt; maps to &lt;code&gt;RTM_NEWADDR&lt;/code&gt;; and routing protocol configuration is delegated to FRRouting.&lt;/p&gt;
&lt;p&gt;This approach preserves the operational advantages of Cisco loopbacks without maintaining a custom kernel driver. The central rule is simple: &lt;strong&gt;dummy interfaces anchor addresses; physical interfaces and tunnels carry transit traffic.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://man7.org/linux/man-pages/man8/ip-link.8.html&quot;&gt;Linux &lt;code&gt;ip-link(8)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://man7.org/linux/man-pages/man8/ip-route.8.html&quot;&gt;Linux &lt;code&gt;ip-route(8)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://man7.org/linux/man-pages/man8/ip-rule.8.html&quot;&gt;Linux &lt;code&gt;ip-rule(8)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://man7.org/linux/man-pages/man7/rtnetlink.7.html&quot;&gt;Linux rtnetlink(7)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://docs.frrouting.org/&quot;&gt;FRRouting documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.infradead.org/~tgr/libnl/&quot;&gt;libnl documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https://www.keepalived.org/&quot;&gt;Keepalived documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Restoring a Roland MT-120: Building an Arduino Floppy Drive Tester</title>
        <published>2025-08-25T00:00:00+00:00</published>
        <updated>2025-08-25T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://iamavk.info/blog/roland-mt120-fdtester/"/>
        <id>https://iamavk.info/blog/roland-mt120-fdtester/</id>
        
        <content type="html" xml:base="https://iamavk.info/blog/roland-mt120-fdtester/">&lt;p&gt;An old Roland MT-120 was the starting point for an unexpectedly interesting embedded project. I wanted to restore the instrument, but its floppy drive was not working correctly. Before replacing parts or making mechanical adjustments, I needed a way to control the drive independently and see what its signals were doing.&lt;/p&gt;
&lt;p&gt;That led to &lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/FDTester&quot;&gt;FDTester&lt;/a&gt;&lt;/strong&gt;: an open-source Arduino-based tester for standard PC-style 3.5-inch floppy drives. Rather than attempting to read files, it exercises the drive&#39;s mechanical and electrical interface directly.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; FDTester is a mechanical and electrical diagnostic tool, &lt;strong&gt;not&lt;/strong&gt; a complete floppy disk controller. It does not decode MFM sectors, read filesystem data, or prove that the magnetic read channel works.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-problem-a-drive-that-could-not-read-other-disks&quot;&gt;The problem: a drive that could not read other disks&lt;/h2&gt;
&lt;p&gt;The Roland MT-120 restoration raised a practical question: was the failure caused by the drive mechanics, its interface signals, the spindle speed, or head positioning? In particular, &lt;strong&gt;Track 0 alignment&lt;/strong&gt; was a challenge: the drive did not reliably read disks written by other devices.&lt;/p&gt;
&lt;p&gt;A drive can respond to motor and seek commands while still being unable to read a disk correctly. A Track 0 sensor confirms that the mechanism reached its reference position; it does not, by itself, prove correct radial head alignment or interchangeability with another drive. That distinction shaped the test strategy.&lt;/p&gt;
&lt;h2 id=&quot;architecture-separating-control-from-the-host&quot;&gt;Architecture: separating control from the host&lt;/h2&gt;
&lt;p&gt;FDTester uses an Arduino as a small, dedicated interface controller. A serial terminal sends commands; the Arduino drives the floppy interface and reports status and measurements. The repository documents an &lt;strong&gt;Arduino Uno / ATmega328P&lt;/strong&gt; reference setup, while its project description also mentions Mega compatibility.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/roland-mt120-fdtester/architecture.svg&quot; alt=&quot;FDTester system architecture: serial terminal, Arduino, buffered control lines, floppy drive and external power&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The repository&#39;s validated target is an &lt;strong&gt;Epson SMD-300&lt;/strong&gt;, configured for DS0. This is the documented test configuration; compatibility with other drives depends on their pinout, jumpers, and electrical characteristics.&lt;/p&gt;
&lt;p&gt;The floppy drive has its &lt;strong&gt;own external 5 V supply&lt;/strong&gt; (the README specifies approximately 1 A capability), with a common ground shared with the Arduino. Do not power the motor from the Arduino USB port. A ULN2803 or appropriate transistor buffering is recommended for the active-low interface lines; check your exact drive&#39;s electrical requirements before connecting it.&lt;/p&gt;
&lt;h2 id=&quot;the-signals-that-matter&quot;&gt;The signals that matter&lt;/h2&gt;
&lt;p&gt;A classic 34-pin floppy interface gives direct access to the most useful diagnostic controls and feedback signals:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/roland-mt120-fdtester/interface.svg&quot; alt=&quot;Simplified drive interface and documented Arduino Uno pin mapping&quot; /&gt;&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Floppy pin&lt;/th&gt;&lt;th&gt;Signal&lt;/th&gt;&lt;th&gt;Arduino Uno pin&lt;/th&gt;&lt;th&gt;Diagnostic purpose&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;Drive Select 0&lt;/td&gt;&lt;td&gt;D7&lt;/td&gt;&lt;td&gt;Select the target drive&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;16&lt;/td&gt;&lt;td&gt;Motor Enable&lt;/td&gt;&lt;td&gt;D6&lt;/td&gt;&lt;td&gt;Start and stop spindle&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;18&lt;/td&gt;&lt;td&gt;Direction&lt;/td&gt;&lt;td&gt;D5&lt;/td&gt;&lt;td&gt;Set seek direction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;20&lt;/td&gt;&lt;td&gt;Step&lt;/td&gt;&lt;td&gt;D4&lt;/td&gt;&lt;td&gt;Advance the head by one track&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;32&lt;/td&gt;&lt;td&gt;Side Select&lt;/td&gt;&lt;td&gt;D8&lt;/td&gt;&lt;td&gt;Select head 0 or 1&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;8&lt;/td&gt;&lt;td&gt;Index&lt;/td&gt;&lt;td&gt;D2 / INT0&lt;/td&gt;&lt;td&gt;Count revolutions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;26&lt;/td&gt;&lt;td&gt;Track 0&lt;/td&gt;&lt;td&gt;D11&lt;/td&gt;&lt;td&gt;Detect home position&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;28&lt;/td&gt;&lt;td&gt;Write Protect&lt;/td&gt;&lt;td&gt;D12&lt;/td&gt;&lt;td&gt;Inspect disk protection status&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;34&lt;/td&gt;&lt;td&gt;Disk Change&lt;/td&gt;&lt;td&gt;D10&lt;/td&gt;&lt;td&gt;Detect media change (default configuration)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;24&lt;/td&gt;&lt;td&gt;Write Gate&lt;/td&gt;&lt;td&gt;D9&lt;/td&gt;&lt;td&gt;Dangerous write control; locked by default&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; The table reflects the repository&#39;s Epson SMD-300 / DS0 wiring. Odd-numbered connector pins are grounds. Pin 34 may have a different function on other drives or jumper settings. Pin 2 (Density Select) is left unconnected in the documented setup.&lt;/p&gt;
&lt;h2 id=&quot;diagnostic-workflow&quot;&gt;Diagnostic workflow&lt;/h2&gt;
&lt;p&gt;I wanted a repeatable way to distinguish an interface problem from a mechanical problem before attempting fine adjustment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/roland-mt120-fdtester/workflow.svg&quot; alt=&quot;Suggested step-by-step diagnostic workflow, including Track 0 verification and separate alignment checks&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The basic sequence is straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Check wiring and drive selection.&lt;/strong&gt; Confirm common ground, the external supply, and the DS0 jumper configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spin the motor.&lt;/strong&gt; Confirm that the mechanism starts reliably.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure rotational speed.&lt;/strong&gt; Count Index pulses over a known interval.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Home the head.&lt;/strong&gt; Move toward Track 0 and inspect the Track 0 signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exercise stepping.&lt;/strong&gt; Seek to known tracks and sweep across a range to reveal mechanical issues.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inspect status lines.&lt;/strong&gt; Check Write Protect and Disk Change while inserting or removing media.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluate disk interchangeability separately.&lt;/strong&gt; If disks from other devices still cannot be read, investigate mechanical alignment and the read path using appropriate reference media and equipment.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;rpm-measurement-using-the-index-pulse&quot;&gt;RPM measurement using the Index pulse&lt;/h3&gt;
&lt;p&gt;The Index signal provides one pulse per revolution on a typical drive. If &lt;code&gt;N&lt;/code&gt; pulses arrive during a measurement window of &lt;code&gt;T&lt;/code&gt; seconds, rotational speed is approximately:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;RPM = 60 × N / T&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;For example, 15 pulses over three seconds corresponds to about 300 RPM. That is a calculation example, &lt;strong&gt;not&lt;/strong&gt; a recorded measurement from the Roland restoration.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/roland-mt120-fdtester/rpm.svg&quot; alt=&quot;Index pulses and a three-second measurement window for a 300 RPM example&quot; /&gt;&lt;/p&gt;
&lt;h3 id=&quot;track-0-homing-is-not-alignment&quot;&gt;Track 0: homing is not alignment&lt;/h3&gt;
&lt;p&gt;The most interesting part of this repair was Track 0. Homing tells the firmware where the mechanism&#39;s reference sensor is triggered. However, an incorrectly positioned head assembly can still produce a plausible Track 0 signal and fail to read disks recorded by another machine.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://iamavk.info/blog/roland-mt120-fdtester/track0.svg&quot; alt=&quot;Conceptual distinction between Track 0 sensor detection and physical head alignment&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is why I treated &lt;strong&gt;Track 0 detection&lt;/strong&gt; and &lt;strong&gt;head alignment&lt;/strong&gt; as different problems. FDTester can help with the former and with repeatable movement tests; it cannot directly measure magnetic head alignment or validate sector decoding. Alignment should be checked against appropriate reference media or specialist equipment rather than inferred solely from the Track 0 switch.&lt;/p&gt;
&lt;h2 id=&quot;using-fdtester&quot;&gt;Using FDTester&lt;/h2&gt;
&lt;p&gt;The firmware exposes a simple &lt;strong&gt;115200-baud serial console&lt;/strong&gt;. Once the drive is wired and powered correctly, these are useful commands from the published README:&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;text&quot;&gt;P                  # Report status
M                  # Toggle motor
R 3                # Measure RPM for 3 seconds
H                  # Home to Track 0
G 40               # Seek to absolute track 40
S 1                # Select side 1
T 0 79 100         # Sweep tracks 0–79, 100 ms dwell
RATE 6             # Set stepping interval to 6 ms
?                  # Show help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The firmware also includes Write Gate controls protected by an explicit &lt;code&gt;UNLOCK&lt;/code&gt; command. &lt;strong&gt;Do not unlock or assert Write Gate on a disk containing valuable data.&lt;/strong&gt; None of the diagnostic steps above requires writing to the disk.&lt;/p&gt;
&lt;p&gt;See the &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/FDTester/blob/main/FDTester.ino&quot;&gt;source sketch&lt;/a&gt; and &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/FDTester#readme&quot;&gt;README&lt;/a&gt; for the implementation and full command reference.&lt;/p&gt;
&lt;h2 id=&quot;what-the-tester-can-and-cannot-tell-you&quot;&gt;What the tester can—and cannot—tell you&lt;/h2&gt;
&lt;p&gt;FDTester is useful for isolating failures in spindle control, stepping, drive selection, home sensing, status wiring, and basic timing. It also gives you a repeatable way to move the mechanism while inspecting its behavior.&lt;/p&gt;
&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; establish that the read head, analog read channel, MFM decoding, or media are good. If the drive responds correctly to all commands but cannot read disks from other devices, the next step is targeted read-path and alignment diagnosis, not an assumption that the controller has fixed the problem.&lt;/p&gt;
&lt;h2 id=&quot;lessons-learned&quot;&gt;Lessons learned&lt;/h2&gt;
&lt;p&gt;The most valuable outcome was not a new storage controller; it was &lt;strong&gt;observability&lt;/strong&gt;. Breaking the drive into independent signals and commands turned a vague &quot;doesn&#39;t read disks&quot; failure into a sequence of testable questions.&lt;/p&gt;
&lt;p&gt;For me, the Roland MT-120 restoration was also a reminder of why embedded engineering is rewarding: software, timing, electrical interfaces, and mechanics all meet at the same debugging bench.&lt;/p&gt;
&lt;h2 id=&quot;related-projects&quot;&gt;Related projects&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://iamavk.info/blog/bluescsi-daynaport-linux-windows/&quot;&gt;From BlueSCSI Ultra to DaynaPORT&lt;/a&gt;: the same kind of hands-on work on legacy SCSI devices, from the controller side.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://iamavk.info/blog/iomega-tools/&quot;&gt;Recovering Iomega Media in 2026&lt;/a&gt;: the floppy-era hardware story continues with Jaz and Zip drives and QIC-122 tape.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/FDTester&quot;&gt;FDTester on GitHub&lt;/a&gt; · &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/FDTester/blob/main/FDTester.ino&quot;&gt;Arduino sketch&lt;/a&gt; · &lt;a rel=&quot;external&quot; href=&quot;https://github.com/avkghost/FDTester/blob/main/LICENSE&quot;&gt;GPL-3.0 license&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Diagrams in this article are conceptual illustrations, not oscilloscope captures or a certified wiring schematic. Confirm connections against the repository and your drive&#39;s technical manual.&lt;/em&gt;&lt;/p&gt;
</content>
        
    </entry>
</feed>
