<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yash Kumar Saini</title>
    <description>The latest articles on DEV Community by Yash Kumar Saini (@yashksaini).</description>
    <link>https://dev.to/yashksaini</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1242333%2F27bdd497-8c93-4290-a901-94eec3759056.jpeg</url>
      <title>DEV Community: Yash Kumar Saini</title>
      <link>https://dev.to/yashksaini</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yashksaini"/>
    <language>en</language>
    <item>
      <title>Dev log #18 From Redis Brokers to Knowledge Graphs: A 177-Commit Sprint</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sun, 23 Aug 2026 16:26:50 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-18-from-redis-brokers-to-knowledge-graphs-a-177-commit-sprint-1mni</link>
      <guid>https://dev.to/yashksaini/dev-log-18-from-redis-brokers-to-knowledge-graphs-a-177-commit-sprint-1mni</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Hit a perfect 7-day streak this week, shipping 177 commits across systems architecture and personal tooling. Between hardening Redis brokers and fixing terminal hijacking in tmux, I managed to push nearly 7k lines of code across eight different projects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;There’s a specific kind of satisfaction that comes from a perfect 7-day streak where the flow state just doesn't quit. This week was a massive 177-commit sprint, largely split between hardening the infrastructure for Reachable and refining the data pipelines in my opportunity-radar. I spent a lot of time in the "maintenance and hardening" phase—fixing memory leaks, bounding caches, and making sure my CLI tools don't hijack my terminal. With nearly 7,000 lines added and 2,700 removed, the codebase feels significantly more robust than it did last Monday.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reachable: Security and Performance
&lt;/h3&gt;

&lt;p&gt;I spent the lion's share of my time (84 commits!) on &lt;strong&gt;Reachable&lt;/strong&gt;. Most of the work was centered around the &lt;code&gt;worker/reachable&lt;/code&gt; and &lt;code&gt;web/components&lt;/code&gt; areas. One of the more critical changes was in the ingest layer: I bounded the packument cache by size. If you've ever dealt with massive metadata ingestion, you know how quickly an unbounded cache can eat your RAM, so getting that logic in place was a huge win for stability.&lt;/p&gt;

&lt;p&gt;On the security side, I reworked how we handle keys. I updated the system to show minted keys as masked values instead of printing them whole (basic security hygiene, but easy to overlook early on) and added the ability for testers to revoke keys immediately. Waiting out a TTL just to test a revocation flow is a special kind of developer hell, so I’m glad that’s behind me. I also did some housekeeping by stripping out internal working notes from the docs and pointing everything to the proper references.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opportunity-Radar: Scraping and UI
&lt;/h3&gt;

&lt;p&gt;My Python-based &lt;strong&gt;opportunity-radar&lt;/strong&gt; saw 40 commits this week. Scraping is always a game of whack-a-mole with edge cases. I spent a good chunk of Tuesday fixing a bug where neighboring listings were getting "glued" onto titles—one of those classic regex-adjacent headaches. &lt;/p&gt;

&lt;p&gt;To make the data actually usable, I implemented paging for the opportunities table and derived category filters directly from the data instead of hardcoding them. I also hooked up the dashboard to supply run tokens directly, which makes the manual trigger flow much smoother. It’s starting to feel like a real product rather than just a collection of scripts.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Personal Stack: Obsidian and Portfolio
&lt;/h3&gt;

&lt;p&gt;I’m a big believer in "sharpening the saw," so I put 27 commits into my &lt;strong&gt;obsidian-vault&lt;/strong&gt;. I’ve been working on syncing the graph zoom and UI state across devices. There’s something deeply nerdy but satisfying about getting your &lt;code&gt;#stream&lt;/code&gt; and &lt;code&gt;#reference&lt;/code&gt; tags to color-coordinate perfectly across your knowledge base.&lt;/p&gt;

&lt;p&gt;I also gave my &lt;strong&gt;portfolio&lt;/strong&gt; some love. I’m a bit of a stickler for UX, so I spent time enlarging the ⌘K hint chips in the header so they actually look like controls. I also fixed a gnarly bug in the hero section where scrub audio would stay locked after a page reload. If you’re going to have audio on a dev portfolio, it has to work perfectly, or it’s just noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests
&lt;/h2&gt;

&lt;p&gt;I opened 6 PRs this week, and the results were a mix of "shipped it" and "back to the drawing board."&lt;/p&gt;

&lt;p&gt;Over at &lt;strong&gt;sixb&lt;/strong&gt;, I had two successful merges. One was a critical fix for the broker: I bounded a Redis command so a single stuck reply couldn't hang the entire publication pipeline. The other involved releasing event subscriptions for action runs that settled late—essentially plugging a memory leak I’d been hunting for a while. I did close one massive PR (+2,030 lines) regarding connector stores and sealed credentials without merging it. It was a big architectural pivot, and after looking at the diff, I realized I wanted to approach the credential sealing differently. Sometimes the best thing you can do for a codebase is realize a 2,000-line change isn't the right one.&lt;/p&gt;

&lt;p&gt;In the &lt;strong&gt;agent-orchestrator&lt;/strong&gt; repo, I’ve got two PRs currently open. They both deal with tmux integration. One fixes a bug where destroying a session would accidentally hand the orchestrator's terminal over to another session, and the other handles resuming agents when using a non-default tmux base-index. If you’ve ever had your terminal focus stolen by a background process, you know why these are high priority for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues &amp;amp; Discussions
&lt;/h2&gt;

&lt;p&gt;I was pretty active on the issue tracker this week, opening 5 issues across various projects. &lt;/p&gt;

&lt;p&gt;The most interesting one was in &lt;strong&gt;hydradb&lt;/strong&gt;, where I noticed an inconsistency in how HTTP and Bolt map &lt;code&gt;CLASS_FRESHNESS&lt;/code&gt; errors. One treats them as retryable, the other doesn't. That’s the kind of subtle bug that causes "it works on my machine" tickets, so I’m hoping to dive into the fix for that soon.&lt;/p&gt;

&lt;p&gt;I also tracked down a CPU spike in &lt;strong&gt;nezuko&lt;/strong&gt; where the runtime was spinning at 100% while idle and leaking file descriptors. There’s no faster way to lose my trust in a tool than having it cook my CPU while it’s doing literally nothing, so getting that issue documented (and subsequently closed) was a relief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;This was a true polyglot week. &lt;strong&gt;TypeScript&lt;/strong&gt; was my primary driver for the web and worker logic in Reachable and sixb, while &lt;strong&gt;Python&lt;/strong&gt; handled the heavy lifting for the data scraping in opportunity-radar. I also dipped into &lt;strong&gt;Go&lt;/strong&gt; to add a &lt;code&gt;AO_DISABLE_GPU&lt;/code&gt; escape hatch for the agent-orchestrator—sometimes you just need a way to bypass broken drivers when you're working on desktop tools.&lt;/p&gt;

&lt;p&gt;The stats tell the story: 177 commits over 7 days. My add/delete ratio was about 2.5:1. While I love a good refactoring week where I delete more than I add, this week was clearly about expansion. Building out the paging systems, the credential stores (even the ones I pivoted on), and the dashboard features meant a lot of new logic hitting the disk.&lt;/p&gt;

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

&lt;p&gt;Next week, I’m focusing on the tmux fixes in agent-orchestrator. I want to get those PRs merged so the terminal handling is rock solid. I’m also planning to revisit the credential sealing logic I backed out of in sixb—I have a better idea now of how to handle the "sealed" part without making the storage layer too brittle.&lt;/p&gt;

&lt;p&gt;Consistency is key, and hitting a 7-day streak feels like I've got the momentum I need heading into September. See you on the next push!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>contentwriting</category>
      <category>productivity</category>
      <category>cli</category>
    </item>
    <item>
      <title>One rented /24 could eclipse a Kademlia node. Now it takes ten.</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:56:59 +0000</pubDate>
      <link>https://dev.to/yashksaini/one-rented-24-could-eclipse-a-kademlia-node-now-it-takes-ten-121p</link>
      <guid>https://dev.to/yashksaini/one-rented-24-could-eclipse-a-kademlia-node-now-it-takes-ten-121p</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setting
&lt;/h2&gt;

&lt;p&gt;py-libp2p speaks Kademlia — a distributed hash table where every node keeps a routing table of &lt;em&gt;other&lt;/em&gt; nodes, bucketed by how far their IDs sit from its own. You find a peer or a record by asking the nodes closest to the target, who point you closer, and closer, until you arrive. It's elegant, and it works because of one quiet assumption: &lt;strong&gt;the peers in your routing table are a fair sample of the network.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An eclipse attack breaks exactly that assumption. If an attacker can get &lt;em&gt;their&lt;/em&gt; nodes into enough of your routing buckets — specifically the closest-K slots for a target key — they don't need to break any crypto. They just surround you. Every lookup you make gets answered by them. They can hide records, feed you stale routing, or silently partition you from the real DHT. You're still online. You're still "connected." You're just connected to a lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signing authenticates the record, not the routing table
&lt;/h2&gt;

&lt;p&gt;The reason this one nags at me is that the DHT already has an integrity story: records are &lt;strong&gt;signed&lt;/strong&gt;. So my instinct — and I don't think I'm alone — was that the surface was mostly covered. If a malicious peer can't forge a record, how much damage can it do?&lt;/p&gt;

&lt;p&gt;A lot, it turns out, because signing and eclipse answer different questions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wg3k1oowvnql6fwstpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wg3k1oowvnql6fwstpj.png" alt="Two different questions — signing only answers one" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Record signing&lt;/strong&gt; authenticates &lt;em&gt;content&lt;/em&gt;. Hand me a DHT record and I can verify the owning key produced it. I can't be fed forged values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eclipse&lt;/strong&gt; attacks &lt;em&gt;membership&lt;/em&gt;. The attacker never forges anything. They make sure the only peers you ever &lt;em&gt;ask&lt;/em&gt; are theirs, then answer with perfectly valid, perfectly signed records — just never the &lt;em&gt;whole&lt;/em&gt; set. Withholding has no signature. Silence has no signature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the real surface isn't the record. It's &lt;strong&gt;how a peer earns a slot in a k-bucket.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the attack to measure it
&lt;/h2&gt;

&lt;p&gt;I didn't want to argue this from a whiteboard, so I built an eclipse-attack simulation inside py-libp2p — it lives at &lt;code&gt;tests/examples/attack_simulation/eclipse_attack/&lt;/code&gt;. It spins up a network of honest &lt;code&gt;KadDHT&lt;/code&gt; nodes, introduces malicious peers that &lt;strong&gt;flood the honest nodes' routing tables and poison DHT entries&lt;/strong&gt;, and then measures what actually degrades: a &lt;code&gt;RealAttackMetrics&lt;/code&gt; collector runs real lookups against the network and records the success rate as the attack takes hold.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/examples/attack_simulation/eclipse_attack/attack_scenarios.py
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open_nursery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;nursery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;mp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;malicious_peers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;honest_peers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;nursery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_soon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poison_dht_entries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;nursery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_soon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flood_peer_table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                   &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;honest_peer_tables&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point of the harness is to turn "eclipse is possible" into a number: &lt;strong&gt;what fraction of lookups can an attacker capture, and how does that change once the routing table stops admitting them so cheaply?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrbt1e8pk54a0x6fvc16.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcrbt1e8pk54a0x6fvc16.png" alt="How an eclipse fills a k-bucket — diverse subnets vs one attacker subnet" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The point of the harness is to turn "eclipse is possible" into a number. I drove the fork's real &lt;code&gt;KBucket.add_peer&lt;/code&gt; (&lt;code&gt;k = 20&lt;/code&gt;) with Sybil peers from attacker-controlled &lt;code&gt;/24&lt;/code&gt;s, running it with &lt;code&gt;MAX_PEERS_PER_SUBNET&lt;/code&gt; set to &lt;code&gt;0&lt;/code&gt; (the pre-#1399 behaviour) versus its default of &lt;code&gt;2&lt;/code&gt;. This is a component-level measurement — it isolates exactly what the admission rule changes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attacker capability&lt;/th&gt;
&lt;th&gt;Pre-#1399 (no filter)&lt;/th&gt;
&lt;th&gt;With #1399 (max 2 / subnet)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bucket slots captured — flood from a single &lt;code&gt;/24&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20 / 20 (100%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 / 20 (10%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distinct &lt;code&gt;/24&lt;/code&gt;s needed to fully own a 20-slot bucket&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Share of slots taken when the bucket already holds 10 honest, subnet-diverse peers&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;50%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Before the fix, one rented &lt;code&gt;/24&lt;/code&gt; owns the whole bucket. After it, an attacker needs &lt;strong&gt;ten&lt;/strong&gt; genuinely distinct networks to do the same — the cost moved from "grind cheap IDs" to "rent diverse address space," which is the entire point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause
&lt;/h2&gt;

&lt;p&gt;In vanilla Kademlia, a peer earns a bucket slot mostly by being live and having an ID that lands in range. Node IDs are cheap — you can grind as many as you want. So an attacker spins up a Sybil fleet and, crucially, parks them at &lt;strong&gt;IP addresses in a subnet they control&lt;/strong&gt;. Their cheap resource is node IDs; the resource they'd actually have to &lt;em&gt;spend&lt;/em&gt; is distinct network positions — and nothing in the base admission rule was pricing that.&lt;/p&gt;

&lt;p&gt;A rented cloud block is typically a &lt;strong&gt;/24&lt;/strong&gt;, not a scattering of unrelated addresses. That's the lever: if admission is priced in &lt;em&gt;distinct subnets&lt;/em&gt; rather than &lt;em&gt;distinct IDs&lt;/em&gt;, surrounding a node suddenly costs real, diverse address space the attacker can't cheaply conjure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix — #1399: enforce IP subnet diversity in k-buckets
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/libp2p/py-libp2p/pull/1399" rel="noopener noreferrer"&gt;libp2p/py-libp2p#1399&lt;/a&gt; (closes &lt;a href="https://github.com/libp2p/py-libp2p/issues/1383" rel="noopener noreferrer"&gt;#1383&lt;/a&gt;) makes &lt;code&gt;KBucket.add_peer&lt;/code&gt; reject a new peer when its globally-routable &lt;strong&gt;/24 (IPv4)&lt;/strong&gt; or &lt;strong&gt;/48 (IPv6)&lt;/strong&gt; subnet already holds &lt;code&gt;MAX_PEERS_PER_SUBNET&lt;/code&gt; (default &lt;strong&gt;2&lt;/strong&gt;) peers in that bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# IP/subnet diversity limits for k-buckets (issue #1383). Within one bucket,
# at most MAX_PEERS_PER_SUBNET peers may share the same globally-routable subnet.
# /24 matches realistic attacker economics — a rented cloud block is typically
# a /24, not a /16 — and avoids bundling an ASN dataset.
&lt;/span&gt;&lt;span class="n"&gt;MAX_PEERS_PER_SUBNET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_subnet_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peer_info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PeerInfo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;addr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;peer_info&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addrs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p2p-circuit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;      &lt;span class="c1"&gt;# relayed addr = relay's IP, not the peer's
&lt;/span&gt;            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix_len&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUBNET_PREFIX_LEN_V4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUBNET_PREFIX_LEN_V6&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="bp"&gt;...&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="c1"&gt;# loopback/private/CGNAT/link-local → exempt
&lt;/span&gt;                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix_len&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The details are where the real engineering is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Only globally-routable addresses are grouped&lt;/strong&gt; (&lt;code&gt;ip.is_global&lt;/code&gt;) — loopback, RFC1918/ULA private, CGNAT (&lt;code&gt;100.64.0.0/10&lt;/code&gt;), link-local, and documentation ranges are exempt, so CI and local testnets are unaffected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS-named and relayed (&lt;code&gt;p2p-circuit&lt;/code&gt;) peers are exempt&lt;/strong&gt; — a circuit address exposes the &lt;em&gt;relay's&lt;/em&gt; IP, not the peer's, so grouping on it would wrongly bundle distinct peers behind a shared relay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-homed peers&lt;/strong&gt; are grouped by their first globally-routable address (a deliberate divergence from go-libp2p, which checks &lt;em&gt;every&lt;/em&gt; address — simpler, and it avoids false rejections of legitimately multi-homed peers).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject-only, no eviction&lt;/strong&gt; — matching go-libp2p's &lt;code&gt;TryAddPeer&lt;/code&gt;, which avoids opening a churn/DoS vector where an attacker forces evictions.&lt;/li&gt;
&lt;li&gt;Opt-out via &lt;code&gt;MAX_PEERS_PER_SUBNET &amp;lt;= 0&lt;/code&gt;, and a guard so a subnet rejection on a &lt;em&gt;non-full&lt;/em&gt; bucket can't trigger a spurious bucket split.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No protocol or wire change — just a stricter admission rule at exactly the layer the eclipse attacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The trap wasn't a bug in the code — it was a bug in the &lt;em&gt;question&lt;/em&gt;. "Records are signed, so the DHT is safe" quietly swaps the question you need answered ("is this set of peers a fair sample?") for one you've already answered ("is this record authentic?"). Authentication and membership-diversity are orthogonal: signing is a real defense against forgery and &lt;strong&gt;no defense at all&lt;/strong&gt; against being surrounded by valid liars.&lt;/p&gt;

