<?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: UNYIME ETIM</title>
    <description>The latest articles on DEV Community by UNYIME ETIM (@etim66).</description>
    <link>https://dev.to/etim66</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%2F4080884%2F19278a1e-5e98-43c8-a2ed-2012bf1ad111.png</url>
      <title>DEV Community: UNYIME ETIM</title>
      <link>https://dev.to/etim66</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/etim66"/>
    <language>en</language>
    <item>
      <title>Discovery Is Not Authentication: Designing a Local-Network File Transfer System in Rust</title>
      <dc:creator>UNYIME ETIM</dc:creator>
      <pubDate>Sat, 03 Oct 2026 09:46:51 +0000</pubDate>
      <link>https://dev.to/etim66/discovery-is-not-authentication-designing-a-local-network-file-transfer-system-in-rust-4n6o</link>
      <guid>https://dev.to/etim66/discovery-is-not-authentication-designing-a-local-network-file-transfer-system-in-rust-4n6o</guid>
      <description>&lt;p&gt;Lanweave is an open-source Rust application that sends files and folders between devices on the same local network. Two people run the terminal UI, one selects the other's device, they pair with a one-time code, and either side can propose files; the recipient reviews the full list and chooses where the files land. There is no account, no cloud service, and no background daemon. The device that starts a connection is the &lt;em&gt;initiator&lt;/em&gt;; the device that accepts it is the &lt;em&gt;responder&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This article is an engineering case study of that architecture: how discovery, encryption, pairing, per-transfer approval, session lifecycle, and filesystem handling fit together — and where the guarantees stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status, stated up front:&lt;/strong&gt; Lanweave is a work in progress, has not been audited, and is not safe for sensitive files. The pairing implementation is a prototype on an unaudited PAKE library, and the project keeps a release gate open pending specialist review. Nothing below claims the system is secure; the goal is to explain the reasoning and name what is not established. The repository is the authority for every statement: &lt;a href="https://github.com/etim66/lanweave" rel="noopener noreferrer"&gt;github.com/etim66/lanweave&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem, stated precisely
&lt;/h2&gt;

&lt;p&gt;The goal is narrow: move files between two devices on the same LAN, using only those devices, with clear consent at every step. That narrowness is what makes the security design interesting — the usual shortcut, a server both sides trust, is not available.&lt;/p&gt;

&lt;p&gt;The constraints I set for the first version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No central infrastructure.&lt;/strong&gt; The app must work on an isolated network with no internet access and no coordination server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No persistent trust.&lt;/strong&gt; Authorization belongs to one live connection: no trusted-device database, no reusable certificate, no reconnect token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LAN is hostile input.&lt;/strong&gt; Any device can publish discovery records, copy display names, open connections, and send arbitrary bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Received data is untrusted until proven otherwise.&lt;/strong&gt; A filename on the wire is not a path; a byte count on the wire is not the size of what arrives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded everything.&lt;/strong&gt; Memory, queues, parse sizes, prompt lifetimes, and idle time all have fixed limits, because "the peer behaves" is not something the receiver controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those constraints split the problem into separate questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery and authentication:&lt;/strong&gt; where might a peer be, and is this the device the user intends?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization:&lt;/strong&gt; may this peer send these files?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidentiality and integrity:&lt;/strong&gt; who can read the bytes, and is what arrived what was sent?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle:&lt;/strong&gt; how long does any of this last?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem safety:&lt;/strong&gt; what happens to the bytes after they arrive?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That decomposition produces the central claim of this article:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Discovering a device on a network is not the same thing as authenticating it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;mDNS answers &lt;em&gt;where a listener might be&lt;/em&gt;, not who operates it. Any device can advertise &lt;code&gt;_lanweave._tcp.local.&lt;/code&gt;, copy a display name, or point a connection at an endpoint it controls. A design that treats "it appeared in the device list" as trust would fail immediately. In Lanweave, discovery produces &lt;strong&gt;candidates&lt;/strong&gt;; only pairing turns a candidate into &lt;em&gt;the intended connection&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph LAN["LAN — untrusted"]
        MDNS[mDNS records&amp;lt;br/&amp;gt;spoofable, unauthenticated]
        PEER[Any TCP peer&amp;lt;br/&amp;gt;arbitrary bytes]
    end

    subgraph APP["Lanweave process"]
        DISC[Discovery&amp;lt;br/&amp;gt;candidate list, bounds, escaping]
        TLS[Transport&amp;lt;br/&amp;gt;TLS 1.3, fresh identity per connection]
        PAIR[Pairing&amp;lt;br/&amp;gt;one-time code + PAKE confirmation]
        SESS[Protocol state machine&amp;lt;br/&amp;gt;authorized idle session]
        REVIEW[Transfer review&amp;lt;br/&amp;gt;manifest + local accept/reject]
        STORE[Storage&amp;lt;br/&amp;gt;validate → temp file&amp;lt;br/&amp;gt;→ verify → publish]
    end

    USER[Local user decisions]

    MDNS --&amp;gt; DISC
    DISC --&amp;gt;|select a candidate| PEER
    PEER --&amp;gt; TLS --&amp;gt; PAIR
    USER --&amp;gt;|accept or reject request| PAIR
    PAIR --&amp;gt; SESS --&amp;gt; REVIEW
    USER --&amp;gt;|approve manifest, choose directory| REVIEW --&amp;gt; STORE&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The rest of this article walks down that stack, focusing on what messages are allowed to mean. The wire format itself — frame boundaries, decoding, streaming without loading whole files into memory — belongs to the companion article, &lt;em&gt;&lt;a href="https://dev.to/etim66/tcp-is-a-byte-stream-designing-a-framed-application-protocol-in-rust-with-tokio-4g4k"&gt;TCP Is a Byte Stream: Designing a Framed Application Protocol in Rust with Tokio&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  System shape
&lt;/h2&gt;

&lt;p&gt;Lanweave is one Cargo package: a private library and a thin binary. The modules that matter here are &lt;code&gt;discovery&lt;/code&gt;, &lt;code&gt;transport&lt;/code&gt;, &lt;code&gt;pairing&lt;/code&gt;, &lt;code&gt;protocol&lt;/code&gt;, &lt;code&gt;session&lt;/code&gt;, &lt;code&gt;transfer&lt;/code&gt;, and &lt;code&gt;storage&lt;/code&gt;, composed by a &lt;code&gt;bootstrap&lt;/code&gt; module that owns task startup and ordered shutdown.&lt;/p&gt;

&lt;p&gt;Two architectural rules shape the security reasoning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One task owns each connection and its mutable state.&lt;/strong&gt; Reader, writer, file I/O, and timer code talk to that owner through bounded channels; the UI gets a read-only snapshot and never mutates live session state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layers cannot make decisions that belong to other layers.&lt;/strong&gt; Protocol validation has no dependency on the TUI, sockets, mDNS, or the filesystem. Transport carries frames but does not decide consent. Storage validates names but never invents overwrite or rename behavior. The TUI renders state but contains no protocol rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation makes the state machine testable without a network, the frame decoder fuzzable without a filesystem, and "who is allowed to decide this?" answerable one layer at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovery: a candidate list, not a trust list
&lt;/h2&gt;

&lt;p&gt;Lanweave advertises and browses a DNS-SD service, &lt;code&gt;_lanweave._tcp.local.&lt;/code&gt;, using mDNS, so devices appear without configuration (&lt;a href="https://www.rfc-editor.org/rfc/rfc6762" rel="noopener noreferrer"&gt;RFC 6762&lt;/a&gt;, &lt;a href="https://www.rfc-editor.org/rfc/rfc6763" rel="noopener noreferrer"&gt;RFC 6763&lt;/a&gt;). The SRV record supplies the host and port. The TXT record carries exactly one value:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The listener speaks experimental protocol version 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The service instance is the local computer name, sanitized into a DNS label but keeping its casing, so the list stays human-readable. The host record is not the computer name: it is a per-run lowercase label with a random suffix, because reusing the computer name can collide with the platform's own responder. The app also tracks its own advertisement aliases and mDNS name-conflict events, so it does not list itself as a peer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why mDNS?&lt;/strong&gt; With no server in the design, there are three options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Manual &lt;code&gt;host:port&lt;/code&gt; entry&lt;/strong&gt; avoids multicast, but users must find and share addresses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A coordination service&lt;/strong&gt; assumes a reachable coordinator, contradicting the isolated-network requirement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-configuration discovery&lt;/strong&gt; (mDNS/DNS-SD) is already deployed on most LANs and needs no server, configuration, or internet connection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose the third; direct &lt;code&gt;host:port&lt;/code&gt; entry remains a fallback, not a replacement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What mDNS is not.&lt;/strong&gt; mDNS records are unauthenticated by design — a naming mechanism, not a security mechanism. Any device can publish a record, copy a name, or flood the network with noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When multicast fails.&lt;/strong&gt; Guest Wi-Fi and VLAN policies sometimes block multicast, so the app accepts a direct &lt;code&gt;host:port&lt;/code&gt; that skips mDNS and nothing else: pairing, the one-time code, and transfer approval still apply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IPv4 only, for now.&lt;/strong&gt; This version disables IPv6 in the listener and the discovery adapter: the listener binds an IPv4 wildcard socket on an ephemeral port, and the mDNS adapter disables IPv6 interfaces rather than relying on dual-stack defaults. That is a v1 limitation, not a claimed capability.&lt;/p&gt;

&lt;p&gt;The candidate store bounds the number of devices, the endpoints per device, and the byte length of service, host, and interface names, so hostile advertisements cannot become a resource problem. Names are untrusted display text: control characters and Unicode bidirectional controls are escaped into visible &lt;code&gt;\u{XXXX}&lt;/code&gt; sequences before rendering, and the device list labels them untrusted rather than treating them as identity. When a device disappears, any selection of it is cleared, so a stale row cannot connect to a device that has been replaced.&lt;/p&gt;

&lt;p&gt;Discovery is not a runtime dependency of an established session: once two peers have paired, losing the mDNS record neither closes nor alters the session. The &lt;em&gt;connection&lt;/em&gt;, not the &lt;em&gt;record&lt;/em&gt;, is the unit of trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The encrypted channel: TLS before any trust exists
&lt;/h2&gt;

