<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>networking · wleeaf field notes</title><link>https://articles.wleeaf.dev/tags/networking/</link><description>Hands-on guides and build logs from the wleeaf studio: self-hosting, Linux, networking, Docker, the command line and reverse engineering.</description><language>en-us</language><lastBuildDate>Mon, 06 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://articles.wleeaf.dev/tags/networking/feed.xml" rel="self" type="application/rss+xml"/><item><title>DNS For People Who Just Want Their Site to Work</title><link>https://articles.wleeaf.dev/posts/dns-explained-for-developers/</link><pubDate>Mon, 06 Apr 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/dns-explained-for-developers/</guid><description>A, AAAA, CNAME, MX, TXT, NS, SOA. What they do, when to use them, and how to debug when things break.</description><category>dns</category><category>networking</category><category>web</category><content:encoded><![CDATA[<p>DNS is the thing that turns <code>example.com</code> into <code>93.184.216.34</code>. It sounds simple until you need to actually configure it. Then you are staring at a dashboard full of record types, TTL values, and propagation delays wondering why your site still points to the old server.</p>
<h2 id="the-record-types-that-matter">The Record Types That Matter<a class="anchor" href="#the-record-types-that-matter" aria-label="Link to “The Record Types That Matter”">#</a></h2>
<h3 id="a-and-aaaa">A and AAAA<a class="anchor" href="#a-and-aaaa" aria-label="Link to “A and AAAA”">#</a></h3>
<p>The most basic. Maps a domain to an IP address:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>example.dev.    A      203.0.113.50
example.dev.    AAAA   2001:db8::1</code></pre>
</div>
<p>A is IPv4. AAAA is IPv6. You want both if your server supports v6.</p>
<h3 id="cname">CNAME<a class="anchor" href="#cname" aria-label="Link to “CNAME”">#</a></h3>
<p>An alias. Points one name to another:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>www.example.dev.    CNAME    example.dev.</code></pre>
</div>
<p>This says &ldquo;www is the same as the bare domain.&rdquo; The DNS resolver follows the chain and ends up at the A record for <code>example.dev</code>.</p>
<p>The critical rule: <strong>you cannot have a CNAME alongside other records for the same name.</strong> This means the zone apex (bare domain) usually cannot be a CNAME. Use an A record there instead.</p>
<h3 id="mx">MX<a class="anchor" href="#mx" aria-label="Link to “MX”">#</a></h3>
<p>Where to deliver email:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>example.dev.    MX    10 mail.example.dev.</code></pre>
</div>
<p>The number is priority. Lower means preferred. If you have a backup mail server:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>example.dev.    MX    10 mail.example.dev.
example.dev.    MX    20 backup-mail.example.dev.</code></pre>
</div>
<p>Senders try priority 10 first, fall back to 20 if it is down.</p>
<h3 id="txt">TXT<a class="anchor" href="#txt" aria-label="Link to “TXT”">#</a></h3>
<p>Arbitrary text. Used for verification and email authentication:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>example.dev.    TXT    &#34;v=spf1 a mx ip4:203.0.113.50 ~all&#34;</code></pre>
</div>
<p>SPF tells receiving mail servers which IPs are allowed to send email for your domain. DKIM and DMARC are also TXT records. Google and other services use TXT for domain ownership verification too.</p>
<h3 id="ns">NS<a class="anchor" href="#ns" aria-label="Link to “NS”">#</a></h3>
<p>Nameserver delegation. Usually set by your registrar:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>example.dev.    NS    ada.ns.cloudflare.com.
example.dev.    NS    greg.ns.cloudflare.com.</code></pre>
</div>
<p>This tells the internet &ldquo;Cloudflare knows the DNS records for example.dev.&rdquo; You rarely touch these directly.</p>
<h2 id="debugging-with-dig">Debugging With dig<a class="anchor" href="#debugging-with-dig" aria-label="Link to “Debugging With dig”">#</a></h2>
<p><code>dig</code> is the standard DNS debugging tool:</p>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Basic lookup</span>
</span></span><span class="line"><span class="cl">dig example.dev
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Specific record type</span>
</span></span><span class="line"><span class="cl">dig MX example.dev
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Short output</span>
</span></span><span class="line"><span class="cl">dig +short example.dev
</span></span><span class="line"><span class="cl"><span class="c1"># 203.0.113.50</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Use a specific DNS server</span>
</span></span><span class="line"><span class="cl">dig @8.8.8.8 example.dev
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Trace the full resolution path</span>
</span></span><span class="line"><span class="cl">dig +trace example.dev</span></span></code></pre></div>
</div>
<p>The <code>+trace</code> option follows the entire DNS chain from the root servers down. This is invaluable when debugging propagation issues.</p>
<h2 id="ttl">TTL<a class="anchor" href="#ttl" aria-label="Link to “TTL”">#</a></h2>
<p>Time To Live. How long DNS resolvers cache a record:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>example.dev.    300    IN    A    203.0.113.50</code></pre>
</div>
<p>300 means 5 minutes. After that, resolvers ask again. Lower TTL means faster propagation when you change records, but more DNS queries hitting your nameservers.</p>
<p>Before a migration:</p>
<ol>
<li>Lower TTL to 60 seconds, 24 hours ahead</li>
<li>Wait for the old TTL to expire</li>
<li>Change the record</li>
<li>Verify</li>
<li>Raise TTL back to 3600 or higher</li>
</ol>
<p>This way, when you change the IP, everyone picks up the new address within a minute.</p>
<h2 id="common-patterns">Common Patterns<a class="anchor" href="#common-patterns" aria-label="Link to “Common Patterns”">#</a></h2>
<h3 id="subdomain-to-a-different-server">Subdomain to a different server<a class="anchor" href="#subdomain-to-a-different-server" aria-label="Link to “Subdomain to a different server”">#</a></h3>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>api.example.com.    A    203.0.113.50</code></pre>
</div>
<h3 id="wildcard">Wildcard<a class="anchor" href="#wildcard" aria-label="Link to “Wildcard”">#</a></h3>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>*.example.com.    A    93.184.216.34</code></pre>
</div>
<p>Matches any subdomain that does not have a more specific record. Useful for catch-all routing.</p>
<h3 id="email-setup">Email setup<a class="anchor" href="#email-setup" aria-label="Link to “Email setup”">#</a></h3>
<p>Minimum viable email DNS:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>example.com.         MX   10 mail.example.com.
mail.example.com.    A    93.184.216.34
example.com.         TXT  &#34;v=spf1 a mx ~all&#34;
_dmarc.example.com.  TXT  &#34;v=DMARC1; p=quarantine&#34;
sel._domainkey.example.com.  TXT  &#34;v=DKIM1; k=rsa; p=MIIBIj...&#34;</code></pre>
</div>
<h3 id="cloudflare-proxy-vs-dns-only">Cloudflare proxy vs DNS-only<a class="anchor" href="#cloudflare-proxy-vs-dns-only" aria-label="Link to “Cloudflare proxy vs DNS-only”">#</a></h3>
<p>If you use Cloudflare:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code># Proxied (orange cloud) - traffic goes through Cloudflare CDN
example.dev.    A    203.0.113.50    (proxied)