&lt;p&gt;The broader habit I'm keeping: when someone says "that's already handled," ask &lt;em&gt;which property&lt;/em&gt; is handled. The most dangerous vulnerabilities live in the gap between two defenses that each look complete on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Fix: &lt;a href="https://github.com/libp2p/py-libp2p/pull/1399" rel="noopener noreferrer"&gt;libp2p/py-libp2p#1399 — feat(kad_dht): enforce IP subnet diversity in k-buckets&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/libp2p/py-libp2p/issues/1383" rel="noopener noreferrer"&gt;#1383&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Eclipse-attack simulation: &lt;a href="https://github.com/libp2p/py-libp2p/tree/main/tests/examples/attack_simulation/eclipse_attack/" rel="noopener noreferrer"&gt;&lt;code&gt;tests/examples/attack_simulation/eclipse_attack/&lt;/code&gt;&lt;/a&gt; in py-libp2p&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>The /sdp endpoint that trusted its callers: fixing a memory-amplification DoS in py-libp2p</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:56:05 +0000</pubDate>
      <link>https://dev.to/yashksaini/the-sdp-endpoint-that-trusted-its-callers-fixing-a-memory-amplification-dos-in-py-libp2p-3g2e</link>
      <guid>https://dev.to/yashksaini/the-sdp-endpoint-that-trusted-its-callers-fixing-a-memory-amplification-dos-in-py-libp2p-3g2e</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/libp2p/py-libp2p" rel="noopener noreferrer"&gt;py-libp2p&lt;/a&gt; is the Python implementation of libp2p — the peer-to-peer networking stack that underpins IPFS, Filecoin, and Ethereum-class nodes. I've been working on its &lt;strong&gt;WebRTC-Direct&lt;/strong&gt; transport, which lets two peers connect without a certificate authority: the peer's multiaddr carries a hash of its TLS cert, and the DTLS handshake is verified against it.&lt;/p&gt;

&lt;p&gt;Before that encrypted transport exists, the two sides have to swap SDP offer/answer blobs. Until the STUN-based listener lands (&lt;a href="https://github.com/libp2p/py-libp2p/issues/1352" rel="noopener noreferrer"&gt;#1352&lt;/a&gt;), py-libp2p ships a minimal dev harness for this: a tiny hand-rolled HTTP server (no aiohttp dependency) that accepts an SDP offer over &lt;code&gt;POST /sdp&lt;/code&gt; and hands the body to an offer handler. This post is about a memory-amplification DoS I found and fixed in that harness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;POST /sdp&lt;/code&gt; handler read the caller's &lt;code&gt;Content-Length&lt;/code&gt; and buffered exactly that many bytes — &lt;strong&gt;with no upper bound&lt;/strong&gt; — before the handshake ever happened. Here's the pre-fix read path in &lt;code&gt;_aiortc_helpers.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Read HTTP request line (consumed but not used) + headers
&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SDP_HTTP_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SDP_HTTP_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;content_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content_length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readexactly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content_length&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SDP_HTTP_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are attacker-controlled and unbounded:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The body.&lt;/strong&gt; &lt;code&gt;content_length&lt;/code&gt; comes straight from the request. &lt;code&gt;reader.readexactly(content_length)&lt;/code&gt; accumulates that many bytes into memory — and &lt;code&gt;readexactly&lt;/code&gt; bypasses the &lt;code&gt;StreamReader&lt;/code&gt;'s default 64 KiB flow-control limit, so nothing throttles it. &lt;code&gt;body.decode()&lt;/code&gt; makes a second copy, the handler string a third. Net amplification ≈ &lt;strong&gt;2× the bytes on the wire, with no ceiling.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The headers.&lt;/strong&gt; &lt;code&gt;while True&lt;/code&gt; with only a &lt;em&gt;per-line&lt;/em&gt; timeout: an attacker can keep sending header lines forever — the loop never counts them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is pre-handshake, unauthenticated input. Anyone who can reach the port can trigger it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3ktz26wodvajo5cwxov.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr3ktz26wodvajo5cwxov.png" alt="Anatomy of the /sdp memory-amplification DoS — before vs after" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where I found it:&lt;/strong&gt; a reviewer flagged the &lt;em&gt;shape&lt;/em&gt; of this inside my own WebRTC PR (&lt;a href="https://github.com/libp2p/py-libp2p/pull/1309" rel="noopener noreferrer"&gt;#1309&lt;/a&gt;) — a one-line "reads the whole body into memory with no cap, availability risk" note. Easy to read as a style nit; it wasn't. Pre-handshake input is attacker-controlled by definition, and "reads the whole body with no cap" is the entire exploit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The numbers.&lt;/strong&gt; I built a reproduction harness that imports the &lt;em&gt;real&lt;/em&gt; &lt;code&gt;run_signaling_server&lt;/code&gt; at the pre-fix parent (&lt;code&gt;9506041&lt;/code&gt;) and the merged fix (&lt;code&gt;759c75b&lt;/code&gt;), fires the malicious request, and samples RSS on a 20 ms timer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (pre-fix)&lt;/th&gt;
&lt;th&gt;After (#1396)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS, 512 MiB malicious body&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1,113 MB&lt;/strong&gt; (+1,052)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;61 MB&lt;/strong&gt; (+0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS, 1 GiB malicious body&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2,137 MB&lt;/strong&gt; (+2,076)&lt;/td&gt;
&lt;td&gt;rejected, idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS, 4 concurrent × 300 MiB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1,575 MB&lt;/strong&gt; (+1,514)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;88 MB&lt;/strong&gt; (+27)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to refuse oversized request&lt;/td&gt;
&lt;td&gt;never (buffers to OOM)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.3 ms&lt;/strong&gt; (&lt;code&gt;413&lt;/code&gt; on headers alone)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Body size accepted&lt;/td&gt;
&lt;td&gt;unbounded&lt;/td&gt;
&lt;td&gt;32 KiB (&lt;code&gt;413&lt;/code&gt; above)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo5jcjech9v0gnnouoqs6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo5jcjech9v0gnnouoqs6.png" alt="Peak RSS under a malicious POST, before vs after #1396" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Measured in a Python 3.11 / aiortc 1.15 sandbox on a single asyncio loop — re-run the harness on your own hardware for local figures; the ~2× ratio holds.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PR: &lt;a href="https://github.com/libp2p/py-libp2p/pull/1396" rel="noopener noreferrer"&gt;libp2p/py-libp2p#1396 — fix(webrtc): harden /sdp HTTP server against memory-amplification DoS&lt;/a&gt;&lt;/strong&gt; · merged &lt;strong&gt;Aug 14, 2026&lt;/strong&gt; · closes &lt;strong&gt;&lt;a href="https://github.com/libp2p/py-libp2p/issues/1354" rel="noopener noreferrer"&gt;#1354&lt;/a&gt;&lt;/strong&gt; · commit &lt;a href="https://github.com/libp2p/py-libp2p/commit/759c75b" rel="noopener noreferrer"&gt;&lt;code&gt;759c75b&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The fix adds three bounded constants and rejects &lt;em&gt;before&lt;/em&gt; touching any body buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Bounds for the HTTP /sdp dev harness — defend against memory-amplification
# DoS while the harness exists (until the STUN-based listener lands, #1352).
&lt;/span&gt;&lt;span class="n"&gt;_MAX_SDP_BODY_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;  &lt;span class="c1"&gt;# 32 KiB; SDP offers are typically 1–4 KiB
&lt;/span&gt;&lt;span class="n"&gt;_MAX_HEADER_LINES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;
&lt;span class="n"&gt;_MAX_HEADER_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;    &lt;span class="c1"&gt;# 8 KiB total across all header lines
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Headers are bounded by line count and cumulative bytes (the &lt;code&gt;for/else&lt;/code&gt; fires a &lt;code&gt;400&lt;/code&gt; if the terminator never arrives):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;header_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_MAX_HEADER_LINES&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_SDP_HTTP_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;header_bytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;header_bytes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_MAX_HEADER_BYTES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 400 Bad Request&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 0&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 400 Bad Request&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 0&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;Content-Length&lt;/code&gt; is validated before any body buffer exists — malformed/negative → &lt;code&gt;400&lt;/code&gt;, oversized → &lt;code&gt;413&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;raw_cl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;content_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_cl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 400 Bad Request&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 0&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content_length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 400 Bad Request&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 0&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content_length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_MAX_SDP_BODY_SIZE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP/1.1 413 Payload Too Large&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 0&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PR also ships a 228-line regression test (&lt;a href="https://github.com/libp2p/py-libp2p/pull/1396/files" rel="noopener noreferrer"&gt;&lt;code&gt;test_aiortc_helpers.py&lt;/code&gt;&lt;/a&gt;) whose key assertion checks that RSS &lt;em&gt;didn't grow&lt;/em&gt; after a &lt;code&gt;413&lt;/code&gt; — not just the status code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_too_large&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rss_before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getrusage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUSAGE_SELF&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ru_maxrss&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST /sdp HTTP/1.1&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s"&gt;Content-Length: 999999999&lt;/span&gt;&lt;span class="se"&gt;\r\n\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;413&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;
    &lt;span class="n"&gt;rss_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getrusage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUSAGE_SELF&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ru_maxrss&lt;/span&gt;
    &lt;span class="nf"&gt;assert &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rss_after&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;rss_before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;one_mib&lt;/span&gt;   &lt;span class="c1"&gt;# body buffer was never allocated
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;I deliberately chose a &lt;strong&gt;hard cap&lt;/strong&gt; over a streaming-with-ceiling read. SDP offers are 1–4 KiB in practice; no legitimate caller needs to POST megabytes here, so a fixed 32 KiB ceiling with early rejection is simpler &lt;em&gt;and&lt;/em&gt; safer than draining a bounded stream. Rejecting on the &lt;code&gt;Content-Length&lt;/code&gt; header alone means the oversized request never allocates a buffer at all — which is why the "after" column of the table plateaus at the idle baseline and the refusal lands in 0.3 ms.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg3fgm3ow3kt4vt2u4355.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg3fgm3ow3kt4vt2u4355.png" alt="The fix: three gates before a byte of body is buffered" width="800" height="622"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact in context:&lt;/strong&gt; py-libp2p underpins Python IPFS/Filecoin-class nodes. A signaling endpoint that OOM-kills the whole process on unauthenticated, pre-handshake input is a cheap remote crash — no credentials, no handshake, one connection. Bounding it to 32 KiB turns "attacker picks my memory ceiling" into "attacker gets a 413."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; the reviewer's one-line "availability risk" aside was a working exploit, and I almost filed it under style. Pre-handshake input is attacker-controlled by definition — "trust the &lt;code&gt;Content-Length&lt;/code&gt;" is never a safe default. And the &lt;em&gt;test&lt;/em&gt; matters as much as the fix: asserting &lt;code&gt;413&lt;/code&gt; proves the status code; asserting RSS-didn't-grow proves the property — that no buffer was allocated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reproduce it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/libp2p/py-libp2p &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;py-libp2p
pip &lt;span class="nb"&gt;install &lt;/span&gt;aiortc
git checkout 9506041   &lt;span class="c"&gt;# PRE-FIX: RSS climbs to ~1.1 GB, 200 OK&lt;/span&gt;
python3 exploit.py PREFIX body 512
git checkout 759c75b   &lt;span class="c"&gt;# POST-FIX (#1396): 413 in 0.3 ms, RSS flat&lt;/span&gt;
python3 exploit.py POSTFIX bodyreject 512
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;There's no first-class trio integration for Sentry (its Python integrations are asyncio / Django / Flask / FastAPI / AIOHTTP / etc.), so I attached manually — and it fit cleanly because the pieces genuinely apply:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;/sdp&lt;/code&gt; server is plain asyncio&lt;/strong&gt;, so wrapping the handler in &lt;code&gt;sentry_sdk.start_transaction(op="sdp.post", name="/sdp handler")&lt;/code&gt; gave me a real trace waterfall — the span sitting on &lt;code&gt;reader.readexactly()&lt;/code&gt; is visibly the culprit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AsyncioIntegration&lt;/code&gt; actually applies&lt;/strong&gt; — the WebRTC bridge from #1309 runs a real asyncio event loop in a daemon thread, so the integration isn't decorative.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SocketIntegration&lt;/code&gt;&lt;/strong&gt; adds DNS/connect spans for free, on-topic for a P2P lib.&lt;/li&gt;
&lt;li&gt;I emitted RSS as a &lt;strong&gt;custom measurement&lt;/strong&gt; so the before/after shows up inside Sentry:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk.integrations.asyncio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncioIntegration&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk.integrations.socket&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SocketIntegration&lt;/span&gt;

&lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;YOUR_DSN&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;traces_sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;profiles_sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;integrations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AsyncioIntegration&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;SocketIntegration&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rss_mb&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getrusage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUSAGE_SELF&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;ru_maxrss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;  &lt;span class="c1"&gt;# Linux: KB→MB
&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sdp.post&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/sdp handler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rss_before_mb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rss_mb&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;megabyte&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ... handle request ...
&lt;/span&gt;    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_measurement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rss_after_mb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;rss_mb&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;megabyte&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the captured trace — the &lt;code&gt;sdp.read_body&lt;/code&gt; span running &lt;code&gt;reader.readexactly(157286400)&lt;/code&gt; is the whole story, and the &lt;code&gt;rss_before_mb&lt;/code&gt; / &lt;code&gt;rss_after_mb&lt;/code&gt; measurements ride along on the transaction:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo0t4md6a081hwvzypz0j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo0t4md6a081hwvzypz0j.png" alt="Sentry trace waterfall — the readexactly span is the culprit" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seer's root-cause analysis&lt;/strong&gt; — this was the standout. I ran Seer on the captured event and, with no knowledge of the fix I'd already shipped, it pinned the exact cause and proposed essentially the same patch:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Focj1p8hwyczj4x0yrs8n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Focj1p8hwyczj4x0yrs8n.png" alt="Sentry Seer RCA — independently proposes a body cap plus header caps" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Quoting it: Seer flagged a &lt;strong&gt;"+196 MB in one request"&lt;/strong&gt; spike, found that &lt;code&gt;handle()&lt;/code&gt; "reads &lt;code&gt;Content-Length&lt;/code&gt; directly from the attacker-controlled HTTP header and passes it verbatim to &lt;code&gt;reader.readexactly(content_length)&lt;/code&gt; … with no upper-bound check," &lt;em&gt;and&lt;/em&gt; separately caught the header loop as an "unbounded while-loop." Its recommended fix — a &lt;code&gt;MAX_SDP_BODY_SIZE&lt;/code&gt; cap that returns &lt;code&gt;413 Payload Too Large&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; the read, plus per-line/total header caps — is the same shape as &lt;a href="https://github.com/libp2p/py-libp2p/pull/1396" rel="noopener noreferrer"&gt;#1396&lt;/a&gt;. The only real difference: Seer suggested a 64 KiB cap; I shipped 32 KiB. Honest verdict: &lt;strong&gt;right, and specific enough to act on&lt;/strong&gt; — the uncommon case where the AI root-cause analysis lands the actual fix rather than a vague "add validation."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Session Replay:&lt;/strong&gt; not applicable — there's no browser in this path, so I didn't fake a screenshot. The honest "N/A" is more useful to a maintainer than a contrived one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I pasted the pre-fix read path into &lt;strong&gt;Google AI Studio (Gemini)&lt;/strong&gt; and asked for three concrete hypotheses for the unbounded memory growth, ranked by likelihood, with a one-line fix for each.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qj0d94puh6g469i9nqc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6qj0d94puh6g469i9nqc.png" alt="Gemini ranks " width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Its &lt;strong&gt;#1, ranked "Most Likely," was exactly right&lt;/strong&gt;: &lt;em&gt;"the server trusts the client-provided &lt;code&gt;content-length&lt;/code&gt; header and attempts to buffer the full amount into RAM … an attacker can send a large value (e.g. &lt;code&gt;10^9&lt;/code&gt;) and slowly stream data, forcing the server to grow its internal buffer to match."&lt;/em&gt; It even independently named the ~2× amplification I'd measured — &lt;em&gt;"Python's object-copying behaviour is acting as a memory multiplier."&lt;/em&gt; The guard it proposed (&lt;code&gt;if content_length &amp;gt; 1_048_576: raise …&lt;/code&gt;) is the same shape as the fix I shipped in &lt;a href="https://github.com/libp2p/py-libp2p/pull/1396" rel="noopener noreferrer"&gt;#1396&lt;/a&gt; — the only difference is the cap size (Gemini picked 1 MiB; I used 32 KiB, since real SDP offers are 1–4 KiB). Honest read: for a tightly-scoped prompt, Gemini's top-ranked hypothesis landed the real bug on the first try.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>webrtc</category>
      <category>security</category>
    </item>
    <item>
      <title>The test was green. Every real connection would have failed.</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Thu, 20 Aug 2026 18:54:53 +0000</pubDate>
      <link>https://dev.to/yashksaini/the-test-was-green-every-real-connection-would-have-failed-3e37</link>
      <guid>https://dev.to/yashksaini/the-test-was-green-every-real-connection-would-have-failed-3e37</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setting
&lt;/h2&gt;

&lt;p&gt;WebRTC-Direct in libp2p has a neat trick for connecting without a certificate authority: the peer's multiaddr &lt;em&gt;contains&lt;/em&gt; a hash of its TLS certificate — &lt;code&gt;/webrtc-direct/certhash/&amp;lt;...&amp;gt;&lt;/code&gt;. When you dial, the DTLS handshake presents a cert, you hash it, and you check it against the &lt;code&gt;certhash&lt;/code&gt; in the address. No CA, no trust store — the address &lt;em&gt;is&lt;/em&gt; the pin.&lt;/p&gt;

&lt;p&gt;For that to work, py-libp2p has to make aiortc use &lt;em&gt;our&lt;/em&gt; libp2p-generated certificate, not the one aiortc auto-generates for itself. So the code pins it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;certificates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rtc_cert&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Obvious. And, as of aiortc ≥ 1.5, completely wrong — in two different ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first sign
&lt;/h2&gt;

&lt;p&gt;The first way was loud: aiortc ≥ 1.5 &lt;strong&gt;dropped &lt;code&gt;certificates=&lt;/code&gt; from &lt;code&gt;RTCConfiguration&lt;/code&gt;&lt;/strong&gt;. Passing it raises &lt;code&gt;TypeError&lt;/code&gt;. Easy to spot, easy to "fix" — just move the cert onto the object after construction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_certificates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rtc_cert&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# looks right. isn't.
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tests went green. The loopback echo test — open a data channel, send a payload, get it back — passed. I could have shipped it right there.&lt;/p&gt;

&lt;p&gt;The quiet way is the one that would have burned every user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The investigation
&lt;/h2&gt;

&lt;p&gt;Here's the thing that made this a &lt;em&gt;story&lt;/em&gt; and not a one-liner. &lt;code&gt;pc._certificates = [...]&lt;/code&gt; is a &lt;strong&gt;silent no-op.&lt;/strong&gt; aiortc never reads &lt;code&gt;_certificates&lt;/code&gt;. Inside the class, it stores and reads the cert as &lt;code&gt;self.__certificates&lt;/code&gt; — a &lt;em&gt;double-underscore&lt;/em&gt; attribute. And Python does something specific with double-underscore names inside a class body: it &lt;strong&gt;mangles&lt;/strong&gt; them. &lt;code&gt;self.__certificates&lt;/code&gt; inside &lt;code&gt;class RTCPeerConnection&lt;/code&gt; is compiled to &lt;code&gt;self._RTCPeerConnection__certificates&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So from the outside:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pc._certificates&lt;/code&gt; — a brand-new attribute I invented. aiortc reads it never.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pc._RTCPeerConnection__certificates&lt;/code&gt; — the &lt;em&gt;actual&lt;/em&gt; slot aiortc reads at &lt;code&gt;createOffer&lt;/code&gt;/&lt;code&gt;createAnswer&lt;/code&gt; time to write the SDP &lt;code&gt;a=fingerprint&lt;/code&gt; line (see &lt;code&gt;aiortc/rtcpeerconnection.py:295&lt;/code&gt; and &lt;code&gt;:1129&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was setting the first one. aiortc was reading the second one — still holding its own auto-generated cert. The result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;/webrtc-direct/certhash/&amp;lt;...&amp;gt;&lt;/code&gt; multiaddr advertised &lt;strong&gt;our&lt;/strong&gt; libp2p cert hash.&lt;/li&gt;
&lt;li&gt;The DTLS handshake used &lt;strong&gt;aiortc's&lt;/strong&gt; auto-generated cert.&lt;/li&gt;
&lt;li&gt;Every real dial would fail peer verification with &lt;code&gt;Remote DTLS fingerprint does not match certhash&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the loopback echo test &lt;em&gt;still passed&lt;/em&gt; — because loopback doesn't validate the DTLS fingerprint against a multiaddr. It just opens a channel and echoes bytes. The one property that was broken was the one property the happy-path test never checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9cstrtlc4fo392gw0f7p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9cstrtlc4fo392gw0f7p.png" alt="Why pc._certificates is a silent no-op — Python name mangling" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a bug that lives in the seam between two systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python's language rule:&lt;/strong&gt; name mangling rewrites &lt;code&gt;__name&lt;/code&gt; references &lt;em&gt;inside a class body&lt;/em&gt; so subclasses can't accidentally clobber a base class's "private" attributes. It's a language feature doing exactly what it's designed to do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;aiortc's library internals:&lt;/strong&gt; it stores the cert under &lt;code&gt;__certificates&lt;/code&gt;, i.e. it &lt;em&gt;relies&lt;/em&gt; on that mangling for its own encapsulation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither is wrong on its own. But when you reach into a library's private state from outside the class, the mangled name is the &lt;em&gt;only&lt;/em&gt; name that works — and the un-mangled one you'd naturally type is a no-op that fails silently. There's no &lt;code&gt;AttributeError&lt;/code&gt;, no &lt;code&gt;TypeError&lt;/code&gt;, no warning. You just create a junk attribute and move on, and everything downstream keeps working &lt;em&gt;except the security property.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Set the mangled attribute directly, after construction, before any SDP operation triggers the handshake:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iceServers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ice_servers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ice_servers&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="n"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Replace aiortc's auto-generated cert. Must use the mangled name —
# aiortc reads only `self.__certificates`, which mangles to this.
&lt;/span&gt;&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_RTCPeerConnection__certificates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rtc_cert&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore[attr-defined]
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's an ugly line, and the docstring says so out loud — reaching into &lt;code&gt;_RTCPeerConnection__certificates&lt;/code&gt; is exactly the kind of private-state coupling that breaks on a library upgrade. But it's &lt;em&gt;correct&lt;/em&gt;, and it's &lt;em&gt;annotated&lt;/em&gt; so the next person knows why it can't just be &lt;code&gt;pc._certificates&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The canary that would have caught it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8nlrat0yla811q1zayr6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8nlrat0yla811q1zayr6.png" alt="Two tests, one bug — only the invariant test sees it" width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real fix wasn't the one-liner — it was the test that makes the silent failure loud. The happy-path echo test can't see this bug, so I added one that asserts the &lt;em&gt;invariant&lt;/em&gt;: the pinned cert's fingerprint must actually appear in the SDP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.trio&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_cert_pinning_lands_in_sdp_fingerprint&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Guards against a class of cert-pin bug: if the pin is a no-op (e.g.
    written to a public attribute aiortc never reads because it actually
    stores the cert under a name-mangled private slot), the SDP a=fingerprint
    line reflects aiortc&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s auto-generated cert instead of ours.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebRTCCertificate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_aiortc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coro&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;create_peer_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_rtc_certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ice_servers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[]))&lt;/span&gt;

    &lt;span class="c1"&gt;# Need at least one data channel for the SCTP m-line, otherwise