&lt;p&gt;The order of operations is deliberate: TCP connects, TLS 1.3 completes, and only then does any application message flow. ALPN carries the identifier &lt;code&gt;lanweave/1&lt;/code&gt;, and the connection is TLS 1.3 only: both sides configure rustls with TLS 1.3 as the sole version, and the initiator's verifier fails closed if a TLS 1.2 handshake signature reaches it (&lt;a href="https://www.rfc-editor.org/rfc/rfc8446" rel="noopener noreferrer"&gt;RFC 8446&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why TLS rather than a custom encrypted transport?&lt;/strong&gt; Because there is no version of "we'll implement our own record layer" that ends well. TLS 1.3 has been analysed, implemented, and deployed by people whose full-time job is not a file-sharing app, and Rust has a mature implementation in &lt;a href="https://docs.rs/rustls" rel="noopener noreferrer"&gt;&lt;code&gt;rustls&lt;/code&gt;&lt;/a&gt;. Confidentiality is needed before the pairing code is used, and it must not depend on anything Lanweave-specific. Pairing then confirms &lt;em&gt;which&lt;/em&gt; connection this is; it does not need to invent encryption.&lt;/p&gt;

&lt;p&gt;Identity is the interesting question, because two devices that have never met have no certificates to verify against each other. Lanweave's answer has three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The responder generates a fresh self-signed P-256 certificate and key for every connection&lt;/strong&gt; (via &lt;code&gt;rcgen&lt;/code&gt;). Nothing is reused, so there is no long-lived local key to steal and nothing to pin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The initiator's verifier is deliberately narrow.&lt;/strong&gt; A trust-chain check would fail — the certificate is self-signed — and a server-name check would be meaningless, since the name is not an identity. The verifier relaxes only those two checks. It still requires a single well-formed X.509 end-entity certificate (no intermediates) and still verifies the TLS 1.3 &lt;code&gt;CertificateVerify&lt;/code&gt; signature with a fixed ECDSA P-256 / SHA-256 scheme, proving the responder holds the certificate's private key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The certificate is never treated as an identity.&lt;/strong&gt; It is a per-connection encryption key; user-visible identity comes from pairing.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The provisional initiator verifier: chain and server-name checks are&lt;/span&gt;
&lt;span class="c1"&gt;// relaxed, every other check is kept.&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;verify_server_cert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;end_entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;CertificateDer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;intermediates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CertificateDer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;_server_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ServerName&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;_ocsp_response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;_now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UnixTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ServerCertVerified&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TlsError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// The profile expects exactly one fresh end-entity certificate: a peer&lt;/span&gt;
    &lt;span class="c1"&gt;// that sends intermediates presents an unexpected chain shape.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;intermediates&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;TlsError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;InvalidCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CertificateError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;UnknownIssuer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Required shape: a parseable X.509 end-entity certificate.&lt;/span&gt;
    &lt;span class="nn"&gt;EndEntityCert&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;try_from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&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="nn"&gt;TlsError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;InvalidCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CertificateError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BadEncoding&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="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ServerCertVerified&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertion&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The handshake signature must use the fixed P-256 scheme, and the signature&lt;/span&gt;
&lt;span class="c1"&gt;// must verify: proof of private-key possession.&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;verify_tls13_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;CertificateDer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;DigitallySignedStruct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HandshakeSignatureValid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TlsError&lt;/span&gt;&lt;span class="o"&gt;&amp;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;dss&lt;/span&gt;&lt;span class="py"&gt;.scheme&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nn"&gt;SignatureScheme&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ECDSA_NISTP256_SHA256&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;TlsError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;PeerMisbehaved&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nn"&gt;PeerMisbehaved&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SignedHandshakeWithUnadvertisedSigScheme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;EndEntityCert&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;try_from&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="nf"&gt;.map_err&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="nn"&gt;TlsError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;InvalidCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CertificateError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BadEncoding&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;cert&lt;/span&gt;&lt;span class="nf"&gt;.verify_signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;webpki&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ring&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ECDSA_P256_SHA256&lt;/span&gt;&lt;span class="p"&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;dss&lt;/span&gt;&lt;span class="nf"&gt;.signature&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&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="nn"&gt;TlsError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;InvalidCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CertificateError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BadSignature&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="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;HandshakeSignatureValid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;assertion&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 further details matter. First, &lt;strong&gt;resumption, session tickets, pre-shared keys, 0-RTT and early application data are all disabled&lt;/strong&gt;, on both sides. Every connection gets fresh TLS state and a fresh key schedule, so no abbreviated handshake can bypass a future user decision. The server stores no sessions and sends no tickets; the client disables resumption.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;ALPN is enforced explicitly after the handshake.&lt;/strong&gt; &lt;code&gt;rustls&lt;/code&gt; alone does not fail when the peer sends no ALPN, so the code checks the negotiated protocol and rejects anything but exactly &lt;code&gt;lanweave/1&lt;/code&gt;. Tests cover mismatched and absent ALPN on both sides.&lt;/p&gt;

&lt;p&gt;After the handshake, each side derives a fresh 32-byte TLS exporter using &lt;a href="https://docs.rs/rustls/latest/rustls/struct.ConnectionCommon.html#method.export_keying_material" rel="noopener noreferrer"&gt;&lt;code&gt;export_keying_material&lt;/code&gt;&lt;/a&gt; with the &lt;code&gt;lanweave/v1&lt;/code&gt; label (&lt;a href="https://www.rfc-editor.org/rfc/rfc8446#section-7.5" rel="noopener noreferrer"&gt;RFC 8446 §7.5&lt;/a&gt;). The exporter is not a file key; it is one input to the pairing confirmation below, which binds the act of pairing to &lt;em&gt;this&lt;/em&gt; TLS connection and no other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honesty note:&lt;/strong&gt; the custom verifier and the TLS/pairing composition are flagged for specialist review. A verifier that relaxes chain checks is a sharp tool; the narrowness of the remaining checks is what makes it reviewable at all — but no review has happened yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pairing: turning a connection into the intended connection
&lt;/h2&gt;

&lt;p&gt;Lanweave separates three decisions that must never imply one another:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Who makes it&lt;/th&gt;
&lt;th&gt;What it grants&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pairing decision&lt;/td&gt;
&lt;td&gt;The responder's user accepts or rejects the request&lt;/td&gt;
&lt;td&gt;Nothing by itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code authorization&lt;/td&gt;
&lt;td&gt;The responder's user shares a one-time code; the initiator enters it&lt;/td&gt;
&lt;td&gt;An authorized session on this connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer decision&lt;/td&gt;
&lt;td&gt;The recipient's user reviews the manifest&lt;/td&gt;
&lt;td&gt;Permission to send exactly that file list&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Accepting a pairing request does not authorize the session; the code does. An authorized session does not approve any files; the recipient does that separately for every transfer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one-time code
&lt;/h3&gt;

&lt;p&gt;The pairing code is &lt;strong&gt;generated only after the responder's user accepts a live request&lt;/strong&gt;. It is eight decimal digits, kept in memory, valid for 120 seconds, usable for exactly one cryptographic pairing attempt, and never sent as a protocol field. One screen displays it, the other types it, and the humans move it through some private channel. The specification forbids it in discovery, logs, or any normal wire message.&lt;/p&gt;

&lt;p&gt;Generation uses the operating system's cryptographically secure generator with rejection sampling, so every value in &lt;code&gt;00000000..=99999999&lt;/code&gt; is equally likely and leading zeroes stay significant. A &lt;code&gt;PairingCode&lt;/code&gt; zeroizes its digits on drop, has no &lt;code&gt;Display&lt;/code&gt; implementation, and prints as &lt;code&gt;PairingCode([REDACTED])&lt;/code&gt; in &lt;code&gt;Debug&lt;/code&gt; output. A failed pairing does not reveal whether the code was wrong, expired, already used, or bound to a different connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Uniform sampling over the ten-million-value code space.&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;LIMIT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1u64&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;32&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="mi"&gt;1u64&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;CODE_RANGE&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="nf"&gt;.next_u32&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;sample&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;LIMIT&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nn"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CODE_RANGE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An eight-digit code is about 26.6 bits of entropy. That is small, and the design does not pretend otherwise: the defence is not that the code resists offline guessing, but that there is &lt;strong&gt;one online attempt per accepted request&lt;/strong&gt;, a wrong guess destroys the code and closes the connection, and the lifetime is short. The human ceremony — the responder's user must accept before a code even exists — is what keeps an attacker from grinding guesses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mutual confirmation with SPAKE2
&lt;/h3&gt;

&lt;p&gt;The cryptographic step is an RFC 9382 SPAKE2 exchange using the P-256 / SHA-256 / HKDF / HMAC ciphersuite (&lt;a href="https://www.rfc-editor.org/rfc/rfc9382" rel="noopener noreferrer"&gt;RFC 9382&lt;/a&gt;). The pairing initiator is Party A, the responder is Party B, and the identity strings (&lt;code&gt;lanweave-v1-initiator&lt;/code&gt;, &lt;code&gt;lanweave-v1-responder&lt;/code&gt;) are fixed, not negotiated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a PAKE?&lt;/strong&gt; Sending the code (or its hash) over the connection would turn it into a bearer token: anyone who learns it — including an intermediary relaying the connection — could complete the exchange, and anyone who observes or relays the transcript could verify guesses offline. A PAKE prevents both: an observer or an attacker without the code cannot verify a guess, and a wrong guess yields confirmation failure rather than a reusable signal. Lanweave does not implement the group arithmetic; the adapter wraps an existing crate, and the project forbids implementing elliptic-curve arithmetic itself.&lt;/p&gt;

&lt;p&gt;The code is mapped to the SPAKE2 password scalar as &lt;code&gt;w = OS2IP(digits) mod n&lt;/code&gt;. Because the code space is small and the group order is large, this mapping is injective over all ten million codes. The mapping is small, reviewable, and flagged for specialist review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Binding pairing to the connection
&lt;/h3&gt;

&lt;p&gt;A confirmation that only proves "both sides know a code" would be vulnerable to a split-connection relay: an intermediary that sits between the two peers and pairs separately with each side. Lanweave binds the confirmation to the live connection by constructing additional authenticated data from four length-prefixed fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;u32 length + "lanweave-v1-pairing"
u32 length + 32-byte TLS exporter
u32 length + exact initiator hello body
u32 length + exact responder hello body
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;initiator_hello&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;responder_hello&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Zeroizing&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PairingError&lt;/span&gt;&lt;span class="o"&gt;&amp;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;exporter&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;EXPORTER_LEN&lt;/span&gt;
        &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;initiator_hello&lt;/span&gt;&lt;span class="nf"&gt;.len&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;MAX_HELLO_BODY_BYTES&lt;/span&gt;
        &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;responder_hello&lt;/span&gt;&lt;span class="nf"&gt;.len&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;MAX_HELLO_BODY_BYTES&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;PairingError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InvalidInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;binding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Zeroizing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="nf"&gt;push_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BINDING_LABEL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// b"lanweave-v1-pairing"&lt;/span&gt;
    &lt;span class="nf"&gt;push_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exporter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;push_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initiator_hello&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;push_field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;binding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;responder_hello&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binding&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 length prefixes mean no combination of fields can collide with another. The &lt;code&gt;hello&lt;/code&gt; bodies are used exactly as encoded on the wire — the received bytes for the peer's message, the same deterministic encoding for the local one — and are bounded to 4,000 bytes. The SPAKE2 adapter passes this value as associated data, which RFC 9382 mixes into the confirmation key derivation, so a pairing completed across two different TLS connections cannot produce matching confirmations. That composition is flagged for adversarial testing.&lt;/p&gt;