# DNS-only (gray cloud) - direct connection to your server
mail.example.dev.    A    203.0.113.50    (DNS-only)</code></pre>
</div>
<p>Proxied records hide your real IP and add DDoS protection. But they break non-HTTP protocols. Mail servers, game servers, and anything that is not HTTP/HTTPS needs DNS-only.</p>
<h2 id="why-is-my-dns-not-working">Why Is My DNS Not Working?<a class="anchor" href="#why-is-my-dns-not-working" aria-label="Link to “Why Is My DNS Not Working?”">#</a></h2>
<p>In order of likelihood:</p>
<ol>
<li>
<p><strong>Propagation delay.</strong> You changed the record 2 minutes ago. Wait for the TTL to expire. Check with <code>dig @8.8.8.8</code> to see what Google&rsquo;s DNS returns.</p>
</li>
<li>
<p><strong>Browser cache.</strong> Chrome caches DNS aggressively. Try <code>chrome://net-internals/#dns</code> and clear it, or just use a different browser.</p>
</li>
<li>
<p><strong>Wrong record type.</strong> You added a CNAME where you needed an A record, or vice versa.</p>
</li>
<li>
<p><strong>Cloudflare proxy interfering.</strong> Your service needs direct access but the record is proxied. Switch to DNS-only.</p>
</li>
<li>
<p><strong>Missing trailing dot.</strong> In some DNS configurations, <code>mail.example.com</code> and <code>mail.example.com.</code> are different. The dot makes it fully qualified. Most providers handle this for you, but raw zone files need it.</p>
</li>
</ol>
<div class="code">
<div class="code-head"><span class="code-lang">bash</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Quick sanity check script</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> record in A AAAA MX TXT CNAME<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;=== </span><span class="nv">$record</span><span class="s2"> ===&#34;</span>
</span></span><span class="line"><span class="cl">  dig +short <span class="nv">$record</span> example.com
</span></span><span class="line"><span class="cl"><span class="k">done</span></span></span></code></pre></div>
</div>
<p>DNS is not complicated once you understand the record types. The frustrating part is always the caching and propagation delays. Lower your TTLs before making changes, use <code>dig</code> to verify, and be patient.</p>
]]></content:encoded></item><item><title>Building a WebSocket Server From Scratch in Node.js</title><link>https://articles.wleeaf.dev/posts/websocket-from-scratch/</link><pubDate>Sun, 05 Apr 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/websocket-from-scratch/</guid><description>The WebSocket protocol is simple enough to implement by hand. Here is what happens under the hood.</description><category>node.js</category><category>websocket</category><category>networking</category><content:encoded><![CDATA[<p>Libraries like Socket.IO and ws abstract away the WebSocket protocol. That is usually fine. But understanding what happens at the protocol level makes debugging easier and helps you make better architecture decisions.</p>
<p>Let us build a WebSocket server from scratch using only the Node.js standard library.</p>
<h2 id="the-handshake">The Handshake<a class="anchor" href="#the-handshake" aria-label="Link to “The Handshake”">#</a></h2>
<p>WebSocket starts as an HTTP upgrade request. The client sends:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>GET / HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13</code></pre>
</div>
<p>The server responds:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=</code></pre>
</div>
<p>The <code>Sec-WebSocket-Accept</code> value is computed from the client&rsquo;s key:</p>
<div class="code">
<div class="code-head"><span class="code-lang">javascript</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">crypto</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;crypto&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">computeAcceptKey</span><span class="p">(</span><span class="nx">clientKey</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">MAGIC</span> <span class="o">=</span> <span class="s1">&#39;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">crypto</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">createHash</span><span class="p">(</span><span class="s1">&#39;sha1&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">clientKey</span> <span class="o">+</span> <span class="nx">MAGIC</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">digest</span><span class="p">(</span><span class="s1">&#39;base64&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>The magic string is literally part of the RFC. Every WebSocket implementation uses it.</p>
<h2 id="the-server">The Server<a class="anchor" href="#the-server" aria-label="Link to “The Server”">#</a></h2>
<div class="code">
<div class="code-head"><span class="code-lang">javascript</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">http</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;http&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">crypto</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;crypto&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">MAGIC</span> <span class="o">=</span> <span class="s1">&#39;258EAFA5-E914-47DA-95CA-C5AB0DC85B11&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">createServer</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">clients</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Set</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">server</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;upgrade&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">socket</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">clientKey</span> <span class="o">=</span> <span class="nx">req</span><span class="p">.</span><span class="nx">headers</span><span class="p">[</span><span class="s1">&#39;sec-websocket-key&#39;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">clientKey</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">socket</span><span class="p">.</span><span class="nx">destroy</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">acceptKey</span> <span class="o">=</span> <span class="nx">crypto</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">createHash</span><span class="p">(</span><span class="s1">&#39;sha1&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">update</span><span class="p">(</span><span class="nx">clientKey</span> <span class="o">+</span> <span class="nx">MAGIC</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="nx">digest</span><span class="p">(</span><span class="s1">&#39;base64&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">socket</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;HTTP/1.1 101 Switching Protocols\r\n&#39;</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;Upgrade: websocket\r\n&#39;</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;Connection: Upgrade\r\n&#39;</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="sb">`Sec-WebSocket-Accept: </span><span class="si">${</span><span class="nx">acceptKey</span><span class="si">}</span><span class="sb">\r\n`</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;\r\n&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">clients</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">socket</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Client connected (</span><span class="si">${</span><span class="nx">clients</span><span class="p">.</span><span class="nx">size</span><span class="si">}</span><span class="sb"> total)`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">socket</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;data&#39;</span><span class="p">,</span> <span class="p">(</span><span class="nx">buffer</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">message</span> <span class="o">=</span> <span class="nx">decodeFrame</span><span class="p">(</span><span class="nx">buffer</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">message</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Received:&#39;</span><span class="p">,</span> <span class="nx">message</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Echo to all clients
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">for</span> <span class="p">(</span><span class="kr">const</span> <span class="nx">client</span> <span class="k">of</span> <span class="nx">clients</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">client</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">encodeFrame</span><span class="p">(</span><span class="nx">message</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">socket</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;close&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">clients</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="nx">socket</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="sb">`Client disconnected (</span><span class="si">${</span><span class="nx">clients</span><span class="p">.</span><span class="nx">size</span><span class="si">}</span><span class="sb"> total)`</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nx">socket</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">&#39;error&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">clients</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="nx">socket</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nx">server</span><span class="p">.</span><span class="nx">listen</span><span class="p">(</span><span class="mi">8080</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;WebSocket server on ws://localhost:8080&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div>
</div>
<h2 id="frame-decoding">Frame Decoding<a class="anchor" href="#frame-decoding" aria-label="Link to “Frame Decoding”">#</a></h2>
<p>WebSocket messages are wrapped in frames. The frame format:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code> 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |            (16/64)            |
|N|V|V|V|       |S|             |                               |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Masking key (if MASK set)                                 |
+-------------------------------+-------------------------------+
|     Payload Data                                              |
+---------------------------------------------------------------+</code></pre>
</div>
<p>Client-to-server frames are always masked. Server-to-client frames are never masked. Here is the decoder:</p>
<div class="code">
<div class="code-head"><span class="code-lang">javascript</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">decodeFrame</span><span class="p">(</span><span class="nx">buffer</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">firstByte</span> <span class="o">=</span> <span class="nx">buffer</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">opcode</span> <span class="o">=</span> <span class="nx">firstByte</span> <span class="o">&amp;</span> <span class="mh">0x0f</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">// 0x1 = text, 0x8 = close, 0x9 = ping, 0xA = pong
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>  <span class="k">if</span> <span class="p">(</span><span class="nx">opcode</span> <span class="o">===</span> <span class="mh">0x8</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span> <span class="c1">// close frame
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">secondByte</span> <span class="o">=</span> <span class="nx">buffer</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">isMasked</span> <span class="o">=</span> <span class="p">(</span><span class="nx">secondByte</span> <span class="o">&amp;</span> <span class="mh">0x80</span><span class="p">)</span> <span class="o">!==</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kd">let</span> <span class="nx">payloadLength</span> <span class="o">=</span> <span class="nx">secondByte</span> <span class="o">&amp;</span> <span class="mh">0x7f</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kd">let</span> <span class="nx">offset</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">payloadLength</span> <span class="o">===</span> <span class="mi">126</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">payloadLength</span> <span class="o">=</span> <span class="nx">buffer</span><span class="p">.</span><span class="nx">readUInt16BE</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">offset</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">payloadLength</span> <span class="o">===</span> <span class="mi">127</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">payloadLength</span> <span class="o">=</span> <span class="nb">Number</span><span class="p">(</span><span class="nx">buffer</span><span class="p">.</span><span class="nx">readBigUInt64BE</span><span class="p">(</span><span class="mi">2</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">    <span class="nx">offset</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kd">let</span> <span class="nx">maskingKey</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isMasked</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">maskingKey</span> <span class="o">=</span> <span class="nx">buffer</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">offset</span><span class="p">,</span> <span class="nx">offset</span> <span class="o">+</span> <span class="mi">4</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">offset</span> <span class="o">+=</span> <span class="mi">4</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="nx">buffer</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">offset</span><span class="p">,</span> <span class="nx">offset</span> <span class="o">+</span> <span class="nx">payloadLength</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">isMasked</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">payload</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">^=</span> <span class="nx">maskingKey</span><span class="p">[</span><span class="nx">i</span> <span class="o">%</span> <span class="mi">4</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">toString</span><span class="p">(</span><span class="s1">&#39;utf-8&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<h2 id="frame-encoding">Frame Encoding<a class="anchor" href="#frame-encoding" aria-label="Link to “Frame Encoding”">#</a></h2>
<p>Sending from server to client is simpler because we do not mask:</p>
<div class="code">
<div class="code-head"><span class="code-lang">javascript</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">encodeFrame</span><span class="p">(</span><span class="nx">message</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="nx">Buffer</span><span class="p">.</span><span class="nx">from</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="s1">&#39;utf-8&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">length</span> <span class="o">=</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kd">let</span> <span class="nx">header</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="mi">126</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">header</span> <span class="o">=</span> <span class="nx">Buffer</span><span class="p">.</span><span class="nx">alloc</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">header</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x81</span><span class="p">;</span> <span class="c1">// FIN + text opcode
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nx">header</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">length</span> <span class="o">&lt;</span> <span class="mi">65536</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">header</span> <span class="o">=</span> <span class="nx">Buffer</span><span class="p">.</span><span class="nx">alloc</span><span class="p">(</span><span class="mi">4</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">header</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x81</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">header</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="mi">126</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">header</span><span class="p">.</span><span class="nx">writeUInt16BE</span><span class="p">(</span><span class="nx">length</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">header</span> <span class="o">=</span> <span class="nx">Buffer</span><span class="p">.</span><span class="nx">alloc</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nx">header</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x81</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">header</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="mi">127</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nx">header</span><span class="p">.</span><span class="nx">writeBigUInt64BE</span><span class="p">(</span><span class="nx">BigInt</span><span class="p">(</span><span class="nx">length</span><span class="p">),</span> <span class="mi">2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nx">Buffer</span><span class="p">.</span><span class="nx">concat</span><span class="p">([</span><span class="nx">header</span><span class="p">,</span> <span class="nx">payload</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<h2 id="testing-it">Testing It<a class="anchor" href="#testing-it" aria-label="Link to “Testing It”">#</a></h2>
<p>Open a browser console:</p>
<div class="code">
<div class="code-head"><span class="code-lang">javascript</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">ws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WebSocket</span><span class="p">(</span><span class="s1">&#39;ws://localhost:8080&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nx">ws</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s1">&#39;Got:&#39;</span><span class="p">,</span> <span class="nx">e</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nx">ws</span><span class="p">.</span><span class="nx">onopen</span> <span class="o">=</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="nx">ws</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="s1">&#39;hello from browser&#39;</span><span class="p">);</span></span></span></code></pre></div>
</div>
<p>Or use a Node.js client:</p>
<div class="code">
<div class="code-head"><span class="code-lang">javascript</span><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">net</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;net&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">crypto</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;crypto&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">randomBytes</span><span class="p">(</span><span class="mi">16</span><span class="p">).</span><span class="nx">toString</span><span class="p">(</span><span class="s1">&#39;base64&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">socket</span> <span class="o">=</span> <span class="nx">net</span><span class="p">.</span><span class="nx">createConnection</span><span class="p">(</span><span class="mi">8080</span><span class="p">,</span> <span class="s1">&#39;localhost&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">socket</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;GET / HTTP/1.1\r\n&#39;</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;Host: localhost:8080\r\n&#39;</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;Upgrade: websocket\r\n&#39;</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;Connection: Upgrade\r\n&#39;</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="sb">`Sec-WebSocket-Key: </span><span class="si">${</span><span class="nx">key</span><span class="si">}</span><span class="sb">\r\n`</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;Sec-WebSocket-Version: 13\r\n&#39;</span> <span class="o">+</span>
</span></span><span class="line"><span class="cl">    <span class="s1">&#39;\r\n&#39;</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span></span></span></code></pre></div>
</div>
<h2 id="what-this-does-not-handle">What This Does Not Handle<a class="anchor" href="#what-this-does-not-handle" aria-label="Link to “What This Does Not Handle”">#</a></h2>
<p>This implementation is deliberately minimal. A production WebSocket server also needs:</p>
<ul>
<li><strong>Ping/pong frames</strong> for connection keepalive</li>
<li><strong>Fragmented messages</strong> where large payloads span multiple frames</li>
<li><strong>Close handshake</strong> with proper status codes</li>
<li><strong>Per-message compression</strong> (the permessage-deflate extension)</li>
<li><strong>Backpressure</strong> when clients read slower than you write</li>
</ul>
<p>That is why libraries like ws exist. But knowing the protocol means you understand what those libraries are doing, and you can debug problems at the frame level instead of guessing.</p>
<p>The entire WebSocket protocol fits in RFC 6455, which is surprisingly readable as RFCs go. The core framing logic is maybe 100 lines of code. Everything else is just handling edge cases.</p>
]]></content:encoded></item><item><title>Bypassing ISP Deep Packet Inspection on Linux</title><link>https://articles.wleeaf.dev/posts/dpi-bypass-linux/</link><pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/dpi-bypass-linux/</guid><description>When your ISP blocks websites at the packet level, here is how to get around it without a full VPN.</description><category>networking</category><category>privacy</category><category>linux</category><content:encoded><![CDATA[<p>My ISP does deep packet inspection. Not the basic DNS-level blocking that you can get around by switching to a different resolver. Actual packet inspection that looks at TLS handshakes and blocks connections to certain domains based on the SNI field in the ClientHello.</p>
<p>This means changing your DNS server does nothing. The ISP sees the domain name in the TLS handshake before the connection is encrypted and drops the packet. You need a different approach. Mine has two parts: zapret, which splits up the TLS ClientHello so the DPI system cannot read the SNI field in time, and DNS-over-TLS, so DNS queries stop travelling in plain text.</p>
<h2 id="how-dpi-blocking-works">How DPI Blocking Works<a class="anchor" href="#how-dpi-blocking-works" aria-label="Link to “How DPI Blocking Works”">#</a></h2>
<p>When you connect to a website over HTTPS, the first thing your browser sends is a TLS ClientHello message. This message is not encrypted. It contains the Server Name Indication field, which tells the server which domain you are trying to reach. This is necessary because multiple domains can share the same IP address, and the server needs to know which certificate to present.</p>
<p>Your ISP inspects this field. If the domain matches their blocklist, they inject a TCP RST packet or simply drop the connection. From your perspective, the site just times out or you get a connection reset.</p>
<h2 id="the-zapret-approach">The Zapret Approach<a class="anchor" href="#the-zapret-approach" aria-label="Link to “The Zapret Approach”">#</a></h2>
<p>There is a tool called zapret that manipulates outgoing packets to confuse DPI systems. It does not encrypt anything or route traffic through a proxy. Instead, it exploits the fact that DPI systems have to make real-time decisions about packets and often take shortcuts.</p>
<p>The basic techniques include fragmenting the ClientHello across multiple TCP segments, adding padding to push the SNI field into a second packet, or modifying TCP flags in ways that confuse stateful inspection engines but are tolerated by the destination server.</p>
<p>I run zapret as a systemd service. It hooks into the outgoing packet flow using nfqueue and modifies specific packets before they leave my machine. The destination server receives the same data, just split up differently, and handles it fine. The ISP DPI system sees fragments that it cannot reassemble quickly enough and lets them through.</p>
<h2 id="dns-over-tls-complement">DNS-over-TLS Complement<a class="anchor" href="#dns-over-tls-complement" aria-label="Link to “DNS-over-TLS Complement”">#</a></h2>
<p>Zapret handles the TLS handshake problem, but DNS queries can still leak information. Even if the ISP cannot block the connection, they can see which domains you are resolving and log that data.</p>
<p>I force all DNS through DNS-over-TLS using systemd-resolved. The configuration tells resolved to only use TLS-encrypted DNS and to require valid certificates from the upstream resolver. I use Cloudflare and Google as upstream servers since both support DNS-over-TLS.</p>
<p>The resolved config looks straightforward. You set the DNS servers and tell it to require encrypted transport. After restarting the service, all DNS queries from the machine go through TLS on port 853 instead of plaintext on port 53.</p>
<h2 id="the-vpn-conflict">The VPN Conflict<a class="anchor" href="#the-vpn-conflict" aria-label="Link to “The VPN Conflict”">#</a></h2>
<p>This setup conflicts with split-tunnel VPN configurations. When the VPN is active, DNS queries for private services need to go to the VPN DNS server over the tunnel. But the DPI bypass forces all DNS through TLS, which means queries to the VPN DNS server also try to use TLS.</p>
<p>The VPN DNS server is just dnsmasq. It does not speak TLS. So the queries fail and private domain names do not resolve.</p>
<p>The fix is a PostUp hook in the WireGuard configuration that disables DNS-over-TLS specifically on the VPN interface. The system still uses DoT for all other DNS queries, but queries routed through the VPN go as plain DNS over the encrypted tunnel. Since the tunnel itself is encrypted, this is fine from a privacy perspective.</p>
<p>Getting this right took some trial and error. The symptoms were confusing: public websites worked, VPN connected successfully, but private subdomains just would not resolve. Once I traced it to the DoT setting, the fix was one line.</p>
<h2 id="performance-impact">Performance Impact<a class="anchor" href="#performance-impact" aria-label="Link to “Performance Impact”">#</a></h2>
<p>Zapret adds essentially zero latency. The packet manipulation happens in kernel space and takes microseconds. DNS-over-TLS adds a small amount of latency to the first query for each domain, maybe 10 to 20 milliseconds. Subsequent queries are cached by resolved so you do not notice it.</p>
<p>I have been running this setup for months and have never noticed any performance impact in normal usage. Streaming, gaming, and large downloads all work at full speed. The only time I notice it is when I forget to check if the service is running and a blocked site fails to load.</p>
<h2 id="reliability">Reliability<a class="anchor" href="#reliability" aria-label="Link to “Reliability”">#</a></h2>
<p>The DPI bypass works about 99 percent of the time. Occasionally the ISP updates their inspection rules and a specific technique stops working. Zapret has multiple strategies and you can configure fallbacks. Usually updating to the latest version fixes any regressions within a day or two.</p>
<p>The DNS-over-TLS setup is rock solid. I have never had it fail. Systemd-resolved handles reconnections and failovers between upstream servers automatically.</p>
<h2 id="legal-and-ethical-notes">Legal and Ethical Notes<a class="anchor" href="#legal-and-ethical-notes" aria-label="Link to “Legal and Ethical Notes”">#</a></h2>
<p>This is not about accessing illegal content. My ISP blocks legitimate websites, often because a previous user of the domain or IP address did something that triggered a broad block. The blocking is opaque, there is no notification, and there is no process for getting a site unblocked.</p>
<p>Bypassing DPI to access legal content that your ISP has incorrectly or broadly blocked is a reasonable thing to do. The tools involved are all open source and widely used. Nobody has ever gotten in trouble for running zapret on their own machine.</p>
<p>If you are in a situation where your ISP does DPI blocking, this setup gives you back control over your own internet connection without the overhead and privacy implications of routing all your traffic through a third-party VPN.</p>
]]></content:encoded></item><item><title>Split Tunnel WireGuard: Private Services Without Routing Everything</title><link>https://articles.wleeaf.dev/posts/wireguard-split-tunnel/</link><pubDate>Wed, 25 Mar 2026 00:00:00 +0000</pubDate><guid>https://articles.wleeaf.dev/posts/wireguard-split-tunnel/</guid><description>How to set up WireGuard so only traffic to your private services goes through the tunnel, while everything else uses your normal connection.</description><category>wireguard</category><category>vpn</category><category>networking</category><content:encoded><![CDATA[<p>Most WireGuard tutorials show you how to route all traffic through the tunnel. That is great for privacy but terrible for latency and bandwidth. If you just want to access a few private services on your server, there is a much better approach: put only your VPN subnet in the client&rsquo;s <code>AllowedIPs</code>, so traffic to those services goes through the tunnel and everything else takes your normal route.</p>
<h2 id="the-problem">The Problem<a class="anchor" href="#the-problem" aria-label="Link to “The Problem”">#</a></h2>
<p>I run several services on my VPS that should not be publicly accessible: a git server, an admin panel, a VPN management UI, and a mail server admin interface. These need to be reachable from my laptop and phone, but nobody else should be able to see them.</p>
<p>The obvious solution is a VPN. But I do not want all my traffic going through a server in Germany when I am watching a video or browsing the web. I want my normal internet connection for everything except the few private services.</p>
<h2 id="split-tunnel-configuration">Split Tunnel Configuration<a class="anchor" href="#split-tunnel-configuration" aria-label="Link to “Split Tunnel Configuration”">#</a></h2>
<p>The key is the <code>AllowedIPs</code> setting in the WireGuard client config. Instead of <code>0.0.0.0/0</code> which routes everything, you specify only the VPN subnet:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>[Interface]
PrivateKey = &lt;client-private-key&gt;
Address = 10.0.0.2/24
DNS = 10.0.0.1

[Peer]
PublicKey = &lt;server-public-key&gt;
Endpoint = your-server-ip:51820
AllowedIPs = 10.0.0.0/24</code></pre>
</div>
<p>With this config, only traffic destined for <code>10.0.0.x</code> addresses goes through the tunnel. Everything else takes the normal route. Your browsing speed stays the same, your public IP does not change, and you still get access to your private services.</p>
<h2 id="dns-is-where-the-magic-happens">DNS Is Where the Magic Happens<a class="anchor" href="#dns-is-where-the-magic-happens" aria-label="Link to “DNS Is Where the Magic Happens”">#</a></h2>
<p>The split tunnel routes packets, but your browser needs to know that <code>admin.example.com</code> should resolve to a VPN IP instead of a public one. This is where a local DNS server comes in.</p>
<p>I run dnsmasq on the VPN server, listening only on the WireGuard interface. It has entries like:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>address=/git.example.com/10.0.0.1
address=/admin.example.com/10.0.0.1
address=/vpn.example.com/10.0.0.1</code></pre>
</div>
<p>For any domain not in this list, dnsmasq forwards the query to Cloudflare or Google DNS. So private subdomains resolve to the VPN IP, and everything else resolves normally.</p>
<p>The client config sets <code>DNS = 10.0.0.1</code> which tells the system to use the VPN DNS server. Combined with the split tunnel routing, DNS queries for private domains go through the tunnel to dnsmasq, and all other DNS queries go through the tunnel too but get forwarded to public DNS by dnsmasq.</p>
<h2 id="the-dns-over-tls-conflict">The DNS-over-TLS Conflict<a class="anchor" href="#the-dns-over-tls-conflict" aria-label="Link to “The DNS-over-TLS Conflict”">#</a></h2>
<p>This one took me a while to figure out. I use a DPI bypass tool on my local machine that forces all DNS through DNS-over-TLS via systemd-resolved. When the VPN comes up, the DNS setting should override this, but it does not always work cleanly.</p>
<p>The fix was a PostUp script in the WireGuard config:</p>
<div class="code is-plain">
<div class="code-head"><button type="button" class="code-copy" data-copy-code hidden><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2.5"/><path d="M16 8V6.5A2.5 2.5 0 0 0 13.5 4h-7A2.5 2.5 0 0 0 4 6.5v7A2.5 2.5 0 0 0 6.5 16H8"/></svg><span>Copy</span></button></div>
<pre tabindex="0"><code>PostUp = resolvectl dnsovertls %i no</code></pre>
</div>
<p>This disables DNS-over-TLS specifically on the VPN interface so queries to 10.0.0.1 go through as plain DNS over the tunnel. Without this, the system tries to establish a TLS connection to the dnsmasq server, which obviously does not support TLS, and private domains fail to resolve.</p>
<h2 id="no-public-dns-leaks">No Public DNS Leaks<a class="anchor" href="#no-public-dns-leaks" aria-label="Link to “No Public DNS Leaks”">#</a></h2>
<p>One nice side effect of this setup: your private subdomains are completely invisible. There are no public DNS records for them. They do not show up in certificate transparency logs because the wildcard cert covers them. Someone scanning your domain will find the public subdomains but have no idea the private ones exist.</p>
<p>The only way to discover them is to be on the VPN, at which point dnsmasq resolves them. And even if someone knows the subdomain name and tries to access it from the public internet, Caddy returns 403 based on the source IP check.</p>
<h2 id="mobile-access">Mobile Access<a class="anchor" href="#mobile-access" aria-label="Link to “Mobile Access”">#</a></h2>
<p>WireGuard works great on phones. The official app for both iOS and Android supports split tunneling natively. I have the same config on my phone: only VPN traffic goes through the tunnel. This means I can check my git server from my phone without draining battery or slowing down everything else.</p>
<p>The one downside is that you have to manually toggle the VPN on when you need it. There is no automatic connection based on network or location. But honestly, it takes one tap and the connection establishes in under a second.</p>
<h2 id="resource-usage">Resource Usage<a class="anchor" href="#resource-usage" aria-label="Link to “Resource Usage”">#</a></h2>
<p>WireGuard is absurdly lightweight. The server side runs as a single container using about 30MB of RAM. The CPU usage is essentially zero unless you are pushing serious bandwidth. On the client side, the kernel module handles everything so there is no userspace overhead.</p>
<p>Compare this to OpenVPN which needs a full userspace daemon, TLS handshakes, and significantly more CPU per connection. For a personal VPN with one or two clients, WireGuard is comically overpowered.</p>
<h2 id="final-thoughts">Final Thoughts<a class="anchor" href="#final-thoughts" aria-label="Link to “Final Thoughts”">#</a></h2>
<p>Split tunnel WireGuard gives you the best of both worlds: fast, private access to your services without compromising your normal internet experience. The setup takes maybe 30 minutes and the result is rock solid. I have had zero downtime in months.</p>
<p>The only complexity is the DNS piece, and even that is just a few lines in a dnsmasq config file. If you are running private services on a VPS, this is the way to expose them.</p>
]]></content:encoded></item></channel></rss>