&lt;/span&gt;    &lt;span class="c1"&gt;# createOffer omits the DTLS fingerprint entirely.
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coro&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_create_dummy_channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;sdp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coro&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_offer&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_sdp_fingerprint_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sdp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SDP fingerprint does not match pinned cert.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the small landmine inside the fix for the &lt;em&gt;test&lt;/em&gt;: &lt;code&gt;createOffer&lt;/code&gt; omits the DTLS fingerprint entirely unless there's at least one SCTP m-line, so the canary has to create a dummy data channel first or it asserts against an empty offer. The bug had layers even in the reproduction.&lt;/p&gt;

&lt;p&gt;Revert the mangled-name fix and this test goes red immediately with a fingerprint mismatch. Keep the old &lt;code&gt;pc._certificates = [...]&lt;/code&gt; no-op and it's red. That's the whole point: it fails &lt;em&gt;closed&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Two lessons, and they're the same lesson from two directions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the invariant, not the happy path.&lt;/strong&gt; The echo test proved bytes moved. It said nothing about &lt;em&gt;which certificate&lt;/em&gt; moved them — and that was the only thing that mattered for security. A green test is only as good as the property it asserts, and "it worked end to end" quietly excludes every property your end-to-end path doesn't check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A silent no-op is worse than a crash.&lt;/strong&gt; If &lt;code&gt;pc._certificates = [...]&lt;/code&gt; had raised, I'd have fixed it in thirty seconds. Because it &lt;em&gt;succeeded&lt;/em&gt; — created a real attribute, threw no error — it sailed through review and tests and would have shipped a transport where every authenticated dial fails. When you reach across an encapsulation boundary into a library's private state, assume the language is doing something clever with the name, and write the test that proves your write actually landed where the library reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Commit: &lt;a href="https://github.com/libp2p/py-libp2p/commit/ca8331ff" rel="noopener noreferrer"&gt;&lt;code&gt;ca8331ff&lt;/code&gt; — fix(webrtc): pin DTLS certificate via aiortc's mangled private slot&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Canary test: &lt;a href="https://github.com/libp2p/py-libp2p/blob/main/tests/core/transport/webrtc/test_multiplexing_loopback.py" rel="noopener noreferrer"&gt;&lt;code&gt;test_cert_pinning_lands_in_sdp_fingerprint&lt;/code&gt;&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Context: &lt;a href="https://github.com/libp2p/py-libp2p/issues/546" rel="noopener noreferrer"&gt;#546&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;aiortc internals referenced: &lt;code&gt;aiortc/rtcpeerconnection.py:295, 1129&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>Dev log #17 From 16-bit TUIs to p2p Hardening: A 227-Commit Week</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sun, 16 Aug 2026 13:14:30 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-17-from-16-bit-tuis-to-p2p-hardening-a-227-commit-week-1cbg</link>
      <guid>https://dev.to/yashksaini/dev-log-17-from-16-bit-tuis-to-p2p-hardening-a-227-commit-week-1cbg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A high-output week focused on shipping GitBanner 1.3.0, hardening WebRTC and DHT layers in py-libp2p, and archiving kad-monitor with a proper demo harness. 227 commits, 16 PRs, and a perfect 7-day streak.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This week was a massive exercise in context switching, but in the best way possible. I logged 227 commits across a dozen repos, ranging from low-level p2p networking fixes in Python to aesthetic TUI refinements in TypeScript. The highlights? Shipping a 16-bit type system for GitBanner, hardening py-libp2p against DoS attacks, and finally archiving my kad-monitor project with the dignity of a full demo harness.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  GitBanner: The Aesthetic CLI
&lt;/h3&gt;

&lt;p&gt;I spent a huge chunk of my time (95 commits!) on &lt;a href="https://github.com/yashksaini-coder/GitBanner" rel="noopener noreferrer"&gt;GitBanner&lt;/a&gt;. I’m prepping the 1.3.0 release, and I went down a rabbit hole with the TUI type system. I implemented a 16-bit CLI/TUI aesthetic—using Silkscreen for titles and VT323 for basically everything else. It gives the tool this retro-terminal vibe that I’m obsessed with. &lt;/p&gt;

&lt;p&gt;Beyond the visuals, I did a lot of "invisible" work: rebuilding &lt;code&gt;dist&lt;/code&gt;, regenerating cards, and cleaning up the caption logic so the 'total commits' count doesn't feel redundant next to the 52-week scope. It’s about that polish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Obsidian &amp;amp; Personal Knowledge
&lt;/h3&gt;

&lt;p&gt;My &lt;a href="https://github.com/yashksaini-coder/obsidian-vault" rel="noopener noreferrer"&gt;obsidian-vault&lt;/a&gt; saw 58 commits this week. I know, "commits to a vault?" but I treat my knowledge base like a codebase. I overhauled the graph palette to ensure every category has a vibrant, distinct family based on perceptual distance. I also built a &lt;code&gt;calendar-sync&lt;/code&gt; tool to repair frontmatter and backfill my August 2026 daily notes. If it isn't automated, I won't do it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kad-Monitor: The Final Chapter
&lt;/h3&gt;

&lt;p&gt;I decided to officially archive &lt;a href="https://github.com/yashksaini-coder/kad-monitor" rel="noopener noreferrer"&gt;kad-monitor&lt;/a&gt;. But I didn't want to just flip the bit and walk away. I spent 37 commits building a proper "Demo Harness." I merged &lt;a href="https://github.com/yashksaini-coder/kad-monitor/pull/1" rel="noopener noreferrer"&gt;PR #1&lt;/a&gt;, which includes a runnable simulation, a full dashboard, and A/B experiment results. I also documented its limitations honestly in the README. There's something satisfying about parking a project with a clean deployment guide and CI badges rather than letting it rot.&lt;/p&gt;

&lt;h2&gt;
  
  
  P2P &amp;amp; Backend Hardening
&lt;/h2&gt;

&lt;p&gt;Most of my "serious" engineering time went into &lt;a href="https://github.com/libp2p/py-libp2p" rel="noopener noreferrer"&gt;py-libp2p&lt;/a&gt;. P2P networking is unforgiving, and this week was all about resilience.&lt;/p&gt;

&lt;p&gt;I opened &lt;a href="https://github.com/libp2p/py-libp2p/pull/1442" rel="noopener noreferrer"&gt;PR #1442&lt;/a&gt; to enforce IP subnet diversity in Kademlia DHT k-buckets. In a decentralized network, you don't want all your peers coming from the same subnet—that's a centralization risk. I also tackled a security concern in &lt;a href="https://github.com/libp2p/py-libp2p/pull/1396" rel="noopener noreferrer"&gt;PR #1396&lt;/a&gt;, hardening the &lt;code&gt;/sdp&lt;/code&gt; HTTP server against memory-amplification DoS attacks.&lt;/p&gt;

&lt;p&gt;One of the more satisfying fixes was &lt;a href="https://github.com/libp2p/py-libp2p/pull/1441" rel="noopener noreferrer"&gt;PR #1441&lt;/a&gt;. I noticed our varint decoders were accepting truncated or over-long inputs. It’s a classic edge case that can lead to all sorts of weirdness down the line, so I tightened that up to reject malformed data immediately.&lt;/p&gt;

&lt;p&gt;I also took a swing at &lt;a href="https://github.com/hydra-db/hydradb" rel="noopener noreferrer"&gt;hydradb&lt;/a&gt;, opening &lt;a href="https://github.com/hydra-db/hydradb/pull/80" rel="noopener noreferrer"&gt;PR #80&lt;/a&gt; to optimize cursor performance. The old logic was rescanning the cursor tail on every page drain while holding a global mutex—not exactly great for concurrency. I swapped it to subtract drained bytes instead, which should feel much snappier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issues &amp;amp; Discussions
&lt;/h2&gt;