&lt;p&gt;The exchange itself is four records: initiator share, responder share, initiator confirmation, responder confirmation. The ordering rules ensure that no side considers the session authorized until it has verified the peer and is sure its own final confirmation is on the wire:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The responder consumes the code only after the initiator's confirmation verifies.&lt;/li&gt;
&lt;li&gt;The initiator treats the session as authorized only after the responder's confirmation verifies.&lt;/li&gt;
&lt;li&gt;The responder writes and flushes its confirmation before reporting the session authorized, using a transport barrier that resolves only when the frame has actually been written and flushed. If the final confirmation is lost, pairing fails safely and the code is not reusable.
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    actor U1 as User A
    participant I as Initiator app
    participant R as Responder app
    actor U2 as User B
    U1-&amp;gt;&amp;gt;I: select a discovered device
    I-&amp;gt;&amp;gt;R: TLS 1.3 (ALPN lanweave/1), hello
    R--&amp;gt;&amp;gt;I: hello
    I-&amp;gt;&amp;gt;R: pair_request
    R-&amp;gt;&amp;gt;U2: show pairing request
    U2--&amp;gt;&amp;gt;R: accept
    R-&amp;gt;&amp;gt;R: generate one-time code
    R--&amp;gt;&amp;gt;I: pair_response(accepted)
    R-&amp;gt;&amp;gt;U2: display code locally
    U2-&amp;gt;&amp;gt;U1: share code through a private channel
    U1-&amp;gt;&amp;gt;I: enter code
    I-&amp;gt;&amp;gt;R: pairing share (Party A)
    R--&amp;gt;&amp;gt;I: pairing share (Party B)
    I-&amp;gt;&amp;gt;R: pairing confirmation (must flush)
    R--&amp;gt;&amp;gt;I: pairing confirmation (must flush)
    Note over I,R: session authorized&amp;lt;br/&amp;gt;code consumed, never sent&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;strong&gt;Prototype status.&lt;/strong&gt; The SPAKE2 adapter wraps &lt;code&gt;pakery-spake2&lt;/code&gt; and &lt;code&gt;pakery-crypto&lt;/code&gt;, newer and unaudited crates selected for the fixed ciphersuite and its point validation. The release gate stays open until independent review accepts the dependency, the password mapping, and the exporter/hello binding. Until then, this is a plausible cryptographic story, not an established one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transfer authorization: consent per payload
&lt;/h2&gt;

&lt;p&gt;An authorized session is not a file permit. After pairing, the session is idle, and either participant may propose one transfer at a time. Pairing roles — initiator and responder — stay fixed, but &lt;strong&gt;transfer roles change per request&lt;/strong&gt;: whoever proposes is the requester, and whoever reviews is the recipient. Lanweave avoids "sender" and "receiver" as session-wide identities, because those names quietly assume a direction that does not exist.&lt;/p&gt;

&lt;p&gt;A transfer request carries the complete ordered manifest: one to 1,024 entries, each a filename component and an exact byte size, plus optional folder metadata. It carries no local paths, timestamps, permissions, media types, file IDs, or pre-transfer hashes. The recipient sees the whole list — names, sizes, count, checked total, and the requester's untrusted display name — and accepts or rejects it as a unit. There is no partial approval, no remote rename, no overwrite, and the destination directory is chosen locally and never communicated to the peer.&lt;/p&gt;

&lt;p&gt;Two details keep the manifest honest. &lt;strong&gt;The requester re-checks every source before sending it&lt;/strong&gt;: if a file no longer matches the approved name, type, or size, it sends &lt;code&gt;transfer_cancel&lt;/code&gt; rather than silently changing the manifest, which is immutable once sent. And &lt;strong&gt;a request that cannot be stored is rejected before any bytes move&lt;/strong&gt;: the recipient validates every name, checks for conflicts and existing destinations, and prepares the first temporary file &lt;em&gt;before&lt;/em&gt; accepting. If preparation fails, the rejection carries a specific reason (&lt;code&gt;invalid_filename&lt;/code&gt;, &lt;code&gt;name_conflict&lt;/code&gt;, &lt;code&gt;destination_exists&lt;/code&gt;, &lt;code&gt;unavailable&lt;/code&gt;, …) and no data is sent.&lt;/p&gt;

&lt;p&gt;Because both peers can propose while idle, the protocol needs a rule for simultaneous requests. Version 1 has no request IDs, so the pairing initiator's proposal wins. If the responder has a proposal pending when the initiator's request arrives, it withdraws and re-queues its own proposal, records that exactly one stale &lt;code&gt;busy&lt;/code&gt; response is owed, and reviews the initiator's request. It consumes that stale response in whatever phase the winning transfer is in, then may send its queued proposal once the session returns to idle. The rule applies only to simultaneous proposals, not as permanent initiator priority.&lt;/p&gt;

&lt;p&gt;The cost is real: request IDs and a transaction layer would keep both proposals in flight and generalize failure handling. But "one active proposal, fixed priority, one tracked stale response" is a smaller state space — and the price is that the responder may have to wait and retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session lifecycle and failure scopes
&lt;/h2&gt;

&lt;p&gt;The session state machine is where the security properties become operational. Its rules fit in a short list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pairing must finish before the session becomes active.&lt;/li&gt;
&lt;li&gt;Only one transfer request or transfer is active at a time.&lt;/li&gt;
&lt;li&gt;Either participant can become the requester for the next transfer.&lt;/li&gt;
&lt;li&gt;Transfer completion or rejection returns to session idle.&lt;/li&gt;
&lt;li&gt;Protocol, authentication, and transport failures close the session.&lt;/li&gt;
&lt;li&gt;Closing drops all temporary authorization and requires fresh pairing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Failures are scoped in three different ways:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Outcome&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pairing rejection, failure, expiry, or cancellation&lt;/td&gt;
&lt;td&gt;Close the connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malformed framing, unsafe input, wrong-state controls&lt;/td&gt;
&lt;td&gt;Close the connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transfer rejection (including &lt;code&gt;busy&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;End the proposal; session stays idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer_cancel&lt;/code&gt; before &lt;code&gt;ready&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;End the proposal; session stays idle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer_cancel&lt;/code&gt; after &lt;code&gt;ready&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Clean the partial file; close the session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed &lt;code&gt;file_result&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Clean the partial file; close the session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;session_close&lt;/code&gt;, idle expiry, transport loss&lt;/td&gt;
&lt;td&gt;Close the session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; session_idle
    session_idle --&amp;gt; awaiting_transfer_response: local transfer_request
    session_idle --&amp;gt; reviewing_transfer: peer transfer_request
    awaiting_transfer_response --&amp;gt; session_idle: rejected / cancelled before ready
    awaiting_transfer_response --&amp;gt; awaiting_ready: accepted
    reviewing_transfer --&amp;gt; session_idle: rejected
    reviewing_transfer --&amp;gt; receiving_file: accepted and ready
    awaiting_ready --&amp;gt; sending_file: ready
    sending_file --&amp;gt; session_idle: all files verified
    receiving_file --&amp;gt; session_idle: all files verified
    sending_file --&amp;gt; closed: failure or cancel after ready
    receiving_file --&amp;gt; closed: failure or cancel after ready
    session_idle --&amp;gt; closed: session_close / 600 s idle / transport loss
    closed --&amp;gt; [*]
    note right of closed
        Authorization is destroyed; the next
        connection repeats the full pairing flow.
    end note&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The post-&lt;code&gt;ready&lt;/code&gt; rule deserves its explanation, because "close the session on one failed file" looks harsh. Once the recipient has sent &lt;code&gt;ready&lt;/code&gt;, the requester starts writing &lt;code&gt;DATA&lt;/code&gt; frames, and a later failure or cancellation may leave bytes in flight or in the sender's queues.&lt;/p&gt;

&lt;p&gt;Version 1 puts no transfer or file identifiers on &lt;code&gt;DATA&lt;/code&gt;, because the connection state already supplies that context. That keeps frames small and parsers simple — and it is also why in-flight data cannot be safely attributed once its transfer has ended. Closing the session prevents a later transfer from misinterpreting stale bytes. Rejection &lt;em&gt;before&lt;/em&gt; &lt;code&gt;ready&lt;/code&gt; has no such problem: no file data exists yet.&lt;/p&gt;

&lt;p&gt;Time bounds are part of the lifecycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session idle: 600 seconds&lt;/strong&gt;, starting when pairing completes and restarting when a transfer finishes or a proposal is rejected or cancelled before &lt;code&gt;ready&lt;/code&gt;. An active transfer is not idle, but it has its own limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt and code lifetime: 120 seconds.&lt;/strong&gt; Pairing requests, the code, and transfer approval prompts all expire, so an unattended prompt cannot hold resources forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progress deadline: 60 seconds without progress during an active transfer.&lt;/strong&gt; This is local policy, not a wire constant; a stalled sender or silent recipient closes the session after cleanup instead of hanging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handshake and control deadlines&lt;/strong&gt; bound TCP/TLS setup and the initial message exchange.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every deadline runs against a monotonic clock, so wall-clock changes cannot extend an authorization window.&lt;/p&gt;

&lt;p&gt;Closing is explicit when the connection allows it: &lt;code&gt;session_close&lt;/code&gt; carries one of three reasons (&lt;code&gt;user_closed&lt;/code&gt;, &lt;code&gt;idle_timeout&lt;/code&gt;, &lt;code&gt;shutdown&lt;/code&gt;), is valid in any authorized state, and is not acknowledged. EOF and transport loss also end the session, but they never prove an active file succeeded. That asymmetry is intentional: absence of an error is not success.&lt;/p&gt;

&lt;h2&gt;
  
  
  Filesystem safety: received data is hostile input
&lt;/h2&gt;

&lt;p&gt;The recipient's storage layer is where an adversarial manifest would do damage, so it is strict about names before anything is written:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A name must be a single non-empty filename component of at most 255 UTF-8 bytes. Empty names, &lt;code&gt;.&lt;/code&gt;, &lt;code&gt;..&lt;/code&gt;, absolute or path-like names, embedded separators (&lt;code&gt;/&lt;/code&gt;, &lt;code&gt;\&lt;/code&gt;), NUL, and control characters are rejected.&lt;/li&gt;
&lt;li&gt;Platform rules apply on top. On Windows, reserved device names (&lt;code&gt;CON&lt;/code&gt;, &lt;code&gt;PRN&lt;/code&gt;, &lt;code&gt;AUX&lt;/code&gt;, &lt;code&gt;NUL&lt;/code&gt;, &lt;code&gt;COM1&lt;/code&gt;–&lt;code&gt;COM9&lt;/code&gt;, &lt;code&gt;LPT1&lt;/code&gt;–&lt;code&gt;LPT9&lt;/code&gt;), trailing dots or spaces, and forbidden characters (&lt;code&gt;&amp;lt; &amp;gt; : " | ? *&lt;/code&gt;) are rejected, and equivalence compares case-insensitively without trailing dots or spaces.&lt;/li&gt;
&lt;li&gt;Duplicate or equivalent names within one manifest, and names that already exist in the destination, reject the whole request. Existence is checked with &lt;a href="https://doc.rust-lang.org/std/fs/fn.symlink_metadata.html" rel="noopener noreferrer"&gt;&lt;code&gt;symlink_metadata&lt;/code&gt;&lt;/a&gt;, which does not follow links.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data is written only to a temporary file: &lt;code&gt;tempfile&lt;/code&gt; creates an unpredictable &lt;code&gt;.lanweave-*.part&lt;/code&gt; name in the destination directory, without following links, and with mode &lt;code&gt;0600&lt;/code&gt; on Unix. Nothing appears under the final name until the exact declared byte count and the SHA-256 digest both match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Flush, sync, then publish without replacing anything.&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="nf"&gt;.flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="nf"&gt;.sync_all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="nf"&gt;.persist_noclobber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;final_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&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="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(()),&lt;/span&gt;
    &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&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;error&lt;/span&gt;&lt;span class="py"&gt;.error&lt;/span&gt;&lt;span class="nf"&gt;.kind&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;io&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;ErrorKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AlreadyExists&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StorageError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DestinationExists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Err&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="k"&gt;if&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;symlink_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;final_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.is_ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StorageError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DestinationExists&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Err&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="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;StorageError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Io&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;&lt;a href="https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist_noclobber" rel="noopener noreferrer"&gt;&lt;code&gt;persist_noclobber&lt;/code&gt;&lt;/a&gt; makes the final step a no-replace operation: if a destination appears after the manifest check, finalization fails instead of overwriting it. Lanweave never overwrites and never silently renames, and files are written one at a time in manifest order — the sender waits for each file's verified result before starting the next.&lt;/p&gt;

&lt;p&gt;Failure cleanup is defined by what the user keeps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;current partial file&lt;/strong&gt; is deleted.&lt;/li&gt;
&lt;li&gt;Files already &lt;strong&gt;verified in this transfer&lt;/strong&gt; remain.&lt;/li&gt;
&lt;li&gt;Later manifest entries are &lt;strong&gt;not attempted&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The sender is told the failure while the connection is still safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Received files are never automatically opened, previewed, or executed. The application's job ends at "verified bytes under a safe name".&lt;/p&gt;

&lt;h2&gt;
  
  
  Threat model: what this protects against, and what it does not
&lt;/h2&gt;

&lt;p&gt;The project's threat model assumes uncompromised endpoints, a working OS random generator, a correct TLS implementation, and users who share the code privately. Within those assumptions, a condensed excerpt:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat&lt;/th&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Remaining risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spoofed visible device&lt;/td&gt;
&lt;td&gt;Discovery is untrusted; pairing confirmation is required&lt;/td&gt;
&lt;td&gt;Attackers can create noise or denial of service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Misleading display name&lt;/td&gt;
&lt;td&gt;Escaped, labelled untrusted, never treated as identity&lt;/td&gt;
&lt;td&gt;Similar names may still confuse users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pairing-request spam&lt;/td&gt;
&lt;td&gt;Bounded requests and prompts, with deadlines&lt;/td&gt;
&lt;td&gt;Attackers can consume bounded attention and resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pairing code disclosure&lt;/td&gt;
&lt;td&gt;Generated after acceptance, displayed locally, never sent, 120 s lifetime&lt;/td&gt;
&lt;td&gt;Anyone who sees the live code may pair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Online code guessing&lt;/td&gt;
&lt;td&gt;One cryptographic attempt per accepted request, prompt limits, short expiry&lt;/td&gt;
&lt;td&gt;Attackers can cause prompts or denial of service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Split-connection intermediary&lt;/td&gt;
&lt;td&gt;Pairing confirmation bound to the TLS exporter and exact hello bodies&lt;/td&gt;
&lt;td&gt;The property is intended, not verified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replay&lt;/td&gt;
&lt;td&gt;Fresh TLS and pairing state, strict state, no resumption&lt;/td&gt;
&lt;td&gt;Replays still consume bounded parsing work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malformed frames or JSON&lt;/td&gt;
&lt;td&gt;Bounded lengths and strict schemas before allocation&lt;/td&gt;
&lt;td&gt;Parser defects may remain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal escape injection&lt;/td&gt;
&lt;td&gt;Control and bidi characters escaped for display&lt;/td&gt;
&lt;td&gt;Unicode look-alikes can still confuse users&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path traversal and overwrite&lt;/td&gt;
&lt;td&gt;Single safe names, platform checks, no-follow temp files, no-replace finalization&lt;/td&gt;
&lt;td&gt;Filesystem edge cases need platform tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Corrupt transfer&lt;/td&gt;
&lt;td&gt;TLS integrity plus exact size and SHA-256 before finalization&lt;/td&gt;
&lt;td&gt;A paired peer can intentionally choose harmful content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization reuse&lt;/td&gt;
&lt;td&gt;No trust database, resumption, reusable code, or reconnect token&lt;/td&gt;
&lt;td&gt;Users must repeat pairing after every disconnect&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The out-of-scope list matters just as much. Lanweave cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;protect a compromised computer;&lt;/li&gt;
&lt;li&gt;prove a person's real-world identity;&lt;/li&gt;
&lt;li&gt;make a received file safe to open;&lt;/li&gt;
&lt;li&gt;hide all traffic metadata — a passive LAN observer can still see that Lanweave is running and observe connection timing and volume, even though file contents are encrypted;&lt;/li&gt;
&lt;li&gt;protect a code that has been disclosed;&lt;/li&gt;
&lt;li&gt;guarantee network availability, or fully prevent denial of service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cryptographic implementation is also unaudited, which makes the entire "confidentiality and authentication" column a design intention rather than a verified property.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing as evidence of correctness — not of security
&lt;/h2&gt;

&lt;p&gt;The project tests what is testable and is careful not to over-interpret the result. At the time of writing, &lt;code&gt;cargo test --all --locked&lt;/code&gt; reports &lt;strong&gt;229 passing tests&lt;/strong&gt; across 32 test modules. The parts most relevant here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end session tests over real connections.&lt;/strong&gt; Two session services complete a real TLS 1.3 handshake, SPAKE2 exchange, and one-time code, then run transfers in both directions over loopback — folder preparation, cancellation, timeouts, manual close — with no mocked transport or pairing adapter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong-code and expiry tests.&lt;/strong&gt; A wrong code never authorizes either peer; an expired code closes pairing without authorization; a timed-out prompt rejects without ever creating a code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure-scope tests.&lt;/strong&gt; Rejection before &lt;code&gt;ready&lt;/code&gt; keeps the session usable; a cancel after &lt;code&gt;ready&lt;/code&gt; cleans the partial file and closes both peers; a stalled or silent peer closes at the progress deadline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem tests.&lt;/strong&gt; Unsafe, duplicate, equivalent, existing, and path-like names are rejected; symlink and destination races fail safely; partial files are removed on drop; no-replace finalization is asserted against a raced destination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frame and parser tests.&lt;/strong&gt; Every split point of a wire image decodes identically, byte-by-byte feeding reassembles frames, malformed headers fail before allocation, and oversized lengths are rejected regardless of how many bytes arrived. A separate fuzz target exercises the decoder with arbitrary bytes, but it is not yet in CI, and broader protocol fuzzing is planned work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this proves the cryptography is right. As the project's testing document puts it, tests show that the implementation follows the draft; they do not prove that unaudited cryptography is secure. Several gates remain open: dependency review of the PAKE crate, deterministic positive and negative test vectors, certificate-verifier review, and review of the pairing/TLS composition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations and future work
&lt;/h2&gt;

&lt;p&gt;Labelled as limitations, because they are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No audit, prototype PAKE.&lt;/strong&gt; The pairing dependency, password mapping, binding composition, and custom certificate verifier all need specialist review, and the app must not be described as secure or production-ready. &lt;code&gt;pakery-spake2&lt;/code&gt;/&lt;code&gt;pakery-crypto&lt;/code&gt; were chosen for ciphersuite fit and point validation, not for an audit history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IPv4 only for now.&lt;/strong&gt; Discovery disables IPv6 interfaces and the listener binds an IPv4 wildcard socket. IPv6 policy is future work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small code space.&lt;/strong&gt; Eight digits is ~26.6 bits; the model relies on one attempt per accepted request and a short lifetime, not offline resistance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No resume, no parallel transfers, no trusted devices.&lt;/strong&gt; Deliberate v1 exclusions; each would need a new threat review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update channel.&lt;/strong&gt; The self-updater trusts GitHub Releases and checksums, and Windows builds are not code-signed yet. That is a separate trust decision, documented as such.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Write the trust boundary before the code.&lt;/strong&gt; The most valuable early artifact was the sentence "discovery answers where, not who." It settled dozens of decisions — what the device list may claim, what the certificate may mean, what pairing must bind to — before any of them were code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate decisions are stronger than one big gate.&lt;/strong&gt; Pairing, code authorization, and transfer approval are three questions; folding them into one "trusted peer" state would have been simpler to build and worse to reason about, because a mistake in one would silently grant the others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure scope is a design decision.&lt;/strong&gt; Deciding explicitly that post-&lt;code&gt;ready&lt;/code&gt; failures close the session — and writing down why — was more valuable than defensive coding. Vague failure behavior is where protocols accumulate ambiguity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bound everything, and test at the layer where the risk is.&lt;/strong&gt; Per-kind frame limits, bounded queues, parsing before allocation, monotonic deadlines: each is small; together they define what a hostile peer can make the process do. The session tests use real TLS, real files, and real races; the frame tests cover every split point; the state machine is pure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State what the system does not do.&lt;/strong&gt; The limitations section is the part of the design most likely to be right. A tool that explains its own threat model is more trustworthy than one that asserts good intentions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The design has one organising idea: never let one layer's output be mistaken for another layer's decision. Discovery produces candidates; TLS produces a confidential channel; the code produces a confirmed live connection; the recipient produces consent; the state machine produces bounded lifetimes; storage produces a verified file. Each step can fail without silently granting the next. Lanweave is unfinished and unaudited — the pairing prototype and the custom certificate verifier are where that matters most — but the architecture makes the remaining work identifiable instead of hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source and further reading
&lt;/h2&gt;

&lt;p&gt;The implementation, protocol specification, threat model, and testing strategy are in the repository: &lt;a href="https://github.com/etim66/lanweave" rel="noopener noreferrer"&gt;github.com/etim66/lanweave&lt;/a&gt;. The protocol is documented in &lt;code&gt;docs/PROTOCOL.md&lt;/code&gt;, the security boundary in &lt;code&gt;docs/SECURITY.md&lt;/code&gt;, the adversary analysis in &lt;code&gt;docs/THREAT_MODEL.md&lt;/code&gt;, and the open review gates in &lt;code&gt;docs/CRYPTOGRAPHY.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the developer of Lanweave. I build this in the open and document its design decisions so they can be reviewed, challenged, and improved; bug reports and design criticism — especially a broken security or state rule with a reproducible example — are the most useful feedback.&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;RFC 6762 — &lt;em&gt;Multicast DNS&lt;/em&gt;: &lt;a href="https://www.rfc-editor.org/rfc/rfc6762" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc6762&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RFC 6763 — &lt;em&gt;DNS-Based Service Discovery&lt;/em&gt;: &lt;a href="https://www.rfc-editor.org/rfc/rfc6763" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc6763&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RFC 8446 — &lt;em&gt;The Transport Layer Security (TLS) Protocol Version 1.3&lt;/em&gt; (keying material exporters, §7.5): &lt;a href="https://www.rfc-editor.org/rfc/rfc8446" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc8446&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RFC 9382 — &lt;em&gt;SPAKE2, a PAKE&lt;/em&gt;: &lt;a href="https://www.rfc-editor.org/rfc/rfc9382" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc9382&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rustls&lt;/code&gt; documentation: &lt;a href="https://docs.rs/rustls" rel="noopener noreferrer"&gt;https://docs.rs/rustls&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rustls::ConnectionCommon::export_keying_material&lt;/code&gt;: &lt;a href="https://docs.rs/rustls/latest/rustls/struct.ConnectionCommon.html#method.export_keying_material" rel="noopener noreferrer"&gt;https://docs.rs/rustls/latest/rustls/struct.ConnectionCommon.html#method.export_keying_material&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tempfile::NamedTempFile::persist_noclobber&lt;/code&gt;: &lt;a href="https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist_noclobber" rel="noopener noreferrer"&gt;https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist_noclobber&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Rust standard library, &lt;code&gt;std::fs::symlink_metadata&lt;/code&gt;: &lt;a href="https://doc.rust-lang.org/std/fs/fn.symlink_metadata.html" rel="noopener noreferrer"&gt;https://doc.rust-lang.org/std/fs/fn.symlink_metadata.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>security</category>
      <category>networking</category>
      <category>architecture</category>
    </item>
    <item>
      <title>TCP Is a Byte Stream: Designing a Framed Application Protocol in Rust with Tokio</title>
      <dc:creator>UNYIME ETIM</dc:creator>
      <pubDate>Sat, 03 Oct 2026 09:45:52 +0000</pubDate>
      <link>https://dev.to/etim66/tcp-is-a-byte-stream-designing-a-framed-application-protocol-in-rust-with-tokio-4g4k</link>
      <guid>https://dev.to/etim66/tcp-is-a-byte-stream-designing-a-framed-application-protocol-in-rust-with-tokio-4g4k</guid>
      <description>&lt;p&gt;A companion article, &lt;em&gt;&lt;a href="https://dev.to/etim66/discovery-is-not-authentication-designing-a-local-network-file-transfer-system-in-rust-4n6o"&gt;Discovery Is Not Authentication&lt;/a&gt;&lt;/em&gt;, covers how Lanweave establishes an authorized session between two devices on a local network: mDNS discovery, a one-time pairing code, TLS 1.3, and separate approval for every transfer. This article starts where that trust boundary ends: once two peers have an authenticated, encrypted connection, what actually travels over it, and why is it built this way?&lt;/p&gt;

&lt;p&gt;Lanweave is a Rust terminal application for sending files and folders between devices on the same LAN (&lt;a href="https://github.com/etim66/lanweave" rel="noopener noreferrer"&gt;github.com/etim66/lanweave&lt;/a&gt;). It is a work in progress and has not been audited; nothing here is a security claim. The focus is protocol engineering: framing, strict parsing, connection state, backpressure, and streaming — the part of the system that must stay correct when the peer is hostile and the file is larger than memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability is not message orientation
&lt;/h2&gt;

&lt;p&gt;TCP guarantees that bytes arrive in order, without duplication, and without loss, or the connection fails (&lt;a href="https://www.rfc-editor.org/rfc/rfc9293" rel="noopener noreferrer"&gt;RFC 9293&lt;/a&gt;). It does &lt;strong&gt;not&lt;/strong&gt; guarantee that a &lt;code&gt;read&lt;/code&gt; corresponds to a &lt;code&gt;write&lt;/code&gt;, or that a message arrives whole. A single read may return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one byte of a header,&lt;/li&gt;
&lt;li&gt;a complete 12-byte header and half of its body,&lt;/li&gt;
&lt;li&gt;exactly one message,&lt;/li&gt;
&lt;li&gt;five messages concatenated,&lt;/li&gt;
&lt;li&gt;or any other split the kernel, network, and retransmission timing happen to produce.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a bug to be fixed; it is the interface. TCP is a &lt;em&gt;byte stream&lt;/em&gt;. TLS does not change that at the application layer: the &lt;a href="https://www.rfc-editor.org/rfc/rfc8446#section-5" rel="noopener noreferrer"&gt;record layer&lt;/a&gt; has its own framing, but an application read returns decrypted stream bytes, and no API promises that a record boundary lines up with a read or write. Any protocol whose messages are larger than one byte needs its own boundaries and validation.&lt;/p&gt;

&lt;p&gt;The failure mode is familiar: code that works on loopback with small messages, then corrupts data on a real network where a 4,000-byte write is split across several reads. The fix is not a bigger read buffer; it is an incremental parser that treats the socket as an arbitrary byte source and the protocol as a state machine over it.&lt;/p&gt;

&lt;p&gt;Lanweave's frame codec is that parser. The design rule: &lt;strong&gt;validate everything before allocating, and handle partial and coalesced reads identically to perfectly sized ones.&lt;/strong&gt; A unit test takes a wire image of several frames and decodes it with the input split at &lt;em&gt;every&lt;/em&gt; byte position; all splits must produce the same frame sequence.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TB
    A[TcpStream&amp;lt;br/&amp;gt;ordered bytes, no messages] --&amp;gt; B[TLS 1.3&amp;lt;br/&amp;gt;confidentiality + integrity]
    B --&amp;gt; C[Frame codec&amp;lt;br/&amp;gt;boundaries, lengths, kinds]
    C --&amp;gt; D[Strict JSON controls&amp;lt;br/&amp;gt;or raw DATA bytes]
    D --&amp;gt; E[Protocol state machine&amp;lt;br/&amp;gt;direction, order, phase]
    E --&amp;gt; F[Transfer engine&amp;lt;br/&amp;gt;stream, hash, verify, publish]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Each layer has one job. The frame codec knows nothing about messages, the message parser nothing about sockets, the state machine nothing about files. That separation makes each layer independently testable — and lets the frame codec be fuzzed without a network.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes on the wire
&lt;/h2&gt;

&lt;p&gt;One TCP connection carries one authorized session, and that session can carry several sequential transfers in either direction. Control traffic is strict JSON; file bytes are raw binary frames. The distinction is deliberate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JSON for control&lt;/strong&gt; because the messages are small, inspectable field names help debugging, and a strict schema is easy to test exhaustively. A &lt;code&gt;transfer_request&lt;/code&gt; is a list of names and sizes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary for file data&lt;/strong&gt; because base64 or numeric arrays would add CPU and memory overhead for no benefit. File bytes never pass through the JSON parser — the frame layer routes them to the transfer sink.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Newline-delimited JSON is a third possibility. It suits control-only protocols, but not binary file data: file bytes can contain newlines, so the data would still need an escaping layer. Length-prefixing applies one rule to both control and data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frame format
&lt;/h2&gt;

&lt;p&gt;Every control message and every binary chunk travels in one frame with a fixed 12-byte header:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;magic&lt;/td&gt;
&lt;td&gt;ASCII &lt;code&gt;LNWV&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;frame version&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;kind&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0&lt;/code&gt; = JSON control, &lt;code&gt;1&lt;/code&gt; = DATA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;header length&lt;/td&gt;
&lt;td&gt;unsigned big-endian &lt;code&gt;12&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;body length&lt;/td&gt;
&lt;td&gt;unsigned big-endian&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;N&lt;/td&gt;
&lt;td&gt;body&lt;/td&gt;
&lt;td&gt;JSON object or raw file bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The header is fixed-size and fully specified, so a parser can reject a malformed frame after 12 bytes. The body length is a 32-bit unsigned integer, but the codec never trusts it: each frame kind has a maximum, checked before any body allocation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generic JSON control body&lt;/td&gt;
&lt;td&gt;1 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DATA&lt;/code&gt; body&lt;/td&gt;
&lt;td&gt;1 MiB (minimum 1 byte)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hello&lt;/code&gt; body&lt;/td&gt;
&lt;td&gt;4,000 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pairing&lt;/code&gt; body&lt;/td&gt;
&lt;td&gt;4,096 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;transfer_request&lt;/code&gt; body&lt;/td&gt;
&lt;td&gt;256 KiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files per manifest&lt;/td&gt;
&lt;td&gt;1 to 1,024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Integer and checked total&lt;/td&gt;
&lt;td&gt;2^53 − 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One design detail is easy to miss: a &lt;code&gt;DATA&lt;/code&gt; frame carries no file index and no offset. The connection state already knows which file is in flight and how many bytes have been accepted, so repeating that would create two sources of truth that a malicious peer could make disagree. &lt;code&gt;DATA&lt;/code&gt; validity is instead a property of the protocol phase: data is acceptable only while receiving, and only after &lt;code&gt;ready&lt;/code&gt;. A zero-byte file sends no &lt;code&gt;DATA&lt;/code&gt; frame at all; it goes straight to &lt;code&gt;file_end&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note the difference between the &lt;em&gt;maximum&lt;/em&gt; frame size (1 MiB, enforced on untrusted input) and the &lt;em&gt;streaming chunk size&lt;/em&gt; (64 KiB, the sender's policy). The maximum bounds what an attacker can make the process allocate; the chunk size controls how often the sender checks channels, deadlines, and cancellation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incremental decoding without unbounded allocation
&lt;/h2&gt;

&lt;p&gt;The decoder works over a growing &lt;code&gt;BytesMut&lt;/code&gt; buffer. It returns &lt;code&gt;Ok(None)&lt;/code&gt; when more bytes are needed, &lt;code&gt;Ok(Some(frame))&lt;/code&gt; for a complete frame, and an error that closes the connection for anything malformed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;BytesMut&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FrameError&lt;/span&gt;&lt;span class="o"&gt;&amp;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;src&lt;/span&gt;&lt;span class="nf"&gt;.len&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;HEADER_LEN&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;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                          &lt;span class="c1"&gt;// wait for more 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;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;MAGIC&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FrameError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BadMagic&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;FRAME_VERSION&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FrameError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BadVersion&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;KIND_CONTROL&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;FrameKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;KIND_DATA&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;FrameKind&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;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FrameError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BadKind&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;header_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;u16&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_be_bytes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nn"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header_len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;HEADER_LEN&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FrameError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BadHeaderLength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;body_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_be_bytes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;usize&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;body_len&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="nf"&gt;.max_body&lt;/span&gt;&lt;span class="p"&gt;()&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FrameError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;BodyTooLarge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// validated before allocation&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;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nn"&gt;FrameKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;body_len&lt;/span&gt; &lt;span class="o"&gt;==&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;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FrameError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;EmptyData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HEADER_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;body_len&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;src&lt;/span&gt;&lt;span class="nf"&gt;.len&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;needed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Bounded by the just-validated body limit.&lt;/span&gt;
        &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="nf"&gt;.reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;needed&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="nf"&gt;.len&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;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="nf"&gt;.split_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;needed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="nf"&gt;.advance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HEADER_LEN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="nf"&gt;.freeze&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;FrameKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nn"&gt;FrameKind&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four properties carry most of the weight:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Validate before allocate.&lt;/strong&gt; Magic, version, kind, header length, and body length are checked from the first 12 bytes. A claimed 4 GiB body gets &lt;code&gt;BodyTooLarge&lt;/code&gt; after 12 bytes, not a 4 GiB allocation; &lt;code&gt;reserve&lt;/code&gt; only runs for a length that already passed a per-kind limit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The buffer is bounded by construction.&lt;/strong&gt; It accumulates at most one maximum-size frame plus whatever arrived after it. &lt;code&gt;split_to&lt;/code&gt; removes the consumed prefix, so memory does not grow with frame count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The body is handed off, not copied.&lt;/strong&gt; &lt;a href="https://docs.rs/bytes/latest/bytes/struct.BytesMut.html#method.split_to" rel="noopener noreferrer"&gt;&lt;code&gt;split_to&lt;/code&gt;&lt;/a&gt; gives the frame its own slice of the buffer; &lt;code&gt;advance&lt;/code&gt; skips the header; &lt;code&gt;freeze&lt;/code&gt; yields an immutable &lt;code&gt;Bytes&lt;/code&gt; the next layer can forward without copying the payload. For &lt;code&gt;DATA&lt;/code&gt;, that &lt;code&gt;Bytes&lt;/code&gt; reaches the file writer essentially unchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malformed framing is terminal.&lt;/strong&gt; There is no "skip and continue" path: a bad magic, unknown kind, or impossible header length means a bug or hostile intent, and the connection closes. Recovery would mean trusting more of the stream that just proved untrustworthy.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reading frames from a socket
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FramedConnection::read_frame&lt;/code&gt; combines the decoder with an async read loop (&lt;a href="https://docs.rs/tokio/latest/tokio/io/trait.AsyncReadExt.html#method.read_buf" rel="noopener noreferrer"&gt;&lt;code&gt;read_buf&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&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;fn&lt;/span&gt; &lt;span class="nf"&gt;read_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Frame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ReadError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ReadError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Frame&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;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;
            &lt;span class="py"&gt;.reader&lt;/span&gt;
            &lt;span class="nf"&gt;.read_buf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;.await&lt;/span&gt;
            &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ReadError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Io&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;if&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="o"&gt;==&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;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.buffer&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// clean close after complete frames&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="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ReadError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Truncated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// close in the middle of a frame&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference between &lt;code&gt;Ok(None)&lt;/code&gt; and &lt;code&gt;Err(Truncated)&lt;/code&gt; is protocol honesty in miniature. A close after complete frames is a normal end of stream, and every frame that arrived has been delivered. A close mid-frame means the stream was cut short — possibly a crash, possibly an attack — and the partial data cannot be interpreted. The transport refuses to pretend those cases are the same; a unit test writes a frame minus its last three bytes and asserts truncation rather than a clean close.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ReadError&lt;/code&gt; keeps the taxonomy narrow: I/O failure, malformed frame, or truncation. Message-level problems belong to the parser that receives the body.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strict JSON, because ambiguity is a vulnerability
&lt;/h2&gt;

&lt;p&gt;The control layer decodes a &lt;code&gt;Bytes&lt;/code&gt; body into a typed &lt;code&gt;Control&lt;/code&gt; enum. The rules are intentionally unforgiving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The body must be exactly one UTF-8 JSON object with no trailing value (&lt;a href="https://www.rfc-editor.org/rfc/rfc8259" rel="noopener noreferrer"&gt;RFC 8259&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Duplicate fields are rejected.&lt;/li&gt;
&lt;li&gt;Unknown fields are rejected once a schema has consumed its fields.&lt;/li&gt;
&lt;li&gt;Numbers are integers from &lt;code&gt;0&lt;/code&gt; through 2^53 − 1. Floats, exponent notation, and negatives are rejected; the protocol does not use &lt;code&gt;null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;String values come from closed sets (&lt;code&gt;accepted&lt;/code&gt;, &lt;code&gt;verified&lt;/code&gt;, &lt;code&gt;user_rejected&lt;/code&gt;, and so on); anything else is invalid.&lt;/li&gt;
&lt;li&gt;Field names, strings, arrays, nesting, and total body size have fixed bounds.&lt;/li&gt;
&lt;li&gt;Binary values use canonical unpadded base64url with an exact decoded length (&lt;a href="https://www.rfc-editor.org/rfc/rfc4648#section-5" rel="noopener noreferrer"&gt;RFC 4648 §5&lt;/a&gt;); digests are exactly 64 lowercase hexadecimal characters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two of these choices look like pedantry until they prevent a class of bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicate and unknown fields&lt;/strong&gt; create parser disagreement: two implementations that both "accept" &lt;code&gt;{"accepted": false, "accepted": true}&lt;/code&gt; or &lt;code&gt;{"type": "ready", "files": [...]}&lt;/code&gt; may disagree about what the message means. Rejecting removes the disagreement, and it stops a future protocol version from smuggling fields past an old implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors never carry peer-controlled text.&lt;/strong&gt; The parser's error type holds only &lt;code&gt;&amp;amp;'static str&lt;/code&gt; schema names, never the peer's bytes. Error messages are the classic place untrusted input gets logged or reflected, and a detailed parse error can become an oracle for probing the parser. The wire error codes are coarse (&lt;code&gt;invalid_message&lt;/code&gt;, &lt;code&gt;authentication_failed&lt;/code&gt;, and so on); the exact reason stays local.&lt;/p&gt;

&lt;p&gt;The parser's shape is deliberate. A JSON visitor collects entries as borrowed key/value pairs, each value an unparsed &lt;a href="https://docs.rs/serde_json/latest/serde_json/value/struct.RawValue.html" rel="noopener noreferrer"&gt;&lt;code&gt;RawValue&lt;/code&gt;&lt;/a&gt; slice of the original body. Schemas ask for fields by name, and duplicate detection happens at access time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// Returns the single value of `field`, rejecting repeats.&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="n"&gt;RawValue&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MessageError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;Some&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="k"&gt;=&amp;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;values&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.is_some&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MessageError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;DuplicateField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;))&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="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&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="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because entries are borrowed slices of the body — already capped at 1 MiB by the frame layer — parser memory is bounded by the body itself, with no intermediate tree of owned strings.&lt;/p&gt;

&lt;p&gt;On the encoding side, messages serialize with a documented field order and canonical encodings so golden fixtures are byte-stable. That reproducibility lets tests assert exact wire bytes, not just round-trips.&lt;/p&gt;

&lt;h2&gt;
  
  
  A protocol state machine, not just a parser
&lt;/h2&gt;

&lt;p&gt;Parsing tells you a message is well-formed. It cannot tell you that a message is &lt;em&gt;allowed right now&lt;/em&gt;. Lanweave answers the second question in a separate, pure module: &lt;code&gt;protocol::state&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A connection end has a &lt;code&gt;Role&lt;/code&gt; (pairing initiator or responder) and a &lt;code&gt;Phase&lt;/code&gt; tracking the exchange in progress: hello handshake, pairing request and response, pairing record count, authorized-idle, pending outbound proposal, inbound review, sending or receiving a file, terminal closing. The session owner feeds decoded messages into three functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;send(state, &amp;amp;control)&lt;/code&gt; — validates a locally generated control against the phase and returns the implied actions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;accept(state, inbound)&lt;/code&gt; — validates an inbound control or &lt;code&gt;DATA&lt;/code&gt; frame and returns actions such as "deliver these bytes", "proposal ended", "transfer finished", or "session closed".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;send_data(state)&lt;/code&gt; — answers a narrower question: may this side put file bytes on the wire right now?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The state layer has no sockets, no files, no TLS, no UI. Its tests can therefore walk an initiator and a responder through an entire session — two plain state values, no network — and assert every transition.&lt;/p&gt;

&lt;p&gt;The rules are the specification in executable form: the initiator sends the first &lt;code&gt;hello&lt;/code&gt;; pairing records alternate and only the correct party may send each one; &lt;code&gt;DATA&lt;/code&gt; is valid only while receiving inside an un-ended file; &lt;code&gt;file_end&lt;/code&gt; and &lt;code&gt;file_result&lt;/code&gt; indices must match the tracked file; any transition not explicitly allowed is a terminal &lt;code&gt;WrongState&lt;/code&gt; error. Wrong state is not recoverable: a peer that violates the sequence has made the rest of the stream unreliable.&lt;/p&gt;

&lt;p&gt;The state machine also encodes the version 1 simultaneous-proposal rule. Two peers can both propose while idle, and there is no request ID, so the pairing initiator's request wins. A responder with a pending proposal withdraws and re-queues it and sets one &lt;code&gt;collision_pending&lt;/code&gt; marker. The protocol layer then consumes exactly one stale &lt;code&gt;transfer_response(busy)&lt;/code&gt; for that withdrawn proposal — in any phase of the winning transfer — and clears the marker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The responder consumes exactly one stale busy response for its&lt;/span&gt;
&lt;span class="c1"&gt;// withdrawn proposal, in any phase of the winning transfer.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.collision_pending&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nn"&gt;Inbound&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Control&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;TransferResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;inbound&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nd"&gt;matches!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="py"&gt;.reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;TransferRejection&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Busy&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.collision_pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&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;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&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;That rule is a direct consequence of the "no request IDs" decision. Request IDs and a transaction layer would handle races more uniformly, but they would add state and failure modes. The chosen design keeps the state space small and pays with a slightly awkward collision rule the tests exercise explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  One writer, two bounded queues
&lt;/h2&gt;

&lt;p&gt;Reading is a loop. Writing is a task.&lt;/p&gt;

&lt;p&gt;Concurrent writers could interleave their bytes and corrupt the stream, so there is exactly one writer task per connection, fed by two &lt;a href="https://docs.rs/tokio/latest/tokio/sync/mpsc/fn.channel.html" rel="noopener noreferrer"&gt;bounded queues&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    S[session owner] --&amp;gt;|controls, capacity 64| CQ[control queue]
    S --&amp;gt;|DATA frames, capacity 8| DQ[DATA queue]
    CQ --&amp;gt; W[single writer task]
    DQ --&amp;gt; W
    W --&amp;gt;|write_all + flush per frame| TLS[TLS stream]
    B[barrier] --&amp;gt;|queued behind the frames it observes| CQ&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;The writer resolves frames in a &lt;a href="https://docs.rs/tokio/latest/tokio/macro.select.html" rel="noopener noreferrer"&gt;&lt;code&gt;select!&lt;/code&gt;&lt;/a&gt; loop with a &lt;code&gt;biased&lt;/code&gt; branch order that checks DATA first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;select!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;biased&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="nf"&gt;.recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&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;break&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;write_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;controls&lt;/span&gt;&lt;span class="nf"&gt;.recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;WriterItem&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;write_frame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;WriterItem&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Barrier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acknowledge&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acknowledge&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(());&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;stopped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="nf"&gt;.changed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;=&amp;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;stopped&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="nf"&gt;.borrow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ordering guarantee is precise, and the module documents both halves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A control queued &lt;strong&gt;after&lt;/strong&gt; a &lt;code&gt;DATA&lt;/code&gt; frame cannot overtake it. That makes &lt;code&gt;file_end&lt;/code&gt; safe: the session queues the trailing &lt;code&gt;DATA&lt;/code&gt; chunks, then &lt;code&gt;file_end&lt;/code&gt;, and the writer prefers DATA while any is queued, so &lt;code&gt;file_end&lt;/code&gt; always follows the last chunk of its file.&lt;/li&gt;
&lt;li&gt;A control queued &lt;strong&gt;before&lt;/strong&gt; a &lt;code&gt;DATA&lt;/code&gt; frame may still be overtaken, because the writer prefers DATA. Callers must not rely on cross-queue send order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second half is easy to get wrong by assuming one global FIFO across both queues. Naming the limitation and testing the first half was worth more than inventing an ordering guarantee the implementation does not need.&lt;/p&gt;

&lt;p&gt;Two mechanisms make writing robust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flush per frame.&lt;/strong&gt; Every frame is written with &lt;code&gt;write_all&lt;/code&gt;, flushed (&lt;a href="https://docs.rs/tokio/latest/tokio/io/trait.AsyncWriteExt.html#method.flush" rel="noopener noreferrer"&gt;&lt;code&gt;flush&lt;/code&gt;&lt;/a&gt;), and the whole write-plus-flush is wrapped in a 30-second deadline. A peer that stops reading applies backpressure; one that stops &lt;em&gt;forever&lt;/em&gt; surfaces as a write deadline instead of a task that hangs until shutdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A flush barrier.&lt;/strong&gt; &lt;code&gt;send_control_flushed&lt;/code&gt; queues the frame and a barrier in the same control queue and returns only when the writer reaches the barrier — meaning every earlier frame has been written and flushed. This exists for one requirement: the pairing responder must not treat the session as authorized until its final confirmation is actually on the wire. It is the kind of ordering requirement that is invisible until it breaks a security property.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shutdown has two flavors. &lt;code&gt;Outbound::close&lt;/code&gt; signals the writer, waits for the task, lets queued frames finish, and shuts the write half down so the peer sees one clean EOF. Dropping the &lt;code&gt;Outbound&lt;/code&gt; aborts the writer, which can truncate an in-flight frame — the peer sees &lt;code&gt;Truncated&lt;/code&gt; instead. Both paths are tested: how a connection ends is part of the protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backpressure, end to end
&lt;/h2&gt;

&lt;p&gt;Streaming to a peer whose network is slower than the disk is a classic way to turn a small program into a memory-hungry one: read fast, buffer without limit, watch RSS climb. Lanweave's answer: every handoff in the pipeline is a bounded channel, so a slow consumer blocks a fast producer instead of accumulating a queue.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Capacity&lt;/th&gt;
&lt;th&gt;What it protects&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App events&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;The event loop's inbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App effects&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;Work dispatched to network services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discovery events&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;mDNS updates into the app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session commands&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;UI actions into the session owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accepted sockets&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Listener to session owner (connection floods)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-connection commands&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Local decisions into one connection task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outbound control frames&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;Control traffic to the writer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Outbound DATA frames&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;File data to the writer (one frame ≤ 1 MiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File reader to sender&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;64 KiB chunks from disk to the session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The DATA queue matters most for memory: the writer path holds at most eight queued 1 MiB frames plus the one being written, the file-reader channel holds at most four 64 KiB chunks, and the reader owns one 64 KiB buffer. A 40 GB file does not produce a 40 GB buffer; it produces a steady state where the disk read task sleeps whenever the network cannot keep up. Backpressure is not a feature added later — it is what makes the memory bound true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming files without loading them
&lt;/h2&gt;

&lt;p&gt;Both ends of a transfer run the same bounded pattern: read a chunk, hash it, hand it to the network; receive a chunk, write it to a temporary file, hash it.&lt;/p&gt;

&lt;p&gt;On the sending side, the source is re-checked immediately before reading — a file that is no longer a regular file of the reviewed size is refused rather than streamed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&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;fn&lt;/span&gt; &lt;span class="nf"&gt;send_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Sender&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;SourceError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;symlink_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map_err&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="nn"&gt;SourceError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Changed&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;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="nf"&gt;.file_type&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.is_file&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;size&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SourceError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Changed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;File&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.map_err&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="nn"&gt;SourceError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Changed&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;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;hasher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Sha256&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// 65_536&lt;/span&gt;

    &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="nf"&gt;.read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.map_err&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="nn"&gt;SourceError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Io&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;if&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="o"&gt;==&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;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;hasher&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
        &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;copy_from_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
            &lt;span class="k"&gt;.await&lt;/span&gt;
            &lt;span class="nf"&gt;.map_err&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="nn"&gt;SourceError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Io&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="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hasher&lt;/span&gt;&lt;span class="nf"&gt;.finalize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.into&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;&lt;code&gt;read_to_end&lt;/code&gt; would have been three lines. It would also make peak memory proportional to file size, contradicting the bounded-memory constraint. Streaming costs something real: an error can appear mid-file, after bytes have been sent. The protocol handles that by scoping failure to the current transfer and making the consequences explicit.&lt;/p&gt;

&lt;p&gt;On the receiving side, each chunk is checked against the declared size before it is written, and the file is published only after the accumulated digest matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&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;fn&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&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;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;FileFailure&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.written&lt;/span&gt;&lt;span class="nf"&gt;.saturating_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.expected&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FileFailure&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SizeMismatch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// excess data is rejected&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.temp&lt;/span&gt;&lt;span class="nf"&gt;.write_all&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;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.map_err&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="nn"&gt;FileFailure&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;WriteFailed&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;self&lt;/span&gt;&lt;span class="py"&gt;.hasher&lt;/span&gt;&lt;span class="nf"&gt;.update&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;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.written&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;crate&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;fn&lt;/span&gt; &lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;FileFailure&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.written&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.expected&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FileFailure&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SizeMismatch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// short or long&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.hasher&lt;/span&gt;&lt;span class="nf"&gt;.finalize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.into&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;actual&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;digest&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;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;FileFailure&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HashMismatch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.temp&lt;/span&gt;&lt;span class="nf"&gt;.finalize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;StorageError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DestinationExists&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;FileFailure&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;DestinationExists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;FileFailure&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;WriteFailed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The receiver writes to a restrictive temporary file and never under the final name until verification succeeds; dropping the unfinished file removes the partial. Publication is a no-replace operation (&lt;a href="https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist_noclobber" rel="noopener noreferrer"&gt;&lt;code&gt;persist_noclobber&lt;/code&gt;&lt;/a&gt;), so a file that appears after preflight is never overwritten. The sender waits for each &lt;code&gt;file_result&lt;/code&gt; before starting the next file, so "verified" is always a peer's statement about a complete, published file, not a local hope based on a socket write.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant R as File reader task
    participant S as Session owner
    participant W as Writer task
    participant P as Peer
    R-&amp;gt;&amp;gt;S: 64 KiB chunk (bounded channel, capacity 4)
    S-&amp;gt;&amp;gt;W: DATA frame (bounded queue, capacity 8)
    W-&amp;gt;&amp;gt;P: write_all + flush per frame
    R--&amp;gt;&amp;gt;S: reader finished, returns SHA-256
    S-&amp;gt;&amp;gt;W: file_end(index, sha256)
    W-&amp;gt;&amp;gt;P: file_end
    P--&amp;gt;&amp;gt;S: file_result(verified)
    Note over S: only now does the next file start&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Cancellation, timeouts, and cleanup
&lt;/h2&gt;

&lt;p&gt;The send and receive loops are &lt;code&gt;select!&lt;/code&gt; expressions over three events: an inbound read, a local UI command, and (on the sending side) the next chunk from the file reader. Cancellation is not a special path bolted on top; it is one of the arms the loop already waits on.&lt;/p&gt;

&lt;p&gt;Cancellation semantics follow the protocol's failure scopes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before &lt;code&gt;ready&lt;/code&gt;&lt;/strong&gt;, nothing has been sent, so cancellation ends the proposal and the session stays authorized. The reviewed file list is kept locally so the user can try again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After &lt;code&gt;ready&lt;/code&gt;&lt;/strong&gt;, file bytes may be in flight. The canceller sends &lt;code&gt;transfer_cancel&lt;/code&gt;, the partial file is removed, and the session closes — in-flight &lt;code&gt;DATA&lt;/code&gt; cannot be attributed to a later transfer without transfer IDs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a progress deadline&lt;/strong&gt;, a stalled or silent peer is reported with a terminal &lt;code&gt;error(timeout)&lt;/code&gt; when the connection is still writable, and the session closes after cleanup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Receiver cleanup is tied to ownership, not a manual "run this on failure" step: the partial file is an object, and dropping it without finalizing removes the file. Cleanup therefore holds on every early return, not just the ones a developer remembered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the protocol
&lt;/h2&gt;

&lt;p&gt;Protocol input is a byte string, which makes the set of interesting inputs more enumerable than in UI code. The test suite uses that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every split point.&lt;/strong&gt; A wire image of several frames is decoded with the input divided at every byte position; all splits must produce the identical frame sequence, covering header splits, body splits, and coalesced frames in one sweep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Byte-by-byte feeding.&lt;/strong&gt; The same images are fed one byte at a time, catching parsers that only handle boundaries one level deep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Golden vectors.&lt;/strong&gt; Every control message has an exact expected JSON encoding, including canonical base64url pairing values and 64-character lowercase digests, so a specification and an implementation can be compared directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Malformed input.&lt;/strong&gt; Bad magic, bad version, unknown kind, wrong header length, oversized body, and empty DATA form a table of expected errors, all asserted to fail before any body allocation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boundedness under pressure.&lt;/strong&gt; A gated writer that never accepts bytes proves a full DATA queue blocks the sender instead of growing, and that the blocked send completes when the writer drains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real connections.&lt;/strong&gt; The frame and protocol layers also run through actual TLS connections and an end-to-end loopback session: real handshake, real pairing, real files on disk, both directions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One fuzz target complements the deterministic tests: &lt;code&gt;frame_codec&lt;/code&gt; feeds arbitrary bytes to the decoder and requires that it never panics and never allocates for invalid or oversized headers. It is built with &lt;code&gt;cargo fuzz&lt;/code&gt; (&lt;a href="https://rust-fuzz.github.io/book/cargo-fuzz.html" rel="noopener noreferrer"&gt;cargo-fuzz book&lt;/a&gt;), lives in a separate crate excluded from CI, and requires nightly Rust. Its whole body is small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;fuzz_target!&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;]|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;BytesMut&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&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="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_frame&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;lanweave&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fuzzing&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library exposes the private codec to the fuzzer through a &lt;code&gt;fuzz&lt;/code&gt; feature that compiles a re-export shim, so normal builds keep a private API. Fuzzing covers only the frame decoder; property tests for strict JSON, the state machine, pasted paths, and destination-name mapping are planned work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing untrusted bytes: the security implications
&lt;/h2&gt;

&lt;p&gt;A protocol parser is attack surface. The most dangerous designs are generous: they accept what they do not understand, allocate from lengths before validating, keep owned copies of arbitrary size, and return errors that quote the input. Lanweave moves the other way at every point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Validate before allocating, and bound the schema, not just the transport.&lt;/strong&gt; The frame length is checked against a per-kind maximum using only the 12-byte header — a hostile 4 GiB claim costs 12 bytes to reject — and body limits are per message type, with arrays, strings, and integers capped at 2^53 − 1 so checked totals cannot exceed what the domain can represent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject ambiguity.&lt;/strong&gt; Duplicate fields, unknown fields, out-of-set string values, and impossible state transitions are errors, not warnings. An implementation that accepts more than the specification allows is an implementation whose behaviour is defined by accident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never reflect peer data.&lt;/strong&gt; Parse errors contain only static schema names, wire errors use closed codes, and untrusted names are escaped before they reach a terminal. A malformed message is data to be judged, not text to be repeated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Close on malformed framing.&lt;/strong&gt; Once the frame layer has seen a violation, the rest of the stream is untrustworthy. There is no half-open recovery state, which removes a whole category of desynchronization bugs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a proof of safety. Fuzzing shows the absence of crashes it happened to observe; tests show conformance to a draft; parser bugs can still exist in carefully read code. What the design provides is a small, explicit surface with limits stated in one document and enforced in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Boundaries are the protocol's first job.&lt;/strong&gt; The frame header, per-kind limits, and the &lt;code&gt;Ok(None)&lt;/code&gt;-means-incomplete contract make everything above them possible. Deciding what a &lt;code&gt;DATA&lt;/code&gt; frame does &lt;em&gt;not&lt;/em&gt; know — no file index, no offset — pushed the streaming questions to the front, and the answer (the connection state knows) simplified both layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the pure layers pure.&lt;/strong&gt; The state machine has no I/O, the parser no sockets, the frame codec no message knowledge. Each is cheap to test exhaustively, and when something breaks, "which layer is wrong" is usually obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Say exactly what the writer guarantees.&lt;/strong&gt; Single-writer serialization is easy; cross-queue ordering is subtle. Documenting that &lt;code&gt;file_end&lt;/code&gt; cannot overtake its DATA but that a control queued before DATA may be overtaken beats a vague "frames are ordered" claim — and tells the next person which invariants not to rely on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backpressure is architecture, not tuning.&lt;/strong&gt; Bounded channels everywhere make the memory ceiling a property of the topology, not a benchmark. The cost is that producers must be written to await — exactly the discipline the design wants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test the seams.&lt;/strong&gt; Split-point decoding, coalesced reads, truncated closes, full queues, and real TLS connections test where assumptions meet reality. Most protocol bugs live at the seams, not in the middle of functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;TCP gives reliability, not messages; everything above it in Lanweave is built on that distinction. The frame codec turns bytes into bounded, validated frames; the message layer turns frames into unambiguous typed controls; the state machine decides whether those controls are allowed right now; the writer serialises output under an ordering contract it states precisely; the transfer engine streams file data through bounded channels with hashes and deadlines attached. The hard parts — ordering, memory bounds, failure scopes, hostile input — are visible in the code and covered by tests aimed at the seams rather than the happy path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source and further reading
&lt;/h2&gt;

&lt;p&gt;The implementation is at &lt;a href="https://github.com/etim66/lanweave" rel="noopener noreferrer"&gt;github.com/etim66/lanweave&lt;/a&gt;. The frame header and message schemas are documented in &lt;code&gt;docs/MESSAGE_FORMAT.md&lt;/code&gt;, the protocol rules in &lt;code&gt;docs/PROTOCOL.md&lt;/code&gt;, the state machines in &lt;code&gt;docs/STATE_MACHINES.md&lt;/code&gt;, and the transport profile in &lt;code&gt;docs/TRANSPORT.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by the developer of Lanweave. The project is open source and the design documents are part of it; if you find a protocol flaw or a case the framing tests miss, a reproducible example is the most useful contribution.&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;RFC 9293 — &lt;em&gt;Transmission Control Protocol (TCP)&lt;/em&gt;: &lt;a href="https://www.rfc-editor.org/rfc/rfc9293" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc9293&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RFC 8446 — &lt;em&gt;The Transport Layer Security (TLS) Protocol Version 1.3&lt;/em&gt; (record layer, §5): &lt;a href="https://www.rfc-editor.org/rfc/rfc8446#section-5" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc8446#section-5&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RFC 8259 — &lt;em&gt;The JavaScript Object Notation (JSON) Data Interchange Format&lt;/em&gt;: &lt;a href="https://www.rfc-editor.org/rfc/rfc8259" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc8259&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RFC 4648 §5 — &lt;em&gt;Base64url encoding&lt;/em&gt;: &lt;a href="https://www.rfc-editor.org/rfc/rfc4648#section-5" rel="noopener noreferrer"&gt;https://www.rfc-editor.org/rfc/rfc4648#section-5&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tokio, &lt;code&gt;select!&lt;/code&gt; macro: &lt;a href="https://docs.rs/tokio/latest/tokio/macro.select.html" rel="noopener noreferrer"&gt;https://docs.rs/tokio/latest/tokio/macro.select.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tokio, bounded &lt;code&gt;mpsc&lt;/code&gt; channel: &lt;a href="https://docs.rs/tokio/latest/tokio/sync/mpsc/fn.channel.html" rel="noopener noreferrer"&gt;https://docs.rs/tokio/latest/tokio/sync/mpsc/fn.channel.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tokio, &lt;code&gt;AsyncReadExt::read_buf&lt;/code&gt;: &lt;a href="https://docs.rs/tokio/latest/tokio/io/trait.AsyncReadExt.html#method.read_buf" rel="noopener noreferrer"&gt;https://docs.rs/tokio/latest/tokio/io/trait.AsyncReadExt.html#method.read_buf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tokio, &lt;code&gt;AsyncWriteExt::flush&lt;/code&gt;: &lt;a href="https://docs.rs/tokio/latest/tokio/io/trait.AsyncWriteExt.html#method.flush" rel="noopener noreferrer"&gt;https://docs.rs/tokio/latest/tokio/io/trait.AsyncWriteExt.html#method.flush&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bytes&lt;/code&gt;, &lt;code&gt;BytesMut::split_to&lt;/code&gt; and &lt;code&gt;BytesMut::freeze&lt;/code&gt;: &lt;a href="https://docs.rs/bytes/latest/bytes/struct.BytesMut.html" rel="noopener noreferrer"&gt;https://docs.rs/bytes/latest/bytes/struct.BytesMut.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;serde_json::value::RawValue&lt;/code&gt;: &lt;a href="https://docs.rs/serde_json/latest/serde_json/value/struct.RawValue.html" rel="noopener noreferrer"&gt;https://docs.rs/serde_json/latest/serde_json/value/struct.RawValue.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;cargo-fuzz&lt;/code&gt; book: &lt;a href="https://rust-fuzz.github.io/book/cargo-fuzz.html" rel="noopener noreferrer"&gt;https://rust-fuzz.github.io/book/cargo-fuzz.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tempfile::NamedTempFile::persist_noclobber&lt;/code&gt;: &lt;a href="https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist_noclobber" rel="noopener noreferrer"&gt;https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist_noclobber&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rust</category>
      <category>networking</category>
      <category>security</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