&lt;p&gt;I opened 13 issues this week, mostly as I was dogfooding my own tools or digging into upstream dependencies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;HydraDB:&lt;/strong&gt; Found a nasty bug where &lt;code&gt;merge_distributed_inner_join&lt;/code&gt; treats &lt;code&gt;null&lt;/code&gt; keys as equal (&lt;a href="https://github.com/hydra-db/hydradb/issues/96" rel="noopener noreferrer"&gt;Issue #96&lt;/a&gt;). That results in a massive cross-product of null rows that can blow up your memory.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Agent-Orchestrator:&lt;/strong&gt; Had some trouble with the Linux AppImage on Arch (&lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/issues/4006" rel="noopener noreferrer"&gt;Issue #4006&lt;/a&gt;). It turns out it was built with a legacy FUSE2 runtime. I opened &lt;a href="https://github.com/Untrivial-ai/agent-orchestrator/pull/4007" rel="noopener noreferrer"&gt;PR #4007&lt;/a&gt; to move it to a modern runtime so it actually starts on clean installs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Corsair:&lt;/strong&gt; Did a quick accessibility audit and found some missing labels in the Studio ChatPage (&lt;a href="https://github.com/corsairdev/corsair/issues/746" rel="noopener noreferrer"&gt;Issue #746&lt;/a&gt;). I fixed that and also implemented a constant-time comparison for GitLab tokens to prevent timing attacks (&lt;a href="https://github.com/corsairdev/corsair/pull/757" rel="noopener noreferrer"&gt;PR #757&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack &amp;amp; Workflow
&lt;/h2&gt;

&lt;p&gt;This was a heavy &lt;strong&gt;Python&lt;/strong&gt; and &lt;strong&gt;TypeScript&lt;/strong&gt; week, but I also touched quite a bit of &lt;strong&gt;Lua&lt;/strong&gt; while tweaking my Neovim config. &lt;/p&gt;

&lt;p&gt;I actually simplified my &lt;a href="https://github.com/yashksaini-coder/nvim" rel="noopener noreferrer"&gt;nvim&lt;/a&gt; setup this week. I removed &lt;code&gt;toggleterm&lt;/code&gt; and all its associated complexity in favor of a leaner workflow. I also restored my &lt;code&gt;&amp;lt;leader&amp;gt;gg&lt;/code&gt; binding to trigger &lt;code&gt;snacks.lazygit&lt;/code&gt;. Sometimes the best code is the code you delete—I ended the week with a net negative line count on my config, which is always the goal.&lt;/p&gt;

&lt;p&gt;The stats tell a story of massive growth: +29,890 lines and -12,478 lines. A lot of that addition is documentation and the Obsidian backfill, but the 12k deletions represent a lot of refactoring in GitBanner and py-libp2p.&lt;/p&gt;

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

&lt;p&gt;Next week is all about the GitBanner 1.3.0 launch. I want to make sure the new TUI type system is rock solid. I’m also planning to dive deeper into the HydraDB join issues I uncovered—fixing that cross-join on null keys is going to be a fun challenge in distributed logic. &lt;/p&gt;

&lt;p&gt;Seven days of commits in a row. The streak is alive, the coffee is strong, and the code is getting cleaner. See you next week!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Gravy Theory: three chickens, one base</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sat, 15 Aug 2026 09:59:55 +0000</pubDate>
      <link>https://dev.to/yashksaini/gravy-theory-three-chickens-one-base-25h8</link>
      <guid>https://dev.to/yashksaini/gravy-theory-three-chickens-one-base-25h8</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition Perfect Landing&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gravy Theory&lt;/strong&gt; is an interactive field guide to North Indian gravy, built around a single claim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Butter chicken and chicken curry are not different recipes. They are the &lt;strong&gt;same onion–tomato masala&lt;/strong&gt;, stopped at different moments and finished in different directions. Chilli chicken never touches it at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recipes give you minutes. Cooks give you colour. So the page is organised around the thing a recipe can't hand you — the ability to &lt;em&gt;look&lt;/em&gt; at a pan and know where you are.&lt;/p&gt;

&lt;p&gt;It's best understood by dragging the dial rather than reading about it: &lt;strong&gt;&lt;a href="https://yashksaini-coder.github.io/comfort-food-challenge/" rel="noopener noreferrer"&gt;open the live demo&lt;/a&gt;&lt;/strong&gt; (&lt;a href="https://github.com/yashksaini-coder/comfort-food-challenge" rel="noopener noreferrer"&gt;source&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Seven sections, each answering one question:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The base&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A dial that cooks the masala from raw to done. Six ingredients light up as you pass their stage.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The split&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Three dishes, tabbed, each showing what it adds on top of the shared base.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The fork&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One pan divides into two along drawn curves while a wok slides in from the right and &lt;em&gt;stops short&lt;/em&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The clock&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Where the time actually goes. Butter chicken is the only one needing two burners.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Green chilli&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Heat plotted against aroma on a log scale. The Indian green chilli sits where nothing else does.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The dabba&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Seven tins, seven jobs — and what breaks when each one is missing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tonight&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Three questions. It's a constraint problem, not a taste problem.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The constraint
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;One HTML file. Zero image requests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every pan, thali, katori, roti, chilli, spice tin and ingredient is drawn in CSS gradients and inline SVG. No build step, no framework, no bundler, no icon font, no CDN, no tracking. The only external request is Google Fonts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;index.html    ~147 KB    everything
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Open it in a browser. That's the whole setup.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://yashksaini-coder.github.io/comfort-food-challenge/" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Cook the base →&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Best on a real screen — the dial, the tabs and the fork scene all want a pointer or a thumb. It works from 320px up, and with JavaScript switched off.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;
        yashksaini-coder
      &lt;/a&gt; / &lt;a href="https://github.com/yashksaini-coder/comfort-food-challenge" rel="noopener noreferrer"&gt;
        comfort-food-challenge
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An interactive field guide to North Indian gravy. One HTML file, zero image requests — every pan, thali and chilli drawn in CSS and inline SVG. DEV Frontend Challenge, Comfort Food Edition.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Gravy Theory — three chickens, one base&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;An interactive field guide to North Indian gravy, arguing one thing: &lt;strong&gt;butter chicken and chicken
curry are not different recipes.&lt;/strong&gt; They are the same onion–tomato masala, stopped at different
moments and finished in different directions. Chilli chicken never touches it at all.&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/frontend-2026-07-29" rel="nofollow"&gt;DEV Frontend Challenge — Comfort Food Edition&lt;/a&gt;
(&lt;em&gt;Perfect Landing&lt;/em&gt; prompt).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://yashksaini-coder.github.io/comfort-food-challenge/" rel="nofollow noopener noreferrer"&gt;→ Live demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The constraint&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;One HTML file. &lt;strong&gt;Zero image requests.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every pan, thali, katori, roti, chilli, spice tin and ingredient on the page is drawn in CSS
gradients and inline SVG. There is no build step, no framework, no bundler, no icon font, no CDN
and no tracking. The only external request is Google Fonts.&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;index.html    ~146 KB    everything
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Open it in a browser. That's the whole setup.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What's in it&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Section&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The base&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A dial that cooks the masala from raw to&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/yashksaini-coder/comfort-food-challenge" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Plain links, in case the embeds above don't render for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo&lt;/strong&gt; — &lt;a href="https://yashksaini-coder.github.io/comfort-food-challenge/" rel="noopener noreferrer"&gt;https://yashksaini-coder.github.io/comfort-food-challenge/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt; — &lt;a href="https://github.com/yashksaini-coder/comfort-food-challenge" rel="noopener noreferrer"&gt;https://github.com/yashksaini-coder/comfort-food-challenge&lt;/a&gt; (MIT)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The thing that took three attempts
&lt;/h3&gt;

&lt;p&gt;My first pass at the butter chicken looked like a &lt;strong&gt;donut chart&lt;/strong&gt;. Orange disc, gradient, a white wedge for the cream. Technically food-coloured; emotionally a pie chart.&lt;/p&gt;

&lt;p&gt;What fixed it wasn't more detail. It was realising that real food has &lt;strong&gt;three separable optical layers&lt;/strong&gt;, and I had collapsed two of them into one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;an opaque &lt;strong&gt;pigmented base&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;subsurface glow&lt;/strong&gt; — light entering the material, scattering, coming back warmer&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;specular glint&lt;/strong&gt; — a hard reflection off the fat on top&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The glow is &lt;em&gt;big, blurry and coloured.&lt;/em&gt; The glint is &lt;em&gt;small, sharp and near-white.&lt;/em&gt; Merge them into one soft highlight and you get plastic every single time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* subsurface — wide, offset toward the light, blurred, screened */&lt;/span&gt;
&lt;span class="nc"&gt;.kadai__pool&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="m"&gt;58%&lt;/span&gt; &lt;span class="m"&gt;44%&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;66%&lt;/span&gt; &lt;span class="m"&gt;30%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="m"&gt;#f0a03c7&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="m"&gt;#d6431f2&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="m"&gt;54%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;74%&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;34%&lt;/span&gt; &lt;span class="m"&gt;26%&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;33%&lt;/span&gt; &lt;span class="m"&gt;68%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#f0a03c54&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;transparent&lt;/span&gt; &lt;span class="m"&gt;72%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;9px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;mix-blend-mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;/* specular — tiny, hard, warm-white. This one line is the difference. */&lt;/span&gt;
&lt;span class="nc"&gt;.kadai__lit&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;aspect-ratio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;linear-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#fffdf4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#ffe9be&lt;/span&gt; &lt;span class="m"&gt;62%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#ffd79200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;rotate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-22deg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blur&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1.1px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;mix-blend-mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other half of it: &lt;strong&gt;commit to one light source and never break it.&lt;/strong&gt; Everything on the page is lit from the upper right (&lt;code&gt;--lx: 78%; --ly: 18%&lt;/code&gt;). Mismatched light direction is the fastest way to make something read as drawn rather than lit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four filters, all of them static
&lt;/h3&gt;

&lt;p&gt;Inline SVG filters do the work CSS can't: a &lt;code&gt;feTurbulence&lt;/code&gt; + &lt;code&gt;feDisplacementMap&lt;/code&gt; pair gives gravy a ragged edge instead of a perfect circle, a gooey &lt;code&gt;feColorMatrix&lt;/code&gt; makes cream dollops &lt;em&gt;fuse&lt;/em&gt; with surface tension rather than stack like stickers, and thresholded noise paints tandoor char onto the chicken.&lt;/p&gt;

&lt;p&gt;The rule I set myself: &lt;strong&gt;no filter animates its own parameters.&lt;/strong&gt; Animating &lt;code&gt;baseFrequency&lt;/code&gt; or &lt;code&gt;seed&lt;/code&gt; regenerates Perlin noise on the CPU every frame and drops a phone to single-digit FPS. The steam filter is completely static — only the source element translates.&lt;/p&gt;

&lt;p&gt;That char filter also taught me something I didn't know. My first version rendered &lt;em&gt;nothing&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- broken: thresholds at 6/7 ≈ 0.857 --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;feFuncA&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"discrete"&lt;/span&gt; &lt;span class="na"&gt;tableValues=&lt;/span&gt;&lt;span class="s"&gt;"0 0 0 0 0 0 1"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fractalNoise&lt;/code&gt; alpha clusters tightly around 0.5 and essentially never reaches 0.857, so the soot layer was empty. A linear stretch puts the threshold where the data actually lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;feFuncA&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"linear"&lt;/span&gt; &lt;span class="na"&gt;slope=&lt;/span&gt;&lt;span class="s"&gt;"11"&lt;/span&gt; &lt;span class="na"&gt;intercept=&lt;/span&gt;&lt;span class="s"&gt;"-6.6"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Two CSS traps I fell into twice each
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Percentage padding on an absolutely positioned element resolves against the positioned ancestor, not the element.&lt;/strong&gt; &lt;code&gt;padding: 7.5%&lt;/code&gt; on a pan inside a 1000px stage became 75px per side on a 190px pan — the gravy collapsed to a dot. Same bug later stretched my spice tins into ovals. The fix both times is &lt;code&gt;inset: 7.5%&lt;/code&gt; on an absolutely positioned child, which &lt;em&gt;does&lt;/em&gt; resolve against the parent's own box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inline &lt;code&gt;style&lt;/code&gt; beats every media query.&lt;/strong&gt; My entire mobile layout for the fork scene was silently dead because the positions lived in &lt;code&gt;style="--x:27%"&lt;/code&gt;. Custom properties don't escape specificity — anything responsive has to live in a class.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the colour actually interpolate
&lt;/h3&gt;

&lt;p&gt;The dial interpolates the gravy in OKLCH so the ramp doesn't go grey through the middle of a warm palette. But as a plain custom property, &lt;code&gt;--f&lt;/code&gt; is a &lt;em&gt;string&lt;/em&gt; — so the colour stepped instead of transitioning. Registering it fixes that in three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;--f&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;syntax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"&amp;lt;percentage&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;initial-value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;--gravy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;syntax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"&amp;lt;color&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;initial-value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#b94a17&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The part I'm most proud of
&lt;/h3&gt;

&lt;p&gt;Not the food — the &lt;strong&gt;accessibility&lt;/strong&gt;, because it forced better decisions everywhere.&lt;/p&gt;

&lt;p&gt;The chilli scatter and the masala dabba were originally sets of &lt;code&gt;aria-pressed&lt;/code&gt; toggle buttons. But &lt;code&gt;aria-pressed&lt;/code&gt; models &lt;em&gt;N independent toggles&lt;/em&gt;, and a set where exactly one member is active is the APG &lt;strong&gt;radio group&lt;/strong&gt; pattern. Converting them collapsed 13 tab stops into 2, added arrow-key navigation, and made screen readers announce "3 of 6" instead of "pressed."&lt;/p&gt;

&lt;p&gt;A few more that I'd have shipped broken without checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A live region inside a &lt;code&gt;display: none&lt;/code&gt; subtree announces nothing.&lt;/strong&gt; My quiz verdict wrote all its text while hidden, then revealed it — silent both ways. The region now lives outside the collapsible body.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hiding a button's label with &lt;code&gt;display: none&lt;/code&gt; destroys its accessible name.&lt;/strong&gt; Below 30rem my motion toggle fell back to a stale &lt;code&gt;title&lt;/code&gt; and announced the &lt;em&gt;opposite&lt;/em&gt; of what it would do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;role="img"&lt;/code&gt; makes every descendant presentational.&lt;/strong&gt; My fork scene had four visible captions that screen readers never saw.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;prefers-reduced-motion&lt;/code&gt; blocks usually forget delays.&lt;/strong&gt; Zeroing &lt;code&gt;animation-duration&lt;/code&gt; but not &lt;code&gt;animation-delay&lt;/code&gt; leaves the hero at &lt;code&gt;opacity: 0&lt;/code&gt; for half a second.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a &lt;strong&gt;Pause motion&lt;/strong&gt; control in the header that stops every looping animation (WCAG 2.2.2 — a &lt;code&gt;:hover&lt;/code&gt; pause is reachable by neither keyboard nor touch), contrast is verified AA across every text/background pair in &lt;em&gt;both&lt;/em&gt; palettes, and the whole page is readable with &lt;strong&gt;JavaScript switched off&lt;/strong&gt; — tabs become stacked panels, charts fall back to static lists.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bug that embarrassed me most
&lt;/h3&gt;

&lt;p&gt;Late on, a review pass caught that two of my three timing bars &lt;strong&gt;ran past their own labels&lt;/strong&gt;. Chicken curry's bar reached 45 minutes against a "42 min" caption; butter chicken hit 26.1 against "24". On a page whose entire middle section is a timing comparison, I had drawn the timings wrong. There's now a script that asserts every bar ends where its label says it does.&lt;/p&gt;

&lt;p&gt;Lesson filed: the thing you're &lt;em&gt;arguing&lt;/em&gt; is the thing to write a check for.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's next
&lt;/h3&gt;

&lt;p&gt;A cook-along mode. The rotating stamp advertises eighteen minutes and then does nothing with it — start/pause/reset driving the dial in real time against the stage timings would make the page usable at the stove, and the pause control it needs is one I've already built.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://yashksaini-coder.github.io/comfort-food-challenge/" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Watch the oil separate →&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built with vanilla HTML, CSS and JavaScript. No frameworks were harmed. It's one file and it's MIT — &lt;a href="https://github.com/yashksaini-coder/comfort-food-challenge" rel="noopener noreferrer"&gt;take it apart&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Dev log #16 Typographic Hierarchy and the Great Obsidian Purge</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Tue, 11 Aug 2026 06:18:06 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-16-typographic-hierarchy-and-the-great-obsidian-purge-3h5e</link>
      <guid>https://dev.to/yashksaini/dev-log-16-typographic-hierarchy-and-the-great-obsidian-purge-3h5e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Spent the week redesigning my portfolio’s blog layout and nuking thousands of stale notes in my Obsidian vault. Between the UI polish and some deep dives into libp2p DHT de-flaking, I pushed 36 commits and managed to delete almost 16,000 lines of clutter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I’ve always believed that your digital space needs a good pruning every now and then to stay healthy. This week was the embodiment of that philosophy. I pushed 36 commits across four primary projects, resulting in over 23,000 additions and nearly 16,000 deletions. Most of that churn came from a massive redesign of my portfolio's blog and a long-overdue "fresh start" for my Obsidian vault. On the open-source side, I spent some quality time in the weeds of &lt;code&gt;py-libp2p&lt;/code&gt;, chasing down flaky DHT tests and proposing better subnet diversity limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Portfolio Redesign: The Typography Pivot
&lt;/h3&gt;

&lt;p&gt;My main focus this week was my portfolio. I’ve been feeling like the blog layout was getting a bit cluttered, so I opened and merged PR #15, which was all about "typographic hierarchy instead of decoration." I’m moving away from unnecessary borders and boxes and letting the type do the heavy lifting.&lt;/p&gt;

&lt;p&gt;I spent a lot of time in &lt;code&gt;components/blog&lt;/code&gt; and &lt;code&gt;app/blog&lt;/code&gt; refining the layout. I implemented borderless filter pills and full-width rows to give the content more room to breathe. One of the bigger technical shifts was moving the blog list to be fully server-rendered. It feels snappier, and it allowed me to implement more "honest" dates and better hover states on the rows. I also added a real focus ring for accessibility (because we’ve all been frustrated by keyboard navigation that feels like a guessing game). By the time I was done, I’d touched over 200 files in that repo alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Obsidian Purge
&lt;/h3&gt;

&lt;p&gt;I also took a metaphorical chainsaw to my &lt;code&gt;obsidian-vault&lt;/code&gt;. I nuked nearly 10,000 lines of stale content. I removed entire directories for "Projects," "Rust," and "Backend" notes that were just gathering digital dust. &lt;/p&gt;

&lt;p&gt;It’s easy to let a vault become a graveyard of half-finished thoughts. I renamed my local audit to "Persona-Audit-August-2026" and cleared out a bunch of orphaned assets and stale index notes. I even repointed some old PDF links, like the Rust book, and dropped "Practical C" from my active reading list. It feels good to have a clean slate for the second half of the year.&lt;/p&gt;

&lt;h3&gt;
  
  
  Neovim Tinkering (As Always)
&lt;/h3&gt;

&lt;p&gt;My &lt;code&gt;nvim&lt;/code&gt; config saw some action too. I finally made the jump from &lt;code&gt;bufferline&lt;/code&gt; to &lt;code&gt;barbar&lt;/code&gt; for my tabline. I also swapped out &lt;code&gt;aerial&lt;/code&gt; for &lt;code&gt;outline.nvim&lt;/code&gt; to handle my symbols and code structure. &lt;/p&gt;

&lt;p&gt;Beyond the editor itself, I did some work on my config’s documentation site. I redesigned the card layout to use a masonry/bento style CSS-column grid. I reordered the cards "tallest-first" so the columns balance out better visually. I also widened the max width to &lt;code&gt;5xl&lt;/code&gt; and added some hairline section dividers. It’s a much tighter rhythm now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests
&lt;/h2&gt;

&lt;p&gt;I had two main PRs on the go this week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio PR #15 (Merged):&lt;/strong&gt; As I mentioned, this was the big typography overhaul. It was a massive diff (+10,034 / -1,811) because I was essentially re-skinning the entire blog experience. It feels much more professional now—less like a template and more like a custom-built home for my thoughts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;py-libp2p PR #1424 (Open):&lt;/strong&gt; This one was a bit more technical. I’ve been hitting some flakiness in the &lt;code&gt;kad_dht&lt;/code&gt; tests, specifically where the &lt;code&gt;dht_pair&lt;/code&gt; fixture races on routing-table population. Sometimes Node A just doesn't know about Node B in time for the test to run. I introduced a bounded poll to give the nodes time to find each other without making the tests feel sluggish.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Issues &amp;amp; Discussions
&lt;/h2&gt;

&lt;p&gt;Most of my community-facing work happened in the &lt;code&gt;py-libp2p&lt;/code&gt; repo. I opened three issues that I’m pretty passionate about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Issue #1423:&lt;/strong&gt; This was the bug report for the flaky &lt;code&gt;dht_pair&lt;/code&gt; fixture I mentioned above. It’s one of those "it works on my machine but fails in CI" problems that makes everyone's life harder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue #1422 &amp;amp; #1421:&lt;/strong&gt; These are about hardening the Kademlia DHT. I want to make subnet-diversity limits runtime-configurable and add a table-wide IP-group cap (&lt;code&gt;maxForTable&lt;/code&gt;). When you're dealing with p2p networking, you really don't want your routing table to be dominated by a single ISP or subnet—it’s a massive reliability and security risk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also did a tiny bit of housekeeping on &lt;code&gt;Pair-Pal&lt;/code&gt; by creating a &lt;code&gt;FUNDING.yml&lt;/code&gt; file. It’s a small step, but it’s part of taking my open-source projects more seriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;This was a heavily polyglot week, though the numbers are skewed by the sheer amount of content in the portfolio and vault:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript:&lt;/strong&gt; The backbone of the portfolio redesign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lua:&lt;/strong&gt; Keeping the Neovim config evolving.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python:&lt;/strong&gt; The language of choice for my libp2p contributions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown/MDX:&lt;/strong&gt; This was the "cleanup" language this week.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The add/delete ratio was interesting. While I added over 23k lines, a huge chunk of that was the new portfolio structure. Deleting 15k lines—mostly from the Obsidian vault and the old portfolio layout—felt just as productive. Refactoring isn't always about the new stuff; it's about making sure the old stuff isn't in the way.&lt;/p&gt;

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

&lt;p&gt;Next week, I’m hoping to get that libp2p PR merged once the maintainers have a look at my polling logic. I also want to dive deeper into those subnet diversity configurations I proposed. On the personal side, now that the portfolio layout is stable, I need to actually sit down and write some of the long-form content I’ve been planning. &lt;/p&gt;

&lt;p&gt;The editor is clean, the vault is purged, and the site is fast. No more excuses—time to build.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>design</category>
      <category>productivity</category>
      <category>softwaredevelopment</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Dev log #15 Refining the Craft: A week of Neovim UI hacking and portfolio architecture</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Thu, 06 Aug 2026 04:18:06 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-15-refining-the-craft-a-week-of-neovim-ui-hacking-and-portfolio-architecture-45a2</link>
      <guid>https://dev.to/yashksaini/dev-log-15-refining-the-craft-a-week-of-neovim-ui-hacking-and-portfolio-architecture-45a2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Balanced the scales this week with a near-perfect 1:1 ratio of additions to deletions. Focused on stripping back my Neovim UI and restructuring how I showcase my career history—33 commits and a 5-day streak of pure flow.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This week was all about the "one for me, one for the world" balance. I spent a significant chunk of time deep in my Neovim configuration, ripping out UI components that didn't spark joy, while simultaneously overhauling my portfolio's experience section. With 3,162 additions and 3,141 deletions, the codebase effectively stayed the same size but feels twice as fast. It’s that rare kind of week where the net line count is almost zero, but the functional improvement is massive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;h3&gt;
  
  
  nvim (The Editor Overhaul)
&lt;/h3&gt;

&lt;p&gt;I’m a firm believer that your editor is a living organism. If you aren't tweaking it, you aren't learning. This week, I went on a bit of a minimalist crusade in my &lt;code&gt;nvim&lt;/code&gt; config. I made 23 commits across 60 files, mostly centered around &lt;code&gt;lua/plugins&lt;/code&gt; and &lt;code&gt;lua/config&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The biggest move was swapping out &lt;code&gt;mini.statusline&lt;/code&gt; for &lt;code&gt;lualine&lt;/code&gt;. Don't get me wrong, the &lt;code&gt;mini&lt;/code&gt; ecosystem is incredible, but I found myself wanting the specific modularity that &lt;code&gt;lualine&lt;/code&gt; offers for my current workflow. Along with that, I decided to drop &lt;code&gt;barbecue&lt;/code&gt; and implement a custom &lt;code&gt;winbar&lt;/code&gt;. It’s cleaner, less distracting, and gives me exactly the context I need without the visual noise.&lt;/p&gt;

&lt;p&gt;I also tackled a bug that has been driving me absolutely wild: my &lt;code&gt;&amp;lt;leader&amp;gt;bd&lt;/code&gt; (buffer delete) mapping. For a while there, it had a nasty habit of taking the whole editor down instead of just closing the current buffer. There’s nothing quite like being in the middle of a deep logic flow, trying to clean up your tabs, and accidentally nuking your entire session. I finally sat down and fixed the logic so it correctly handles the last buffer—now, it re-opens the dashboard instead of exiting. Speaking of the dashboard, I added a "Recent Files" row using &lt;code&gt;snacks&lt;/code&gt;, which has already saved me a dozen &lt;code&gt;telescope&lt;/code&gt; lookups today.&lt;/p&gt;

&lt;p&gt;On the maintenance side, I tweaked my CI. I used to have a GitHub Action running daily to update all my plugins, but it was getting noisy. I shifted that to a weekly Sunday 00:00 IST schedule. It keeps things fresh without cluttering my notification feed every single morning.&lt;/p&gt;

&lt;h3&gt;
  
  
  portfolio (Restructuring the Narrative)
&lt;/h3&gt;

&lt;p&gt;While the Neovim work was about personal productivity, the work on my &lt;code&gt;portfolio&lt;/code&gt; repo was about how I present my work to the world. I pushed 10 commits here, but they were "heavy" commits—over 2,500 lines added and 1,200 removed.&lt;/p&gt;

&lt;p&gt;The core of this work was a total refactor of the experience section. Previously, my roles were just a flat list, which doesn't really capture the story of growing within a company. I opened and merged a PR to group roles under single company cards. It sounds like a small UI change, but the data restructuring in &lt;code&gt;lib&lt;/code&gt; and &lt;code&gt;data&lt;/code&gt; to make this happen cleanly was a fun puzzle. I also made sure to render the "company narrative" on the home section to give visitors more context about the impact I had at each stop.&lt;/p&gt;

&lt;p&gt;I also spent some time in &lt;code&gt;components/common&lt;/code&gt; and &lt;code&gt;components/sections&lt;/code&gt; just obsessing over the layout. I refactored the layout logic to align every section rule to the content box. There’s a specific kind of satisfaction that comes from seeing your CSS grid and TypeScript props align perfectly across different screen sizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pull Requests
&lt;/h2&gt;

&lt;p&gt;I managed to get one significant PR through the pipeline this week:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/yashksaini-coder/portfolio/pull/14" rel="noopener noreferrer"&gt;feat(experience): group roles under one company card&lt;/a&gt;&lt;/strong&gt;: This was the heart of my portfolio work. It involved 685 additions and 378 deletions. Beyond just the UI, I had to update the TypeScript interfaces to support nested role structures and ensure the rendering logic didn't break for older, single-role entries. It’s merged and live now, and the "Experience" tab looks significantly more professional.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack &amp;amp; Stats
&lt;/h2&gt;

&lt;p&gt;It was a classic polyglot week, though the commit activity was concentrated in &lt;strong&gt;Lua&lt;/strong&gt; (for the editor) and &lt;strong&gt;TypeScript&lt;/strong&gt; (for the portfolio). &lt;/p&gt;

&lt;p&gt;Looking at the broader stats, I'm currently sitting on a massive pile of code across different languages—everything from a heavy &lt;strong&gt;Python&lt;/strong&gt; and &lt;strong&gt;Rust&lt;/strong&gt; backbone to &lt;strong&gt;Go&lt;/strong&gt;, &lt;strong&gt;C#&lt;/strong&gt;, and even some &lt;strong&gt;Solidity&lt;/strong&gt; and &lt;strong&gt;Nix&lt;/strong&gt; tucked away in various repos. It’s a reminder that as a freelancer, you never really stop adding tools to the belt.&lt;/p&gt;

&lt;p&gt;The most telling stat this week is the &lt;strong&gt;Add/Delete ratio&lt;/strong&gt;. 3,162 up, 3,141 down. That is the definition of a refactoring week. I wasn't just piling new features on top of a shaky foundation; I was digging out the old stuff and replacing it with something better. &lt;/p&gt;

&lt;p&gt;I also hit a &lt;strong&gt;5-day commit streak&lt;/strong&gt;. I don't usually game the green squares, but when you're in the middle of a refactor, it’s hard to stay away for more than 24 hours. The momentum just carries you.&lt;/p&gt;

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

&lt;p&gt;With the Neovim UI finally feeling "quiet" and the portfolio restructuring out of the way, I’m looking to dive back into some deeper backend work next week. I’ve got some ideas for CLI tools that have been rattling around my head while I was tweaking my &lt;code&gt;lua&lt;/code&gt; configs. &lt;/p&gt;

&lt;p&gt;The goal for next week? Keep the deletion energy high. If I can ship features while keeping the total line count stable, I’m winning. See you in the next update!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>weeklyui</category>
      <category>ui</category>
    </item>
    <item>
      <title>Dev log #14 Hardening the DHT against Eclipse attacks and the endless battle with flaky p2p tests</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Sat, 01 Aug 2026 04:20:17 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-14-hardening-the-dht-against-eclipse-attacks-and-the-endless-battle-with-flaky-p2p-tests-593j</link>
      <guid>https://dev.to/yashksaini/dev-log-14-hardening-the-dht-against-eclipse-attacks-and-the-endless-battle-with-flaky-p2p-tests-593j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Dived deep into libp2p security this week with a new IP subnet diversity feature for the Kad-DHT, while fighting a few stubborn race conditions in the test suite. 3 PRs open and some automated maintenance on my Neovim setup kept the gears turning.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Security hardening took center stage this week as I focused on making our Kademlia DHT implementation more resilient against Eclipse attacks. Between drafting logic for IP subnet diversity and chasing down race conditions in the &lt;code&gt;py-libp2p&lt;/code&gt; test suite, it was a week of high-leverage architectural thinking paired with the granular frustration of de-flaking async code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fight for a More Secure DHT
&lt;/h2&gt;

&lt;p&gt;Most of my brainpower this week went into &lt;code&gt;py-libp2p&lt;/code&gt;, specifically around how we handle peer selection in the Kademlia DHT. I opened a significant PR, &lt;a href="https://github.com/libp2p/py-libp2p/pull/1399" rel="noopener noreferrer"&gt;feat(kad_dht): enforce IP subnet diversity in k-buckets (#1383)&lt;/a&gt;, which adds 226 lines of new logic to ensure our routing table doesn't get saturated by peers from the same network neighborhood.&lt;/p&gt;

&lt;p&gt;If you're not deep in the p2p weeds, here's the deal: an Eclipse attack happens when an attacker manages to surround a target node with their own malicious nodes. If all your "neighbors" in the network are controlled by one person, they can effectively cut you off from the real network or feed you fake data. By enforcing IP subnet diversity, we make it much harder (and more expensive) for an attacker to pull this off because they can't just spin up a thousand nodes on a single cloud provider's subnet and expect to fill up our k-buckets. &lt;/p&gt;

&lt;p&gt;It’s one of those features that feels invisible until it’s not there, but it’s critical for the long-term health of the library.&lt;/p&gt;

&lt;p&gt;I also kicked off a deep-dive discussion titled &lt;a href="https://github.com/libp2p/py-libp2p/discussions/1400" rel="noopener noreferrer"&gt;Eclipse-attack hardening for kad-dht: our approach, and why we dropped randomized lookup selection (#1383 / #1384)&lt;/a&gt;. We had originally looked at randomized lookup selection as a defense mechanism, but after some back-and-forth, we decided to drop it in favor of more robust bucket management. It’s always better to realize an approach is suboptimal &lt;em&gt;before&lt;/em&gt; you merge the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tax of Asynchronous Testing
&lt;/h2&gt;

&lt;p&gt;When you're working on a library that relies heavily on asynchronous networking, you eventually have to pay the "flaky test tax." This week, I spent a good chunk of time hunting down two particularly annoying race conditions that have been haunting our CI.&lt;/p&gt;

&lt;p&gt;The first was &lt;a href="https://github.com/libp2p/py-libp2p/pull/1409" rel="noopener noreferrer"&gt;test(timed_cache): de-flake test_expiry_removal (#1408)&lt;/a&gt;. The issue was a classic: the test was racing against a background sweeper thread. We were using &lt;code&gt;trio.sleep&lt;/code&gt;, which is usually fine, but in a CI environment where CPU cycles can be unpredictable, the sweeper wasn't always finishing its cleanup before the test checked the results. I also opened an issue to track this specifically: &lt;a href="https://github.com/libp2p/py-libp2p/issues/1408" rel="noopener noreferrer"&gt;flaky: test_expiry_removal races the background sweeper thread&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The second one was &lt;a href="https://github.com/libp2p/py-libp2p/pull/1402" rel="noopener noreferrer"&gt;test(pubsub): de-flake test_gossip_gate_filters_peers (#1401)&lt;/a&gt;. Similar story here—subscription propagation in Gossipsub is inherently timing-dependent. If the test moves faster than the network (even a mocked one), everything breaks. Fixing these isn't the most glamorous work, but there is a specific kind of satisfaction in seeing a red CI pipeline finally turn green after you've tightened up the timing logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Maintenance and Tooling
&lt;/h2&gt;

&lt;p&gt;On the side, I kept my environment healthy. My &lt;a href="https://github.com/yashksaini-coder/nvim" rel="noopener noreferrer"&gt;nvim&lt;/a&gt; config saw 6 commits this week, though they were mostly automated chores. I have a CI setup that keeps all my plugins updated to the latest versions. It’s a "set it and forget it" kind of thing, but it ensures that when I sit down to code, my editor is actually ready for me. &lt;/p&gt;

&lt;p&gt;With 7 additions and 7 deletions across those 6 commits, it was a perfectly balanced week of maintenance. No new bloat, just keeping the tools sharp.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack &amp;amp; Vibe
&lt;/h2&gt;

&lt;p&gt;This week was heavily skewed toward Python, given the work on &lt;code&gt;py-libp2p&lt;/code&gt;. With over 96MB of Python code in the mix (thanks to the sheer scale of the libp2p ecosystem), it’s definitely my primary focus right now. But looking at my language stats, the polyglot life is still very much real—TypeScript, Rust, and Lua are all sitting there in the background, ready for when I pivot back to frontend or systems work.&lt;/p&gt;

&lt;p&gt;The add/delete ratio was interestingly tight this week (7 up, 7 down in the repo I committed to directly). It reflects a week of refinement rather than raw feature expansion. Even the libp2p PRs, while adding new logic, were very focused on fixing specific behaviors rather than just piling on more code.&lt;/p&gt;

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

&lt;p&gt;I have three PRs currently sitting in "OPEN" status on &lt;code&gt;py-libp2p&lt;/code&gt;. My priority for next week is getting those merged. The IP subnet diversity work is the big one—I want to make sure the implementation is bulletproof before it hits main. Once those are in, I’m hoping to get back to some net-new feature work, but as anyone in OSS knows, the next flaky test is probably just around the corner.&lt;/p&gt;

&lt;p&gt;Catch you in the next update. Keep shipping.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>networking</category>
      <category>python</category>
    </item>
    <item>
      <title>Dev log #13 Hardening Kademlia: A week of p2p security architecture and Neovim maintenance</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Thu, 23 Jul 2026 04:06:15 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-1-hardening-kademlia-a-week-of-p2p-security-architecture-and-neovim-maintenance-12jo</link>
      <guid>https://dev.to/yashksaini/dev-log-1-hardening-kademlia-a-week-of-p2p-security-architecture-and-neovim-maintenance-12jo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Spent the week laying the groundwork for major p2p security upgrades while keeping my dev environment sharp. 9 commits and a 7-day streak, but the real work was in the three security-focused issues and a deep-dive discussion on DHT hardening.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Sometimes the most productive weeks are the ones where you don't actually merge a single PR. This week was all about the long game—specifically, hardening the Kademlia DHT in &lt;code&gt;py-libp2p&lt;/code&gt; against eclipse attacks. I pushed 9 commits across two repos, maintained a perfect 7-day streak, and opened three critical security issues that I’m itching to start coding.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT I BUILT
&lt;/h2&gt;

&lt;p&gt;While I didn't close out any massive features this week, I spent a lot of time in the guts of &lt;code&gt;py-libp2p&lt;/code&gt;. If you've ever worked on p2p networking, you know that the "boring" maintenance work is actually what keeps the whole house from falling down.&lt;/p&gt;

&lt;h3&gt;
  
  
  py-libp2p
&lt;/h3&gt;

&lt;p&gt;I spent most of my active coding time in &lt;code&gt;tests/core&lt;/code&gt; and the &lt;code&gt;newsfragments&lt;/code&gt; directory. I managed to push two significant commits here that were more about stability and refactoring than net-new features. &lt;/p&gt;

&lt;p&gt;First, I tackled a nagging issue with the TCP transport. I re-enabled a stale IPv6 listen/dial integration test that had been sitting there (closing issue #1189). There’s something deeply satisfying about turning a skipped test back to green, especially when it involves the finicky nature of IPv6 networking. &lt;/p&gt;

&lt;p&gt;I also did some refactoring on the &lt;code&gt;pubsub-test&lt;/code&gt; suite. I implemented predicate-based readiness in the &lt;code&gt;subscribed_mesh&lt;/code&gt; fixture (PR #1315). Testing asynchronous p2p systems is a nightmare if your timing isn't perfect, and moving to a predicate-based readiness check makes the whole test suite much less flaky. I added 137 lines and only pulled out 9, mostly because robust testing logic usually requires a bit more verbosity than the "hope and pray" method of &lt;code&gt;time.sleep()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  nvim
&lt;/h3&gt;

&lt;p&gt;The rest of my commits (7 of them, to be exact) were in my &lt;code&gt;nvim&lt;/code&gt; config. This is the background hum of my development life. Most of these were automated via CI—keeping plugins updated to their latest versions. You’ll see a bunch of &lt;code&gt;ci: update all plugins to latest [skip ci]&lt;/code&gt; messages in my history. It’s not "feature work" in the traditional sense, but keeping my editor sharp is a non-negotiable part of the craft. Even with these small updates, I touched 7 different files. It’s a net-zero change (+12/-12), which is exactly what you want for maintenance: keep it moving, don't break the flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  ISSUES &amp;amp; DISCUSSIONS
&lt;/h2&gt;

&lt;p&gt;This is where the real "brain work" happened this week. I've been thinking a lot about the security of the Kademlia Distributed Hash Table (DHT), specifically how to make it more resilient against eclipse attacks. I opened three issues and started a major discussion in the &lt;code&gt;py-libp2p&lt;/code&gt; repo to map out the path forward.&lt;/p&gt;

&lt;p&gt;I opened Issue #1383 to address IP/subnet diversity enforcement in &lt;code&gt;KBucket&lt;/code&gt;. If a single attacker can fill up a bucket with nodes from the same subnet, they can effectively "eclipse" a node from the rest of the network. Enforcing diversity is a standard but critical defense.&lt;/p&gt;

&lt;p&gt;Following that, I opened Issue #1384 for adding disjoint lookup paths to &lt;code&gt;find_closest_peers_network&lt;/code&gt;. Parallel lookups are great for speed, but if they aren't disjoint, a single malicious node can still influence the outcome of the entire search. &lt;/p&gt;

&lt;p&gt;Finally, I opened Issue #1385 regarding monotonic sequence numbers for silent-withholding detection in &lt;code&gt;GET_VALUE&lt;/code&gt; requests. This one is a bit more nuanced—it's about detecting when a node is intentionally not giving you the data it should have.&lt;/p&gt;

&lt;p&gt;I tied all of these together in a discussion titled "Hardening the Kademlia DHT against eclipse attacks — three issues I want to work on" (#1386). I put this in the "Ideas" category because I want to make sure the maintainers are aligned with the architectural changes before I start dropping massive PRs. It’s about building consensus before building the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  TECH STACK
&lt;/h2&gt;

&lt;p&gt;This week was a deep dive into Python and Lua. &lt;/p&gt;

&lt;p&gt;Python remains my heavy lifter for backend and p2p work. With over 95MB of Python code in my current environment, it’s where I do my most complex logic. The +149/-21 line ratio this week shows I was mostly in "build and expand" mode rather than "refactor and destroy" mode.&lt;/p&gt;

&lt;p&gt;Lua is my "glue" language, primarily for Neovim. It’s lightweight and does exactly what I need it to do for tooling.&lt;/p&gt;

&lt;p&gt;I also kept my 7-day streak alive. I’m not a fan of the "grind" for the sake of a green square, but when you're deep into a security architecture problem, it's hard &lt;em&gt;not&lt;/em&gt; to check in every day. The flow state is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT'S NEXT
&lt;/h2&gt;

&lt;p&gt;Next week is going to be about turning those issues into code. Now that I’ve laid out the plan for Kademlia hardening and got the discussion started, I’ll be diving into the &lt;code&gt;KBucket&lt;/code&gt; implementation to start enforcing that subnet diversity. I’m also keeping an eye on the &lt;code&gt;py-libp2p&lt;/code&gt; discussion board to see if the maintainers have any feedback on the disjoint lookup path proposal. &lt;/p&gt;

&lt;p&gt;It’s one thing to talk about p2p security; it’s another thing to ship it. Time to get to work.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>automation</category>
    </item>
    <item>
      <title>Dev log #12 Hardening WebRTC and Polishing the UI: A Week of Networking and Refinement</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Mon, 13 Jul 2026 03:13:05 +0000</pubDate>
      <link>https://dev.to/yashksaini/dev-log-12-hardening-webrtc-and-polishing-the-ui-a-week-of-networking-and-refinement-29m2</link>
      <guid>https://dev.to/yashksaini/dev-log-12-hardening-webrtc-and-polishing-the-ui-a-week-of-networking-and-refinement-29m2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Spent the week balancing deep p2p networking work in Python with some much-needed UI polish on my personal site. 11 commits and 6 PRs later, I hit a perfect 7-day streak and made the codebase a bit more secure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This week was all about the "invisible" work that makes software feel solid. I spent a good chunk of time in the weeds of p2p networking, specifically hardening WebRTC implementations, while also carving out time to refine the typography and feel of my personal portfolio. With 11 commits across 5 repos and 6 PRs in flight, I managed to keep the momentum going every single day of the week.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT I BUILT
&lt;/h2&gt;

&lt;p&gt;Most of my direct commit activity this week was split between keeping my dev environment sharp and making my portfolio feel a bit more "me."&lt;/p&gt;

&lt;h3&gt;
  
  
  Portfolio &amp;amp; Personal Branding
&lt;/h3&gt;

&lt;p&gt;I spent some quality time in &lt;code&gt;yashksaini-coder/portfolio&lt;/code&gt;. If you're like me, you can't leave your personal site alone for more than a month. I pushed a few updates to the blog content, but the real fun was in the UI/UX tweaks. I swapped out the primary typography for &lt;strong&gt;JetBrains Mono&lt;/strong&gt;—there’s just something about a good monospace font that makes a dev portfolio feel right. &lt;/p&gt;

&lt;p&gt;I also went through a "make-interfaces-feel-better" phase. I refactored the &lt;code&gt;selectedwork&lt;/code&gt; section, specifically dropping a cursor-follow preview tile that felt a bit too "heavy" and replaced it with something more streamlined. I also polished the index rows to make the transitions feel snappier. It’s about +452/-279 lines of code, which is a healthy amount of churn for a week that was supposed to be about "minor" updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Maintenance Grind
&lt;/h3&gt;

&lt;p&gt;My &lt;code&gt;nvim&lt;/code&gt; config is basically a living organism at this point. I have CI set up to automatically track plugin updates, and this week was particularly noisy with 6 commits just keeping the toolchain current. It’s [skip ci] territory, but it ensures that when I sit down to actually write code, my editor isn't lagging behind the latest Lua API changes.&lt;/p&gt;

&lt;p&gt;I also did a quick version bump for &lt;code&gt;trx&lt;/code&gt; (v0.1.13) and merged a branch in &lt;code&gt;openslate&lt;/code&gt; to fix some styling and search logic in the command palette. It’s the kind of small-scale maintenance that prevents technical debt from snowballing.&lt;/p&gt;

&lt;h2&gt;
  
  
  PULL REQUESTS
&lt;/h2&gt;

&lt;p&gt;The PR list is where the "real" engineering happened this week. I’ve got five open PRs and one merged, mostly focused on the libp2p ecosystem and some documentation DX.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deep Diving into WebRTC (py-libp2p)
&lt;/h3&gt;

&lt;p&gt;The bulk of my brainpower went into &lt;code&gt;libp2p/py-libp2p&lt;/code&gt;. I’ve been working on a few critical pieces for the WebRTC-Direct transport:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Shared-port dispatching&lt;/strong&gt;: I opened a PR to add &lt;code&gt;UdpMux&lt;/code&gt;. This is a big one for anyone running nodes behind restrictive NATs. It allows for shared-port inbound dispatching, which is essential for making WebRTC-Direct viable in the real world. That PR alone is +541 lines of new logic.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Security &amp;amp; Hardening&lt;/strong&gt;: I submitted a fix to harden the &lt;code&gt;/sdp&lt;/code&gt; HTTP server against memory-amplification DoS attacks. Networking code is always a target, and making sure we aren't vulnerable to simple memory exhaustion is a priority.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Refactoring &amp;amp; Cleanup&lt;/strong&gt;: I also managed to delete some code! I deduplicated &lt;code&gt;_varint.py&lt;/code&gt; in favor of the centralized &lt;code&gt;libp2p.utils.varint&lt;/code&gt;. It’s only a net loss of about 60 lines, but every line of code removed is a line I don't have to debug later.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Testing&lt;/strong&gt;: Re-enabled some stale IPv6 integration tests for the TCP transport. Green CI is the only CI I trust.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Documentation DX
&lt;/h3&gt;

&lt;p&gt;Over at Hugging Face, I opened a PR for &lt;code&gt;tau&lt;/code&gt; to add a "copy" button to code blocks in the docs. It’s a small thing, but as a dev, there is nothing more annoying than trying to manually select a multi-line command and accidentally grabbing the line numbers. I opened an issue for it first to track the requirement and then followed up with the implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  CODE REVIEWS
&lt;/h2&gt;

&lt;p&gt;I spent some time on the other side of the fence this week, too. I reviewed and approved a PR for &lt;code&gt;trx&lt;/code&gt; that was updating the main branch with new contributor changes. It was a relatively light review week, but I always try to make sure I’m unblocking others, especially in smaller OSS projects where contributor momentum is everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  TECH STACK
&lt;/h2&gt;

&lt;p&gt;This week was a polyglot's dream. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Python&lt;/strong&gt;: My primary tool for the p2p networking work. Between &lt;code&gt;UdpMux&lt;/code&gt; and DoS protection, I was deep in the Python standard library and libp2p internals.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;TypeScript &amp;amp; MDX&lt;/strong&gt;: Used these for the portfolio updates. The JetBrains Mono swap and the UI refactor kept me busy in the frontend.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Rust&lt;/strong&gt;: Handled the version bumping and chore work for &lt;code&gt;trx&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Lua&lt;/strong&gt;: Mostly automated, but still the backbone of my editing environment.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Svelte&lt;/strong&gt;: Touched this briefly while helping out with the &lt;code&gt;openslate&lt;/code&gt; command palette fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a &lt;strong&gt;7-day streak&lt;/strong&gt;, I’ve stayed consistent. The line delta (+609 / -306) shows that while I'm building new features (like the WebRTC muxing), I'm still making a conscious effort to refactor and delete the old cruft.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHAT'S NEXT
&lt;/h2&gt;

&lt;p&gt;The WebRTC PRs in &lt;code&gt;py-libp2p&lt;/code&gt; are the priority for next week. I need to shepherd those through the review process and address any feedback on the &lt;code&gt;UdpMux&lt;/code&gt; implementation. I’m also looking forward to seeing the copy button land in the &lt;code&gt;tau&lt;/code&gt; docs—it’s a small win for developer experience. &lt;/p&gt;

&lt;p&gt;On the personal side, now that the typography on the portfolio is settled, I might start looking into some more interactive elements for the blog section. But for now, I’m just happy to have a week of green squares and solid networking code under my belt. See you in the next push!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Yash K Saini&lt;/strong&gt; — Engineer, building in public — AI/ML, low-level (Rust/C/C++), and open source.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://x.com/0xcrackedDev" rel="noopener noreferrer"&gt;X&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/yashksaini" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://yashksaini.vercel.app/" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Generated by &lt;a href="https://github.com/yashksaini-coder/DevNotion" rel="noopener noreferrer"&gt;DevNotion&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

</description>
      <category>portfolio</category>
      <category>opensource</category>
      <category>weekendchallenge</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Adding WebRTC to py-libp2p: Two Runtimes, Three Specs, and One Unforgiving Private Slot</title>
      <dc:creator>Yash Kumar Saini</dc:creator>
      <pubDate>Mon, 13 Jul 2026 03:07:12 +0000</pubDate>
      <link>https://dev.to/yashksaini/adding-webrtc-to-py-libp2p-two-runtimes-three-specs-and-one-unforgiving-private-slot-3n22</link>
      <guid>https://dev.to/yashksaini/adding-webrtc-to-py-libp2p-two-runtimes-three-specs-and-one-unforgiving-private-slot-3n22</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — py-libp2p &lt;a href="https://github.com/libp2p/py-libp2p/pull/1309" rel="noopener noreferrer"&gt;PR #1309&lt;/a&gt; adds an &lt;em&gt;experimental&lt;/em&gt; WebRTC transport (&lt;code&gt;/webrtc-direct&lt;/code&gt; and &lt;code&gt;/webrtc&lt;/code&gt;) as a v1 node-to-node foundation. It's ~6,700 lines across 41 files, gated by &lt;code&gt;libp2p[webrtc]&lt;/code&gt; + &lt;code&gt;enable_webrtc&lt;/code&gt;, and merged after four rounds of maintainer review. This is the story of what's inside, why the seams are where they are, and the three bugs that would have shipped silently if the loopback test hadn't been written.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  A quick honest note before we start
&lt;/h2&gt;

&lt;p&gt;I want to be upfront about how this post came together. This is a &lt;em&gt;reflective&lt;/em&gt; engineering write-up on a PR I authored, with the goal of turning the review conversation, spec cross-references, and multi-round fixes into something a future reader can learn from without having to scroll 400 GitHub comments.&lt;/p&gt;

&lt;p&gt;Where I quote maintainers, those are their words. Where I explain "why we did it this way," I've tried to preserve the trade-offs as they actually came up in review — not the polished, past-tense version. This isn't a marketing post. WebRTC in py-libp2p is &lt;em&gt;experimental v1&lt;/em&gt;; browser interop is not supported; there are follow-ups filed. I'll say that plainly throughout.&lt;/p&gt;

&lt;p&gt;If you're new to libp2p, WebRTC, or trio, there's enough background woven in to keep you oriented. If you're deep in this ecosystem already, the interesting sections are the framing, the certificate pin, and the two-runtime bridge — feel free to jump.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: why a Python libp2p wants WebRTC at all
&lt;/h2&gt;

&lt;p&gt;libp2p is a modular network stack: identity, transports, security, multiplexing, routing, and pub/sub, each swappable. Its Go and JS implementations have had WebRTC for a while. Python did not — &lt;a href="https://github.com/libp2p/py-libp2p/issues/546" rel="noopener noreferrer"&gt;issue #546&lt;/a&gt; tracks the gap.&lt;/p&gt;

&lt;p&gt;Why care? Three reasons the ecosystem keeps coming back to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;NAT traversal for real users.&lt;/strong&gt; Most consumer machines sit behind at least one NAT, often more. WebRTC's ICE + STUN + TURN dance is the only widely-deployed answer that browsers speak natively. If your Python peer wants to talk to a browser peer — or two NATed Python peers want to talk directly with only a light-touch relay — WebRTC is the door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native multiplexing.&lt;/strong&gt; WebRTC data channels give you real per-stream framing over SCTP. That means the transport itself carries a "muxer," so you can skip the Yamux / Mplex upgrade step that a TCP-based libp2p connection needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-to-browser without an HTTP server.&lt;/strong&gt; With &lt;code&gt;/webrtc-direct&lt;/code&gt;, a Python node can accept an inbound connection from a browser peer &lt;em&gt;purely&lt;/em&gt; via a UDP socket + a certificate hash embedded in the multiaddr. No signaling server, no HTTPS termination, no TURN unless traversal forces you there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's the "why." Now for the "how" — and specifically, why "how" turned into 6,700 lines of Python that took four review rounds to get right.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually landed
&lt;/h2&gt;

&lt;p&gt;The PR body says it, but here it is in one paragraph so you know the surface area:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WebRTCDirectTransport&lt;/code&gt; (&lt;code&gt;/webrtc-direct&lt;/code&gt;) and &lt;code&gt;WebRTCPrivateTransport&lt;/code&gt; (&lt;code&gt;/webrtc&lt;/code&gt;, relay-signaled).&lt;/li&gt;
&lt;li&gt;Spec-compliant data-channel framing — uvarint length-prefixed protobuf, with the full FIN / FIN_ACK / STOP_SENDING / RESET state machine.&lt;/li&gt;
&lt;li&gt;In-band data channels for application streams; the Noise handshake channel (&lt;code&gt;id=0&lt;/code&gt;) stays pre-negotiated per spec.&lt;/li&gt;
&lt;li&gt;Signaling with the bilateral &lt;code&gt;ICE_DONE&lt;/code&gt; fix from &lt;a href="https://github.com/libp2p/specs/pull/585" rel="noopener noreferrer"&gt;libp2p/specs#585&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Noise XX with a prologue that binds the handshake to the DTLS certificate fingerprints.&lt;/li&gt;
&lt;li&gt;ECDSA P-256 cert generation with multihash / multibase fingerprint encoding.&lt;/li&gt;
&lt;li&gt;A DTLS cert pin that actually pins the cert. More on that shortly.&lt;/li&gt;
&lt;li&gt;A trio ↔ asyncio bridge, because py-libp2p is trio and aiortc is asyncio.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final tests count: &lt;strong&gt;153 / 153 passing&lt;/strong&gt;, including a real &lt;code&gt;RTCPeerConnection&lt;/code&gt; ↔ &lt;code&gt;RTCPeerConnection&lt;/code&gt; loopback test that exercises framing, chunked writes over the 16 KiB SCTP ceiling, and the SDP-fingerprint canary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's out of scope&lt;/strong&gt;, and this matters: browser interop is &lt;em&gt;not&lt;/em&gt; supported here; go / js &lt;code&gt;/webrtc-direct&lt;/code&gt; interop is &lt;em&gt;not&lt;/em&gt; wired (they reconstruct the SDP from STUN &lt;code&gt;USERNAME&lt;/code&gt;, we don't have that hook yet); the &lt;code&gt;/webrtc&lt;/code&gt; dial path is a listener + signaling skeleton only. All of that is deliberate. This is v1. v2 (&lt;a href="https://github.com/libp2p/specs/pull/715" rel="noopener noreferrer"&gt;specs#715&lt;/a&gt;) is where browser dial actually happens, and we wanted the v1 skeleton in the tree so it stops being vaporware.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture in one picture
&lt;/h2&gt;

&lt;p&gt;Two flavors of WebRTC show up in py-libp2p, and it took me a while to keep them straight. Here they are, side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  subgraph Direct["/webrtc-direct (server-reachable)"]
    A[Client] -- SDP offer built&amp;lt;br/&amp;gt;from server multiaddr --&amp;gt; B[Server&amp;lt;br/&amp;gt;publishes cert hash]
    A &amp;lt;-. DTLS + data channels .-&amp;gt; B
  end

  subgraph Private["/webrtc (both peers NATed)"]
    C[Peer A] --- R[Relay v2]
    D[Peer B] --- R
    C -. signaling over relay:&amp;lt;br/&amp;gt;SDP + ICE + ICE_DONE .-&amp;gt; D
    C &amp;lt;-. direct DTLS + data channels .-&amp;gt; D
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two transports, one shared substrate: certs, SDP, Noise binding, stream framing, and the trio↔asyncio bridge. Only signaling differs.&lt;/p&gt;

&lt;p&gt;Inside a single peer, the runtime layout looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TB
  subgraph Trio["Trio world (py-libp2p)"]
    T1[Host / Swarm]
    T2[WebRTCConnection]
    T3[WebRTCStream × N]
    T4[Application handlers]
  end

  subgraph Bridge["AsyncioBridge&amp;lt;br/&amp;gt;(background daemon thread)"]
    BR[asyncio event loop]
  end

  subgraph Asyncio["Asyncio world (aiortc)"]
    A1[RTCPeerConnection]
    A2[RTCDataChannel × N]
    A3[DTLS / ICE / SCTP]
  end

  T1 --&amp;gt; T2 --&amp;gt; T3
  T2 -- run_coro --&amp;gt; BR
  BR -- runs on --&amp;gt; A1
  A1 --&amp;gt; A2 --&amp;gt; A3
  A2 -. on_data via trio_token .-&amp;gt; T3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting seam is the arrow labelled &lt;em&gt;trio_token&lt;/em&gt;: aiortc calls into us from an asyncio thread, and we need those callbacks to hand data to trio memory channels &lt;em&gt;safely&lt;/em&gt;. If we skip the token, we get &lt;code&gt;RunFinishedError&lt;/code&gt; and worse. That code lives in &lt;code&gt;stream.py:382-410&lt;/code&gt; — I'll come back to it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive #1 — framing, or "why bare protobuf was the wrong wire"
&lt;/h2&gt;

&lt;p&gt;If you read the first draft of this PR, application-level writes were serialized as bare protobuf &lt;code&gt;Message&lt;/code&gt; bytes and shoved straight into &lt;code&gt;data_channel.send(...)&lt;/code&gt;. That "works" on the happy path for small payloads. It's also wrong for two independent reasons — and once you understand &lt;em&gt;both&lt;/em&gt;, you understand why the spec is written the way it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason 1: SCTP has a 16 KiB message ceiling
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/libp2p/specs/blob/master/webrtc/webrtc.md" rel="noopener noreferrer"&gt;libp2p WebRTC spec&lt;/a&gt; caps each data-channel message at 16 384 bytes. Below that, you're fine. Above, aiortc will happily hand SCTP an oversized message that gets fragmented in ways your peer can't reassemble, or your peer will drop it, or SCTP itself will complain — the failure modes vary by implementation, which is worse than a clean rejection.&lt;/p&gt;

&lt;p&gt;The fix is to chunk on the sender's side and let the read side see one wire message per SCTP message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason 2: bare protobuf isn't self-delimiting inside a stream frame
&lt;/h3&gt;

&lt;p&gt;Even inside the 16 KiB limit, a Go or JS peer decoding your message needs a length prefix. The spec is explicit: &lt;strong&gt;each SCTP message is one uvarint length prefix + one protobuf &lt;code&gt;Message&lt;/code&gt;&lt;/strong&gt;. Not "raw protobuf." Not "concatenated protobufs." Exactly one framed pair per SCTP datagram.&lt;/p&gt;

&lt;p&gt;Here's the on-wire shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────┬──────────────────────────────────────────┐
│  uvarint length   │  protobuf Message (flag? + message? bytes)│
└───────────────────┴──────────────────────────────────────────┘
                    ← up to 16 KiB total, hard ceiling ──────→
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The uvarint itself is straightforward — 7 bits of payload per byte, top bit for continuation, capped at 10 bytes so it fits &lt;code&gt;uint64&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/_varint.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;encode_uvarint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uvarint cannot encode negative values&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="mh"&gt;0x80&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sender side of a stream then becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/stream.py
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;StreamState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;WebRTCStreamError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stream was reset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_write_closed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;WebRTCStreamError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Write side is closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;MAX_PAYLOAD_SIZE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MAX_PAYLOAD_SIZE&lt;/code&gt; is chosen so that even with the largest reasonable varint length prefix plus protobuf tag bytes, the full framed wire message stays under 16 KiB. The chunk boundary is on the sender; the reader just decodes one frame per SCTP message.&lt;/p&gt;

&lt;p&gt;And this is worth calling out — there's one spec detail that's easy to miss:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The spec allows a message to carry both data and FIN, and the data must be delivered to the reader before the read channel is closed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the receive path enqueues payload &lt;em&gt;first&lt;/em&gt;, then processes the flag. Getting that order wrong gives you a data loss bug that only shows up on the last chunk of a stream close. From &lt;code&gt;stream.py:352-380&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/stream.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_apply_on_trio_thread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Enqueue payload BEFORE processing flags — the spec allows a
&lt;/span&gt;    &lt;span class="c1"&gt;# message to carry both data and FIN, and the data must be
&lt;/span&gt;    &lt;span class="c1"&gt;# delivered to the reader before the read channel is closed.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_read_send&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_nowait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WouldBlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_read_closed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_enqueue_eof_sentinel_locked&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_schedule_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIN_ACK&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIN_ACK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_fin_ack_received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment isn't decoration; it's a spec citation for a future reader who wonders why the ordering matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive #2 — the certificate pin that actually pins
&lt;/h2&gt;

&lt;p&gt;This is my favourite bug in the PR. It's the kind of bug that survives green tests because the failure is silent, the API looks correct, and the wrong value is only observable if you cross-check two places that most people cross-check exactly once — at spec-reading time, then never again.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting the scene
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;/webrtc-direct&lt;/code&gt; works by embedding the server's DTLS certificate hash &lt;em&gt;in the multiaddr&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/ip4/127.0.0.1/udp/9090/webrtc-direct/certhash/uEiA…/p2p/12D3K…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client dials that address, constructs an SDP offer locally (there's no signaling server), and expects the server's DTLS handshake to present a cert whose SHA-256 matches the &lt;code&gt;certhash&lt;/code&gt; in the multiaddr. If they don't match, the client bails — that's the whole security story of &lt;code&gt;/webrtc-direct&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which means: whatever cert the server &lt;em&gt;advertises&lt;/em&gt;, it must actually &lt;em&gt;use&lt;/em&gt; in the DTLS handshake. If those two paths diverge, you either get a broken connection or, worse, a working connection that isn't actually pinned to anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  The naïve pin
&lt;/h3&gt;

&lt;p&gt;In aiortc, you'd expect to set the cert on the &lt;code&gt;RTCPeerConnection&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_certificates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;my_cert&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# ← looks fine, right?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a silent no-op. &lt;code&gt;RTCPeerConnection&lt;/code&gt; reads only its own name-mangled private attribute — &lt;code&gt;self.__certificates&lt;/code&gt; inside the class, which mangles to &lt;code&gt;self._RTCPeerConnection__certificates&lt;/code&gt; from the outside. The &lt;code&gt;pc._certificates&lt;/code&gt; attribute lives on the object, sure, but aiortc never looks at it. It happily generates its &lt;em&gt;own&lt;/em&gt; certificate at construction time and uses that in DTLS. Your advertised &lt;code&gt;/certhash/&lt;/code&gt; is a completely different digest.&lt;/p&gt;

&lt;p&gt;The maintainer review flagged this indirectly — the loopback test caught it directly. Every test that ran without an actual peer connection passed. The moment we ran an SDP round-trip and grepped the &lt;code&gt;a=fingerprint&lt;/code&gt; line, the pinned cert was nowhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/_aiortc_helpers.py
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_peer_connection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;rtc_cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RTCCertificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ice_servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Create an RTCPeerConnection pinned to the given certificate.

    aiortc &amp;gt;= 1.5 no longer accepts certificates= in RTCConfiguration —
    passing it raises TypeError.  We build the peer connection with ICE
    servers only, then replace the auto-generated DTLS certificate before
    any SDP operation triggers the DTLS handshake.  The fingerprint and
    the actual handshake cert are read from a name-mangled private
    attribute (self.__certificates inside RTCPeerConnection →
    self._RTCPeerConnection__certificates from the outside).  Setting
    the un-mangled pc._certificates is a no-op that aiortc never reads.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iceServers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ice_servers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ice_servers&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RTCPeerConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Must use the mangled name — aiortc reads only self.__certificates.
&lt;/span&gt;    &lt;span class="n"&gt;pc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_RTCPeerConnection__certificates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rtc_cert&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore[attr-defined]
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two characters (&lt;code&gt;__&lt;/code&gt;) that would look like a typo in a code review, guarding a subtle security property.&lt;/p&gt;

&lt;h3&gt;
  
  
  The canary
&lt;/h3&gt;

&lt;p&gt;Fixing the bug is one thing; making sure it stays fixed is another. The loopback test now includes a &lt;em&gt;canary&lt;/em&gt; — after the SDP exchange, it parses the &lt;code&gt;a=fingerprint:sha-256&lt;/code&gt; line out of the offer and asserts it equals the SHA-256 of the pinned cert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/core/transport/webrtc/test_multiplexing_loopback.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_sdp_fingerprint_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebRTCCertificate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Render *cert*&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s SHA-256 fingerprint in the colon-separated upper-hex
    form aiortc writes into SDP a=fingerprint:sha-256 ... lines.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If somebody in the future refactors the pin — or aiortc renames &lt;code&gt;__certificates&lt;/code&gt; — this test fails immediately, with a message that points straight at the divergence. That's the whole point of a canary: it wouldn't have been added if the bug hadn't happened, and it stays there so the bug can't come back.&lt;/p&gt;

&lt;p&gt;The subplot here isn't "aiortc is buggy" — it's that libraries with public APIs sometimes leak private-attribute contracts into their integrators, and when they do, the only defense is a test that observes the on-wire outcome, not the API you &lt;em&gt;think&lt;/em&gt; you called.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive #3 — bridging two runtimes without blowing up
&lt;/h2&gt;

&lt;p&gt;py-libp2p is trio. aiortc is asyncio. Both are event loops. Neither will &lt;code&gt;await&lt;/code&gt; the other's coroutines. This isn't a limitation of Python; it's a design decision by trio (structured concurrency, no thread-safety-by-default) that leans hard against interop.&lt;/p&gt;

&lt;p&gt;The options are: pick one runtime, run both loops in one thread with a bridge library, or run the second loop in its own thread. Option one wasn't available — swapping either dependency was a non-starter. Option two (&lt;code&gt;anyio&lt;/code&gt;, &lt;code&gt;trio-asyncio&lt;/code&gt;) has enough corner cases with aiortc's assumptions that we didn't want to chase them under a review deadline. Option three is boring and explicit, which is what we wanted.&lt;/p&gt;

&lt;p&gt;The bridge is a small class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/_asyncio_bridge.py
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AsyncioBridge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Manages a background asyncio event loop for running aiortc coroutines.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_loop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AbstractEventLoop&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_thread&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="c1"&gt;# Protects start/stop against concurrent trio calls
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# Protects _loop/_started/_stopped reads from run_coro against
&lt;/span&gt;        &lt;span class="c1"&gt;# concurrent stop() — needed because run_coro_sync is called
&lt;/span&gt;        &lt;span class="c1"&gt;# from non-trio threads (asyncio callbacks).
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_state_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two locks — a &lt;code&gt;trio.Lock&lt;/code&gt; for the trio side of start/stop, and a &lt;code&gt;threading.Lock&lt;/code&gt; because &lt;em&gt;asyncio callbacks&lt;/em&gt; can hit the bridge from a foreign thread. If you only have the trio lock, you race with those callbacks and it manifests as intermittent &lt;code&gt;AsyncioBridgeError: not running&lt;/code&gt; right at shutdown.&lt;/p&gt;

&lt;p&gt;The heart of it is &lt;code&gt;run_coro&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/_asyncio_bridge.py
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_coro&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Coroutine&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_state_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_loop&lt;/span&gt;
        &lt;span class="n"&gt;running&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_started&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stopped&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;running&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AsyncioBridgeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AsyncioBridge is not running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_coroutine_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_wait_for_result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# abandon_on_cancel=True lets trio cancel the scope immediately.
&lt;/span&gt;        &lt;span class="c1"&gt;# The background thread is abandoned but we cancel the asyncio
&lt;/span&gt;        &lt;span class="c1"&gt;# future below so it doesn't leak.
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;_wait_for_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abandon_on_cancel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cancelled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two subtle things worth pausing on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;abandon_on_cancel=True&lt;/code&gt;&lt;/strong&gt; — without it, trio's cancellation waits for the thread to unwind before actually cancelling. With aiortc's coroutines that can mean seconds, or forever. With &lt;code&gt;abandon_on_cancel=True&lt;/code&gt;, trio releases the scope immediately, we forward the cancellation to the asyncio future, and the background loop cleans up on its own timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;_state_lock&lt;/code&gt; snapshot&lt;/strong&gt; — we read &lt;code&gt;loop&lt;/code&gt; and &lt;code&gt;running&lt;/code&gt; under the lock, then use the local &lt;code&gt;loop&lt;/code&gt; variable. If &lt;code&gt;stop()&lt;/code&gt; runs concurrently, we either dispatched to a valid loop (which will handle the cancel) or we raise cleanly. Never a race where we scheduled on a partially-torn-down loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reverse direction — aiortc calling &lt;em&gt;us&lt;/em&gt; — needs a different trick. When a data channel fires an &lt;code&gt;on_message&lt;/code&gt; event, we're on the asyncio thread, and we need to push bytes into a &lt;code&gt;trio.MemorySendChannel&lt;/code&gt;. Direct calls into trio primitives from a foreign thread are undefined behavior. The trick is &lt;code&gt;trio.lowlevel.current_trio_token()&lt;/code&gt;, captured at construction time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/stream.py
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_trio_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lowlevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_trio_token&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_trio_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_run_on_trio_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;[[],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_trio_token&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lowlevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;in_trio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;in_trio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;in_trio&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trio_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;trio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunFinishedError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we're already on a trio task (tests), we call inline. If we're on an asyncio callback thread, we go through &lt;code&gt;trio.from_thread.run_sync&lt;/code&gt; with the token. If trio has already shut down, we log and drop — not raise, because the callback is fire-and-forget from aiortc's point of view.&lt;/p&gt;

&lt;p&gt;This is the code that makes the whole transport not-flaky. It's ~30 lines. It took two review cycles to converge on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive #4 — Noise XX, but bound to DTLS
&lt;/h2&gt;

&lt;p&gt;WebRTC already gives you an encrypted DTLS channel between two peers. Why do we still run Noise on top of it?&lt;/p&gt;

&lt;p&gt;Because DTLS authenticates the &lt;em&gt;certificate&lt;/em&gt;, not the &lt;em&gt;libp2p peer&lt;/em&gt;. Anyone with a valid ECDSA P-256 cert can complete the DTLS handshake. But libp2p peers are identified by their libp2p identity key, which is a completely different keypair. So we need a step where each side proves control of a libp2p identity — and the natural way to do that is Noise XX, which is already how libp2p peers authenticate on TCP.&lt;/p&gt;

&lt;p&gt;But there's a subtle attack here: what if an attacker sits between the two peers, has their own valid DTLS cert, and forwards both DTLS sessions? Both peers see a valid DTLS handshake, then run Noise on top — and if Noise doesn't know anything about DTLS, it authenticates fine end-to-end &lt;em&gt;through&lt;/em&gt; the attacker.&lt;/p&gt;

&lt;p&gt;The fix is the &lt;em&gt;Noise prologue&lt;/em&gt; — a byte string that's mixed into the Noise handshake state at the start. The libp2p spec binds Noise to DTLS by putting both certificate fingerprints into the prologue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/noise_handshake.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_noise_prologue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;local_fingerprint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;remote_fingerprint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Build the Noise prologue that binds the handshake to the DTLS session.

    Prologue format:
        b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;libp2p-webrtc-noise:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; + encode(local_fp) + encode(remote_fp)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;local_mh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_MH_SHA256_HEADER&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;local_fingerprint&lt;/span&gt;
    &lt;span class="n"&gt;remote_mh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_MH_SHA256_HEADER&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;remote_fingerprint&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NOISE_PROLOGUE_PREFIX&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;local_mh&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;remote_mh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an attacker sits in the middle, each side sees a &lt;em&gt;different&lt;/em&gt; remote fingerprint (the attacker's), so the prologues mismatch, and Noise fails. Clean. Nothing needs to change in the Noise state machine itself — the prologue is a parameter it already accepts.&lt;/p&gt;

&lt;p&gt;The rest of the handshake reuses py-libp2p's existing &lt;code&gt;PatternXX&lt;/code&gt; implementation. That's what "in-band data channels" is buying us — the Noise handshake rides over data channel 0, which is pre-negotiated (&lt;code&gt;negotiated=True, id=0&lt;/code&gt;) so both peers agree on its identity without SDP renegotiation. Application streams get their own in-band channels, so the &lt;code&gt;on('datachannel')&lt;/code&gt; event fires on the peer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; in the first draft of the PR, application streams used &lt;code&gt;negotiated=True&lt;/code&gt; as well. The consequence was that on the remote peer, the &lt;code&gt;datachannel&lt;/code&gt; event &lt;em&gt;never fired&lt;/em&gt;, because pre-negotiated channels bypass it entirely. &lt;code&gt;accept_stream()&lt;/code&gt; blocked forever. It was a classic "the spec is clear, we read it too quickly" fix — Noise uses &lt;code&gt;negotiated=True&lt;/code&gt; because it's channel 0 and both sides know it's there; application streams use &lt;code&gt;negotiated=False&lt;/code&gt; because they need to be &lt;em&gt;announced&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep dive #5 — signaling, ICE_DONE, and a spec fix in flight
&lt;/h2&gt;

&lt;p&gt;For the &lt;code&gt;/webrtc&lt;/code&gt; (relay-signaled) case, two NATed peers need to exchange SDP over some other channel to get started. That channel is a Circuit Relay v2 stream, and the protocol is &lt;code&gt;/webrtc-signaling/0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part isn't the SDP exchange — it's how you know it's &lt;em&gt;done&lt;/em&gt;. ICE candidates trickle over the signaling stream as each peer's ICE agent discovers them. At some point, both sides are done trickling, and the stream can close. But if one side closes prematurely, the other side loses candidates it hadn't received yet.&lt;/p&gt;

&lt;p&gt;The old spec left this timing ambiguous. &lt;a href="https://github.com/libp2p/specs/pull/585" rel="noopener noreferrer"&gt;libp2p/specs#585&lt;/a&gt; fixed it by adding a &lt;em&gt;bilateral&lt;/em&gt; &lt;code&gt;ICE_DONE&lt;/code&gt; message — each side signals it's done, and neither closes until both &lt;code&gt;ICE_DONE&lt;/code&gt;s are seen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Initiator                              Responder (via relay)
  ──── SDP_OFFER ─────────────────────────&amp;gt;
  &amp;lt;─── SDP_ANSWER ─────────────────────────
  &amp;lt;──&amp;gt; ICE_CANDIDATE (trickle, both ways) &amp;lt;&amp;gt;
  ──── ICE_DONE ───────────────────────────&amp;gt;
  &amp;lt;─── ICE_DONE ───────────────────────────
  (both sides close signaling stream)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's what &lt;code&gt;signaling.py&lt;/code&gt; implements. It's not the most complicated code in the PR, but it's the piece that &lt;em&gt;cannot&lt;/em&gt; be handwaved with "we'll add ICE_DONE later" — it changes the state machine of when both peers agree the signaling phase has ended.&lt;/p&gt;




&lt;h2&gt;
  
  
  The seam that will save us a rewrite: &lt;code&gt;_apply_ice_credentials()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There's one more piece of "spec is in flight" that we chose to handle &lt;em&gt;architecturally&lt;/em&gt; rather than by writing a TODO.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/libp2p/specs/issues/672" rel="noopener noreferrer"&gt;libp2p/specs#672&lt;/a&gt; discussion is about Chrome removing SDP munging for ICE credentials — a change that will eventually break the v1 way of injecting the peer's ICE ufrag into the SDP. When that lands, code that has ICE credential handling &lt;em&gt;scattered&lt;/em&gt; across the SDP builder is in for a bad time. Code that has all of it in &lt;em&gt;one function&lt;/em&gt; just changes that function.&lt;/p&gt;

&lt;p&gt;So we put all of it in one function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# libp2p/transport/webrtc/sdp.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_apply_ice_credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sdp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ufrag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fingerprint_hex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;remote_ufrag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;remote_pwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Apply ICE credentials to the SDP.

    **This is the single seam for libp2p/specs#672.**  When Chrome drops
    ICE credential munging support, only this function needs to change.
    Currently a no-op passthrough — local credentials are already in the
    SDP template.  The remote credentials are accepted but unused until
    the spec changes require injecting them via a separate mechanism.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sdp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Today it's a passthrough. Tomorrow, when v2 dispatches by &lt;code&gt;libp2p+webrtc+v1/&lt;/code&gt; / &lt;code&gt;libp2p+webrtc+v2/&lt;/code&gt; ufrag prefixes, this function grows a small branch on the ufrag prefix and everything else in the builder stays intact.&lt;/p&gt;

&lt;p&gt;The maintainer's response, verbatim, was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The v1 foundation here (framing, Noise prologue, certhash, &lt;code&gt;_apply_ice_credentials()&lt;/code&gt; as a seam) is not throwaway — it feeds v2, but &lt;code&gt;_apply_ice_credentials()&lt;/code&gt; will likely evolve into a version-dispatch layer rather than a single swap.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the value of a seam. It reserves the space for future evolution without pre-committing to which evolution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing: 153 tests, but only one of them mattered
&lt;/h2&gt;

&lt;p&gt;Unit tests are cheap; they run in ~10 seconds and give you fast feedback on individual modules. Integration tests are expensive; the two-&lt;code&gt;RTCPeerConnection&lt;/code&gt; loopback test spins up an ICE agent, waits for connectivity, and pushes data through the SCTP stack. It takes a few seconds and is annoyingly flaky under heavy CI load.&lt;/p&gt;

&lt;p&gt;But that expensive test caught three bugs the cheap ones missed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug&lt;/th&gt;
&lt;th&gt;Symptom in unit tests&lt;/th&gt;
&lt;th&gt;Symptom in loopback&lt;/th&gt;
&lt;th&gt;Caught by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pc._certificates = [...]&lt;/code&gt; no-ops&lt;/td&gt;
&lt;td&gt;✅ Green&lt;/td&gt;
&lt;td&gt;Advertised certhash ≠ SDP fingerprint&lt;/td&gt;
&lt;td&gt;SDP-fingerprint canary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application channels used &lt;code&gt;negotiated=True&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ Green&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;accept_stream()&lt;/code&gt; hangs forever&lt;/td&gt;
&lt;td&gt;Real accept-stream round trip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bare protobuf, no length prefix&lt;/td&gt;
&lt;td&gt;✅ Green&lt;/td&gt;
&lt;td&gt;16 KiB payload gets fragmented, reader dumps garbage&lt;/td&gt;
&lt;td&gt;&amp;gt; 16 KiB echo round-trip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each of these was &lt;em&gt;reachable&lt;/em&gt; by unit tests only if you knew to look for them, which no one did. That's the lesson of integration testing WebRTC: unit tests are a floor, not a ceiling, and the ceiling has real hardware in it.&lt;/p&gt;

&lt;p&gt;The loopback test's docstring calls this out explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Integration test: two real RTCPeerConnections, end-to-end loopback.

Validates the WebRTC data-channel layer at the wire level — the three
ecosystem-compatibility fixes documented in PR #1309 review notes:

  - In-band data channels (negotiated=False): the responder&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s
    &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;datachannel&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; event must fire so WebRTCConnection.accept_stream()
    unblocks.
  - uvarint length-prefixed framing: every send must stay within the
    16 KiB spec ceiling; &amp;gt; 16 KiB payload round-trips through chunking
    and reassembly.
  - DTLS certificate pinning: aiortc &amp;gt;= 1.5 dropped &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;certificates=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; in
    RTCConfiguration; the cert override must take effect before
    createOffer/createAnswer so the local DTLS fingerprint in the SDP
    matches the one our peer expects.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If somebody reads this file six months from now, they know exactly &lt;em&gt;why&lt;/em&gt; those three assertions exist. That's the value of leaving the review context in the code, not just in the PR comments.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the review looked like from the inside
&lt;/h2&gt;

&lt;p&gt;Four review pushes on top of the initial scaffolding, roughly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Correctness pass.&lt;/strong&gt; Copilot flagged 22 inline issues, mostly F401 unused imports and one substantial one — that &lt;code&gt;dial()&lt;/code&gt; was returning connections without actually running ICE/DTLS/Noise. We raised &lt;code&gt;NotImplementedError&lt;/code&gt; on the paths that weren't wired yet, so a caller would fail loudly instead of getting a phantom connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wire the transport into aiortc.&lt;/strong&gt; This is where the cert-pin bug lived, silent, until we started running real handshakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framing + in-band channels + cert pin.&lt;/strong&gt; The three bugs above, all in one series of commits. The commit messages are worth reading — each one names exactly what was wrong and why the fix is right.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-PC loopback test + newsfragment reframe.&lt;/strong&gt; Landing the integration test that guards the three fixes, and rewriting the newsfragment so "experimental v1 node-to-node scaffolding" is the first thing a changelog reader sees.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The maintainer's approving comment, in full:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Merging this as &lt;strong&gt;experimental WebRTC&lt;/strong&gt; scaffolding behind &lt;code&gt;libp2p[webrtc]&lt;/code&gt; + &lt;code&gt;enable_webrtc&lt;/code&gt;. The critical-path fixes from the re-review (uvarint framing, in-band channels + DCEP wait, cert pinning, loopback tests, docstring/newsfragment accuracy) are in place, and CI is green.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#546 stays open&lt;/strong&gt; (&lt;code&gt;Refs #546&lt;/code&gt; — do not auto-close on merge). This PR advances the parent issue but does not satisfy it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last line matters. Issue #546 is "WebRTC support in py-libp2p." This PR is not that. This PR is &lt;em&gt;scaffolding for&lt;/em&gt; that. The follow-ups — browser interop, go/js interop, full private dial — are separate issues that get their own PRs on this foundation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it goes next
&lt;/h2&gt;

&lt;p&gt;The concrete follow-ups, in the order they'll probably ship:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;STUN &lt;code&gt;USERNAME&lt;/code&gt; exposure spike in aiortc.&lt;/strong&gt; Go and JS &lt;code&gt;/webrtc-direct&lt;/code&gt; listeners reconstruct the SDP offer from the STUN username on the inbound packet. Our listener uses an HTTP &lt;code&gt;POST /sdp&lt;/code&gt; exchange as a py-to-py harness. If aiortc exposes the inbound STUN username via a hook, our listener can switch and interop lights up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private &lt;code&gt;/webrtc&lt;/code&gt; dial.&lt;/strong&gt; The listener is here, the signaling protocol is here, the &lt;code&gt;WebRTCPrivateTransport&lt;/code&gt; is here. The dial path currently constructs and returns a connection without running relay signaling — that's the next PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version dispatch in &lt;code&gt;_apply_ice_credentials()&lt;/code&gt;.&lt;/strong&gt; When &lt;code&gt;libp2p+webrtc+v1/&lt;/code&gt; and &lt;code&gt;libp2p+webrtc+v2/&lt;/code&gt; ufrag prefixes are standardized, that function grows a branch and &lt;code&gt;WebRTCDirectListener&lt;/code&gt; learns to look at the prefix. v2 is the browser-compatible path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interop tests against go-libp2p and js-libp2p.&lt;/strong&gt; These will fail today, deliberately. When the STUN username hook lands, they start passing, and we know we're on the ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are architecturally scary. The scary architecture is what this PR did: two runtimes, one wire format, one lifecycle, three specs. That's done.&lt;/p&gt;




&lt;h2&gt;
  
  
  The one takeaway I want you to leave with
&lt;/h2&gt;

&lt;p&gt;If you're building anything that crosses two runtimes, two libraries, or two specs, &lt;strong&gt;the seams are the API&lt;/strong&gt;. Not the surface API — the &lt;em&gt;internal&lt;/em&gt; seams. Where does ICE credential handling live? One function. Where does the DTLS cert pin land? One helper. Where does the trio ↔ asyncio boundary get crossed? One class.&lt;/p&gt;

&lt;p&gt;The maintainer review didn't just catch bugs. It repeatedly pushed us toward &lt;em&gt;fewer&lt;/em&gt; places where the interesting decisions were expressed. Every review round pulled scattered logic into a named function, so that when the next spec revision lands (v1 → v2, &lt;code&gt;specs#672&lt;/code&gt;, &lt;code&gt;specs#715&lt;/code&gt;), the change is localized.&lt;/p&gt;

&lt;p&gt;Scaffolding isn't just "the parts that don't work yet." Scaffolding is &lt;em&gt;the shape of the seams&lt;/em&gt;, so the parts you fill in later fit.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it, and honest expectations
&lt;/h2&gt;

&lt;p&gt;You can install and inspect it today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s1"&gt;'libp2p[webrtc]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;libp2p&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;new_host&lt;/span&gt;

&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;new_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enable_webrtc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# ...multiaddr: /ip4/…/udp/…/webrtc-direct/certhash/u…/p2p/…
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Realistic expectations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Two Python peers talking &lt;code&gt;/webrtc-direct&lt;/code&gt; on a loopback interface will work.&lt;/li&gt;
&lt;li&gt;⚠️ Python peer ↔ Go peer or Python peer ↔ JS peer over &lt;code&gt;/webrtc-direct&lt;/code&gt; will &lt;em&gt;not&lt;/em&gt; work yet (the STUN username reconstruction gap).&lt;/li&gt;
&lt;li&gt;❌ Browser peer dialing a Python peer will not work. That's v2's job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to help, the follow-up sub-issues under &lt;a href="https://github.com/libp2p/py-libp2p/issues/546" rel="noopener noreferrer"&gt;#546&lt;/a&gt; are the map. The seams are ready. The wires need connecting.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/specs/tree/master/webrtc" rel="noopener noreferrer"&gt;libp2p WebRTC spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/specs/blob/master/webrtc/webrtc-direct.md" rel="noopener noreferrer"&gt;libp2p WebRTC Direct spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/specs/pull/585" rel="noopener noreferrer"&gt;libp2p/specs#585 — bilateral ICE_DONE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/specs/issues/672" rel="noopener noreferrer"&gt;libp2p/specs#672 — ICE credential munging&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/specs/pull/715" rel="noopener noreferrer"&gt;libp2p/specs#715 — WebRTC v2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/js-libp2p/pull/3480" rel="noopener noreferrer"&gt;js-libp2p#3480 — reference v2 implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/py-libp2p/pull/1309" rel="noopener noreferrer"&gt;py-libp2p#1309 — this PR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/libp2p/py-libp2p/issues/546" rel="noopener noreferrer"&gt;py-libp2p#546 — parent issue, still open&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aiortc/aiortc" rel="noopener noreferrer"&gt;aiortc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://trio.readthedocs.io/" rel="noopener noreferrer"&gt;trio&lt;/a&gt; — structured concurrency&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://github.com/yashksaini-coder" rel="noopener noreferrer"&gt;Yash Kumar Saini&lt;/a&gt; — reflections on a merged py-libp2p PR. Corrections and pushback welcome; I'll update this post in place if I got anything wrong. The PR itself is the source of truth.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>libp2p</category>
      <category>webrtc</category>
      <category>python</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
