<?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: thomas verhave</title>
    <description>The latest articles on DEV Community by thomas verhave (@thomas_verhave_8976b2c977).</description>
    <link>https://dev.to/thomas_verhave_8976b2c977</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%2F4034510%2F14cb4aec-f797-4f7e-8375-78c44f5b07e9.jpg</url>
      <title>DEV Community: thomas verhave</title>
      <link>https://dev.to/thomas_verhave_8976b2c977</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thomas_verhave_8976b2c977"/>
    <language>en</language>
    <item>
      <title>Tiny Alternative Internet</title>
      <dc:creator>thomas verhave</dc:creator>
      <pubDate>Fri, 17 Jul 2026 23:30:03 +0000</pubDate>
      <link>https://dev.to/thomas_verhave_8976b2c977/tiny-alternative-internet-7bk</link>
      <guid>https://dev.to/thomas_verhave_8976b2c977/tiny-alternative-internet-7bk</guid>
      <description>&lt;p&gt;title: "I built a tiny alternative internet in pure Python — a protocol, a browser, and a firewall" published: false tags: python, networking, showdev, programming canonical_url: &lt;a href="https://abouttime-d5a.pages.dev/project-weft" rel="noopener noreferrer"&gt;https://abouttime-d5a.pages.dev/project-weft&lt;/a&gt;&lt;br&gt;
Someone said "let's make a new internet" — sarcastically. So I made a working one.&lt;/p&gt;

&lt;p&gt;Not a metaphor, not a framework. weft is a small line-based protocol over raw TCP, with its own address scheme, its own markup, a terminal browser that speaks it, and an application-layer firewall you can drive in the browser. Three little servers link to each other to form a network. It's all pure Python standard library — zero dependencies — and the whole thing is a few hundred lines.&lt;/p&gt;

&lt;p&gt;It will never scale to a billion users. That was the point.&lt;/p&gt;

&lt;p&gt;The bet&lt;br&gt;
The web we have is heavy. Every page drags a megabyte of JavaScript to show a paragraph of text, and something is always measuring you. weft is the opposite bet, and the whole design falls out of four rules:&lt;/p&gt;

&lt;p&gt;text first, always&lt;br&gt;
no client-side code, so there's no surveillance surface&lt;br&gt;
a page is a file you can read with cat&lt;br&gt;
the network is small enough to hold in your head&lt;br&gt;
Here's what building each layer taught me.&lt;/p&gt;

&lt;p&gt;Layer 1 — the protocol&lt;br&gt;
I didn't want HTTP. HTTP is enormous once you include everything real clients expect. So weft borrows its shape from Gemini: a request is one line, a response is one status line then the body.&lt;/p&gt;

&lt;p&gt;address:   weft://host:port/path&lt;br&gt;
request:   GET /path\n&lt;br&gt;
response:  20 text/weft\n   ...then the body bytes&lt;br&gt;
Four status codes, deliberately. That's the entire vocabulary:&lt;/p&gt;

&lt;p&gt;20  ok, body follows&lt;br&gt;
30  redirect, body is the new address&lt;br&gt;
40  not found&lt;br&gt;
50  server error&lt;br&gt;
The client is almost nothing — one connection, one page, then the socket closes:&lt;/p&gt;

&lt;p&gt;def fetch(addr, *, timeout=5.0):&lt;br&gt;
    with socket.create_connection((addr.host, addr.port), timeout=timeout) as sock:&lt;br&gt;
        sock.sendall(f"GET {addr.path}\n".encode())&lt;br&gt;
        sock.shutdown(socket.SHUT_WR)          # I'm done writing; send me the page&lt;br&gt;
        raw = b""&lt;br&gt;
        while chunk := sock.recv(65536):&lt;br&gt;
            raw += chunk&lt;br&gt;
    header, _, body = raw.partition(b"\n")&lt;br&gt;
    status, _, meta = header.decode(errors="replace").partition(" ")&lt;br&gt;
    return Response(int(status), meta, body)&lt;br&gt;
That shutdown(SHUT_WR) is the whole flow-control story: the client half-closes to say "I've sent my one line, everything you send back is the response." No content-length, no chunked encoding, no keep-alive. The connection is the framing.&lt;/p&gt;

&lt;p&gt;(The default port is 1965 — a wink at Gemini. The year, not the protocol.)&lt;/p&gt;

&lt;p&gt;Layer 2 — the markup&lt;br&gt;
If a page is meant to be readable with cat, the markup has to be plain text that already looks right. So .weft pages are line-based, like Gemtext. A line starting with =&amp;gt; is a link; # and ## are headings; everything else is a paragraph.&lt;/p&gt;

&lt;h1&gt;
  
  
  a small manifesto
&lt;/h1&gt;

&lt;p&gt;The web we have is heavy. weft is the opposite bet:&lt;br&gt;
text first, no client-side code, a page you can read with cat.&lt;/p&gt;

&lt;p&gt;=&amp;gt; / home&lt;br&gt;
=&amp;gt; weft://127.0.0.1:1966/ visit the garden&lt;br&gt;
No inline styling, no nesting, no ambiguity. The parser is a for loop over splitlines(). Because links live on their own lines, the terminal browser can just number them and let you follow a link by typing its number — no cursor, no mouse.&lt;/p&gt;

&lt;p&gt;Layer 3 — the server&lt;br&gt;
Each "node" serves one site (a folder of .weft files) with a thread per connection. The only real defensive code is path-traversal protection — map a request path to a file under the root, or refuse:&lt;/p&gt;

&lt;p&gt;def resolve(root, path):&lt;br&gt;
    rel = path.lstrip("/") or "index.weft"&lt;br&gt;
    candidate = (root / rel).resolve()&lt;br&gt;
    if root.resolve() not in candidate.parents and candidate != root.resolve():&lt;br&gt;
        return None          # tried to escape the site root — deny&lt;br&gt;
    return candidate if candidate.is_file() else None&lt;br&gt;
Three nodes run on ports 1965/1966/1967, and their pages link to each other with weft:// addresses — so you get an actual little inter-network you can browse between.&lt;/p&gt;

&lt;p&gt;Layer 4 — the browser&lt;br&gt;
The browser is a terminal REPL. It fetches a page, renders the text, prints the links as a numbered list, and waits:&lt;/p&gt;

&lt;p&gt;   follow that link&lt;br&gt;
b          back&lt;br&gt;
r          reload&lt;br&gt;
g    go to a weft:// address&lt;br&gt;
q          quit&lt;br&gt;
That's the entire client. It's oddly calming to use — no tabs, no spinner, no cookie banner. Just text and numbered doors.&lt;/p&gt;

&lt;p&gt;Layer 5 — the firewall (the fun part)&lt;br&gt;
This is where it stopped being a toy for me. I put an application-layer firewall in front of a node — a filtering gateway that inspects every connection (source IP + requested path), evaluates an ordered ruleset, enforces a default policy and rate limits, logs every decision, then transparently forwards allowed traffic to the backend.&lt;/p&gt;

&lt;p&gt;The ruleset reads like an iptables chain or a pf config — first match wins, default-deny:&lt;/p&gt;

&lt;p&gt;policy deny&lt;/p&gt;

&lt;h1&gt;
  
  
  a loose overall cap, plus a tighter one just for search
&lt;/h1&gt;

&lt;p&gt;ratelimit 10/10s&lt;br&gt;
ratelimit 2/10s path /search*&lt;/p&gt;

&lt;p&gt;deny  path /admin*                         # nobody reaches the admin area&lt;br&gt;
allow from 127.0.0.0/8 path /              # loopback is trusted&lt;br&gt;
allow from 127.0.0.0/8 path /manifesto&lt;br&gt;
deny  from 192.0.2.0/24                    # a blocked network (RFC 5737 TEST-NET)&lt;/p&gt;

&lt;h1&gt;
  
  
  everything else falls through to &lt;code&gt;policy deny&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;Conditions AND together; a rule with no conditions is a catch-all. Matching is CIDR for sources and shell-glob for paths (/admin* catches /admin/keys). And because editing rules shouldn't drop live connections, it hot-reloads on SIGHUP:&lt;/p&gt;

&lt;p&gt;kill -HUP      # re-reads firewall.rules without closing the socket&lt;br&gt;
Writing a default-deny, first-match-wins engine by hand is the single best way I've found to actually understand why firewall rule order matters — you feel it the moment you put an allow after a deny that shadows it.&lt;/p&gt;

&lt;p&gt;The part that surprised me&lt;br&gt;
The same rule engine runs in the browser. The project page has a live simulator where you can type a source IP and a path, watch the rules evaluate top-to-bottom, and see exactly which line decides the verdict — driven by a JavaScript port of the same logic in firewall.py. Building it twice, in two languages, forced me to write the rules as data rather than control flow, which made both versions cleaner.&lt;/p&gt;

&lt;p&gt;What I actually learned&lt;br&gt;
Constraints are a design tool. "No client-side code" isn't a limitation, it's what makes the markup, the browser, and the privacy story all fall into place at once.&lt;br&gt;
The stdlib is bigger than you think. socket, threading, dataclasses, re, pathlib, fnmatch, ipaddress — that's the entire dependency list, and it's plenty to build a protocol stack.&lt;br&gt;
You understand a thing when you rebuild it. I've used firewalls for years; I only really got rule-order shadowing and default-deny posture after writing 380 lines that enforce them.&lt;br&gt;
Try it&lt;br&gt;
It runs on your machine in one command (./run.sh launches the three nodes and drops you into the browser). The write-up, the live firewall simulator, and links to the code are here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://abouttime-d5a.pages.dev/project-weft" rel="noopener noreferrer"&gt;https://abouttime-d5a.pages.dev/project-weft&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd genuinely love to hear how you'd have designed the transport layer differently — the half-close-as-framing trick feels elegant but I'm sure it has sharp edges I haven't hit yet.&lt;/p&gt;

</description>
      <category>python</category>
      <category>networking</category>
      <category>showdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Someone said "let's make a new internet" — sarcastically.</title>
      <dc:creator>thomas verhave</dc:creator>
      <pubDate>Fri, 17 Jul 2026 23:23:09 +0000</pubDate>
      <link>https://dev.to/thomas_verhave_8976b2c977/someone-said-lets-make-a-new-internet-sarcastically-ljc</link>
      <guid>https://dev.to/thomas_verhave_8976b2c977/someone-said-lets-make-a-new-internet-sarcastically-ljc</guid>
      <description>&lt;p&gt;Thomas Verhave&lt;br&gt;
Home&lt;br&gt;
Projects&lt;br&gt;
Writing&lt;/p&gt;

&lt;p&gt;Writing · Build Notes&lt;br&gt;
I built a tiny alternative internet in pure Python — a protocol, a browser, and a firewall&lt;/p&gt;

&lt;p&gt;Someone said "let's make a new internet" — sarcastically.&lt;/p&gt;

&lt;p&gt;Someone said "let's make a new internet" — sarcastically. So I made a working one.&lt;/p&gt;

&lt;p&gt;Not a metaphor, not a framework. weft is a small line-based protocol over raw TCP, with its own address scheme, its own markup, a terminal browser that speaks it, and an application-layer firewall you can drive in the browser. Three little servers link to each other to form a network. It's all pure Python standard library — zero dependencies — and the whole thing is a few hundred lines.&lt;/p&gt;

&lt;p&gt;It will never scale to a billion users. That was the point.&lt;br&gt;
The bet&lt;/p&gt;

&lt;p&gt;The web we have is heavy. Every page drags a megabyte of JavaScript to show a paragraph of text, and something is always measuring you. weft is the opposite bet, and the whole design falls out of four rules:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;text first, always
no client-side code, so there's no surveillance surface
a page is a file you can read with cat
the network is small enough to hold in your head
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here's what building each layer taught me.&lt;br&gt;
Layer 1 — the protocol&lt;/p&gt;

&lt;p&gt;I didn't want HTTP. HTTP is enormous once you include everything real clients expect. So weft borrows its shape from Gemini: a request is one line, a response is one status line then the body.&lt;/p&gt;

&lt;p&gt;address:   weft://host:port/path&lt;br&gt;
request:   GET /path\n&lt;br&gt;
response:  20 text/weft\n   ...then the body bytes&lt;/p&gt;

&lt;p&gt;Four status codes, deliberately. That's the entire vocabulary:&lt;/p&gt;

&lt;p&gt;20  ok, body follows&lt;br&gt;
30  redirect, body is the new address&lt;br&gt;
40  not found&lt;br&gt;
50  server error&lt;/p&gt;

&lt;p&gt;The client is almost nothing — one connection, one page, then the socket closes:&lt;/p&gt;

&lt;p&gt;def fetch(addr, *, timeout=5.0):&lt;br&gt;
    with socket.create_connection((addr.host, addr.port), timeout=timeout) as sock:&lt;br&gt;
        sock.sendall(f"GET {addr.path}\n".encode())&lt;br&gt;
        sock.shutdown(socket.SHUT_WR)          # I'm done writing; send me the page&lt;br&gt;
        raw = b""&lt;br&gt;
        while chunk := sock.recv(65536):&lt;br&gt;
            raw += chunk&lt;br&gt;
    header, _, body = raw.partition(b"\n")&lt;br&gt;
    status, _, meta = header.decode(errors="replace").partition(" ")&lt;br&gt;
    return Response(int(status), meta, body)&lt;/p&gt;

&lt;p&gt;That shutdown(SHUT_WR) is the whole flow-control story: the client half-closes to say "I've sent my one line, everything you send back is the response." No content-length, no chunked encoding, no keep-alive. The connection is the framing.&lt;/p&gt;

&lt;p&gt;(The default port is 1965 — a wink at Gemini. The year, not the protocol.)&lt;br&gt;
Layer 2 — the markup&lt;/p&gt;

&lt;p&gt;If a page is meant to be readable with cat, the markup has to be plain text that already looks right. So .weft pages are line-based, like Gemtext. A line starting with =&amp;gt; is a link; # and ## are headings; everything else is a paragraph.&lt;/p&gt;

&lt;h1&gt;
  
  
  a small manifesto
&lt;/h1&gt;

&lt;p&gt;The web we have is heavy. weft is the opposite bet:&lt;br&gt;
text first, no client-side code, a page you can read with cat.&lt;/p&gt;

&lt;p&gt;=&amp;gt; / home&lt;br&gt;
=&amp;gt; weft://127.0.0.1:1966/ visit the garden&lt;/p&gt;

&lt;p&gt;No inline styling, no nesting, no ambiguity. The parser is a for loop over splitlines(). Because links live on their own lines, the terminal browser can just number them and let you follow a link by typing its number — no cursor, no mouse.&lt;br&gt;
Layer 3 — the server&lt;/p&gt;

&lt;p&gt;Each "node" serves one site (a folder of .weft files) with a thread per connection. The only real defensive code is path-traversal protection — map a request path to a file under the root, or refuse:&lt;/p&gt;

&lt;p&gt;def resolve(root, path):&lt;br&gt;
    rel = path.lstrip("/") or "index.weft"&lt;br&gt;
    candidate = (root / rel).resolve()&lt;br&gt;
    if root.resolve() not in candidate.parents and candidate != root.resolve():&lt;br&gt;
        return None          # tried to escape the site root — deny&lt;br&gt;
    return candidate if candidate.is_file() else None&lt;/p&gt;

&lt;p&gt;Three nodes run on ports 1965/1966/1967, and their pages link to each other with weft:// addresses — so you get an actual little inter-network you can browse between.&lt;br&gt;
Layer 4 — the browser&lt;/p&gt;

&lt;p&gt;The browser is a terminal REPL. It fetches a page, renders the text, prints the links as a numbered list, and waits:&lt;/p&gt;

&lt;p&gt;   follow that link&lt;br&gt;
b          back&lt;br&gt;
r          reload&lt;br&gt;
g    go to a weft:// address&lt;br&gt;
q          quit&lt;/p&gt;

&lt;p&gt;That's the entire client. It's oddly calming to use — no tabs, no spinner, no cookie banner. Just text and numbered doors.&lt;br&gt;
Layer 5 — the firewall (the fun part)&lt;/p&gt;

&lt;p&gt;This is where it stopped being a toy for me. I put an application-layer firewall in front of a node — a filtering gateway that inspects every connection (source IP + requested path), evaluates an ordered ruleset, enforces a default policy and rate limits, logs every decision, then transparently forwards allowed traffic to the backend.&lt;/p&gt;

&lt;p&gt;The ruleset reads like an iptables chain or a pf config — first match wins, default-deny:&lt;/p&gt;

&lt;p&gt;policy deny&lt;/p&gt;

&lt;h1&gt;
  
  
  a loose overall cap, plus a tighter one just for search
&lt;/h1&gt;

&lt;p&gt;ratelimit 10/10s&lt;br&gt;
ratelimit 2/10s path /search*&lt;/p&gt;

&lt;p&gt;deny  path /admin*                         # nobody reaches the admin area&lt;br&gt;
allow from 127.0.0.0/8 path /              # loopback is trusted&lt;br&gt;
allow from 127.0.0.0/8 path /manifesto&lt;br&gt;
deny  from 192.0.2.0/24                    # a blocked network (RFC 5737 TEST-NET)&lt;/p&gt;

&lt;h1&gt;
  
  
  everything else falls through to &lt;code&gt;policy deny&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;Conditions AND together; a rule with no conditions is a catch-all. Matching is CIDR for sources and shell-glob for paths (/admin* catches /admin/keys). And because editing rules shouldn't drop live connections, it hot-reloads on SIGHUP:&lt;/p&gt;

&lt;p&gt;kill -HUP      # re-reads firewall.rules without closing the socket&lt;/p&gt;

&lt;p&gt;Writing a default-deny, first-match-wins engine by hand is the single best way I've found to actually understand why firewall rule order matters — you feel it the moment you put an allow after a deny that shadows it.&lt;br&gt;
The part that surprised me&lt;/p&gt;

&lt;p&gt;The same rule engine runs in the browser. The project page has a live simulator where you can type a source IP and a path, watch the rules evaluate top-to-bottom, and see exactly which line decides the verdict — driven by a JavaScript port of the same logic in firewall.py. Building it twice, in two languages, forced me to write the rules as data rather than control flow, which made both versions cleaner.&lt;br&gt;
What I actually learned&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Constraints are a design tool. "No client-side code" isn't a limitation, it's what makes the markup, the browser, and the privacy story all fall into place at once.
The stdlib is bigger than you think. socket, threading, dataclasses, re, pathlib, fnmatch, ipaddress — that's the entire dependency list, and it's plenty to build a protocol stack.
You understand a thing when you rebuild it. I've used firewalls for years; I only really got rule-order shadowing and default-deny posture after writing 380 lines that enforce them.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A build-story · see the live project → · more writing →&lt;/p&gt;

</description>
      <category>networking</category>
      <category>python</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Show HN: Weft</title>
      <dc:creator>thomas verhave</dc:creator>
      <pubDate>Fri, 17 Jul 2026 23:17:33 +0000</pubDate>
      <link>https://dev.to/thomas_verhave_8976b2c977/show-hn-weft-mo7</link>
      <guid>https://dev.to/thomas_verhave_8976b2c977/show-hn-weft-mo7</guid>
      <description>&lt;p&gt;Show HN: Weft – an alternative internet with its own TCP-like &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL: &lt;a href="https://abouttime-d5a.pages.dev/project-weft" rel="noopener noreferrer"&gt;https://abouttime-d5a.pages.dev/project-weft&lt;/a&gt;
omeone said "let's make a new internet" — sarcastically. So I made a working one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not a metaphor, not a framework. weft is a small line-based protocol over raw TCP, with its own address scheme, its own markup, a terminal browser that speaks it, and an application-layer firewall you can drive in the browser. Three little servers link to each other to form a network. It's all pure Python standard library — zero dependencies — and the whole thing is a few hundred lines.&lt;/p&gt;

&lt;p&gt;It will never scale to a billion users. That was the point.&lt;br&gt;
The bet&lt;/p&gt;

&lt;p&gt;The web we have is heavy. Every page drags a megabyte of JavaScript to show a paragraph of text, and something is always measuring you. weft is the opposite bet, and the whole design falls out of four rules:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;text first, always
no client-side code, so there's no surveillance surface
a page is a file you can read with cat
the network is small enough to hold in your head
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Here's what building each layer taught me.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>python</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>alternative internet</title>
      <dc:creator>thomas verhave</dc:creator>
      <pubDate>Fri, 17 Jul 2026 22:46:40 +0000</pubDate>
      <link>https://dev.to/thomas_verhave_8976b2c977/alternative-internet-8ng</link>
      <guid>https://dev.to/thomas_verhave_8976b2c977/alternative-internet-8ng</guid>
      <description>&lt;p&gt;title: "I built a tiny alternative internet in pure Python — a protocol, a browser, and a firewall" published: false tags: python, networking, showdev, programming canonical_url: &lt;a href="https://abouttime-d5a.pages.dev/project-weft" rel="noopener noreferrer"&gt;https://abouttime-d5a.pages.dev/project-weft&lt;/a&gt;&lt;br&gt;
Someone said "let's make a new internet" — sarcastically. So I made a working one.&lt;/p&gt;

&lt;p&gt;Not a metaphor, not a framework. weft is a small line-based protocol over raw TCP, with its own address scheme, its own markup, a terminal browser that speaks it, and an application-layer firewall you can drive in the browser. Three little servers link to each other to form a network. It's all pure Python standard library — zero dependencies — and the whole thing is a few hundred lines.&lt;/p&gt;

&lt;p&gt;It will never scale to a billion users. That was the point.&lt;/p&gt;

&lt;p&gt;The bet&lt;br&gt;
The web we have is heavy. Every page drags a megabyte of JavaScript to show a paragraph of text, and something is always measuring you. weft is the opposite bet, and the whole design falls out of four rules:&lt;/p&gt;

&lt;p&gt;text first, always&lt;br&gt;
no client-side code, so there's no surveillance surface&lt;br&gt;
a page is a file you can read with cat&lt;br&gt;
the network is small enough to hold in your head&lt;br&gt;
Here's what building each layer taught me.&lt;/p&gt;

&lt;p&gt;Layer 1 — the protocol&lt;br&gt;
I didn't want HTTP. HTTP is enormous once you include everything real clients expect. So weft borrows its shape from Gemini: a request is one line, a response is one status line then the body.&lt;/p&gt;

&lt;p&gt;address:   weft://host:port/path&lt;br&gt;
request:   GET /path\n&lt;br&gt;
response:  20 text/weft\n   ...then the body bytes&lt;br&gt;
Four status codes, deliberately. That's the entire vocabulary:&lt;/p&gt;

&lt;p&gt;20  ok, body follows&lt;br&gt;
30  redirect, body is the new address&lt;br&gt;
40  not found&lt;br&gt;
50  server error&lt;br&gt;
The client is almost nothing — one connection, one page, then the socket closes:&lt;/p&gt;

&lt;p&gt;def fetch(addr, *, timeout=5.0):&lt;br&gt;
    with socket.create_connection((addr.host, addr.port), timeout=timeout) as sock:&lt;br&gt;
        sock.sendall(f"GET {addr.path}\n".encode())&lt;br&gt;
        sock.shutdown(socket.SHUT_WR)          # I'm done writing; send me the page&lt;br&gt;
        raw = b""&lt;br&gt;
        while chunk := sock.recv(65536):&lt;br&gt;
            raw += chunk&lt;br&gt;
    header, _, body = raw.partition(b"\n")&lt;br&gt;
    status, _, meta = header.decode(errors="replace").partition(" ")&lt;br&gt;
    return Response(int(status), meta, body)&lt;br&gt;
That shutdown(SHUT_WR) is the whole flow-control story: the client half-closes to say "I've sent my one line, everything you send back is the response." No content-length, no chunked encoding, no keep-alive. The connection is the framing.&lt;/p&gt;

&lt;p&gt;(The default port is 1965 — a wink at Gemini. The year, not the protocol.)&lt;/p&gt;

&lt;p&gt;Layer 2 — the markup&lt;br&gt;
If a page is meant to be readable with cat, the markup has to be plain text that already looks right. So .weft pages are line-based, like Gemtext. A line starting with =&amp;gt; is a link; # and ## are headings; everything else is a paragraph.&lt;/p&gt;

&lt;h1&gt;
  
  
  a small manifesto
&lt;/h1&gt;

&lt;p&gt;The web we have is heavy. weft is the opposite bet:&lt;br&gt;
text first, no client-side code, a page you can read with cat.&lt;/p&gt;

&lt;p&gt;=&amp;gt; / home&lt;br&gt;
=&amp;gt; weft://127.0.0.1:1966/ visit the garden&lt;br&gt;
No inline styling, no nesting, no ambiguity. The parser is a for loop over splitlines(). Because links live on their own lines, the terminal browser can just number them and let you follow a link by typing its number — no cursor, no mouse.&lt;/p&gt;

&lt;p&gt;Layer 3 — the server&lt;br&gt;
Each "node" serves one site (a folder of .weft files) with a thread per connection. The only real defensive code is path-traversal protection — map a request path to a file under the root, or refuse:&lt;/p&gt;

&lt;p&gt;def resolve(root, path):&lt;br&gt;
    rel = path.lstrip("/") or "index.weft"&lt;br&gt;
    candidate = (root / rel).resolve()&lt;br&gt;
    if root.resolve() not in candidate.parents and candidate != root.resolve():&lt;br&gt;
        return None          # tried to escape the site root — deny&lt;br&gt;
    return candidate if candidate.is_file() else None&lt;br&gt;
Three nodes run on ports 1965/1966/1967, and their pages link to each other with weft:// addresses — so you get an actual little inter-network you can browse between.&lt;/p&gt;

&lt;p&gt;Layer 4 — the browser&lt;br&gt;
The browser is a terminal REPL. It fetches a page, renders the text, prints the links as a numbered list, and waits:&lt;/p&gt;

&lt;p&gt;   follow that link&lt;br&gt;
b          back&lt;br&gt;
r          reload&lt;br&gt;
g    go to a weft:// address&lt;br&gt;
q          quit&lt;br&gt;
That's the entire client. It's oddly calming to use — no tabs, no spinner, no cookie banner. Just text and numbered doors.&lt;/p&gt;

&lt;p&gt;Layer 5 — the firewall (the fun part)&lt;br&gt;
This is where it stopped being a toy for me. I put an application-layer firewall in front of a node — a filtering gateway that inspects every connection (source IP + requested path), evaluates an ordered ruleset, enforces a default policy and rate limits, logs every decision, then transparently forwards allowed traffic to the backend.&lt;/p&gt;

&lt;p&gt;The ruleset reads like an iptables chain or a pf config — first match wins, default-deny:&lt;/p&gt;

&lt;p&gt;policy deny&lt;/p&gt;

&lt;h1&gt;
  
  
  a loose overall cap, plus a tighter one just for search
&lt;/h1&gt;

&lt;p&gt;ratelimit 10/10s&lt;br&gt;
ratelimit 2/10s path /search*&lt;/p&gt;

&lt;p&gt;deny  path /admin*                         # nobody reaches the admin area&lt;br&gt;
allow from 127.0.0.0/8 path /              # loopback is trusted&lt;br&gt;
allow from 127.0.0.0/8 path /manifesto&lt;br&gt;
deny  from 192.0.2.0/24                    # a blocked network (RFC 5737 TEST-NET)&lt;/p&gt;

&lt;h1&gt;
  
  
  everything else falls through to &lt;code&gt;policy deny&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;Conditions AND together; a rule with no conditions is a catch-all. Matching is CIDR for sources and shell-glob for paths (/admin* catches /admin/keys). And because editing rules shouldn't drop live connections, it hot-reloads on SIGHUP:&lt;/p&gt;

&lt;p&gt;kill -HUP      # re-reads firewall.rules without closing the socket&lt;br&gt;
Writing a default-deny, first-match-wins engine by hand is the single best way I've found to actually understand why firewall rule order matters — you feel it the moment you put an allow after a deny that shadows it.&lt;/p&gt;

&lt;p&gt;The part that surprised me&lt;br&gt;
The same rule engine runs in the browser. The project page has a live simulator where you can type a source IP and a path, watch the rules evaluate top-to-bottom, and see exactly which line decides the verdict — driven by a JavaScript port of the same logic in firewall.py. Building it twice, in two languages, forced me to write the rules as data rather than control flow, which made both versions cleaner.&lt;/p&gt;

&lt;p&gt;What I actually learned&lt;br&gt;
Constraints are a design tool. "No client-side code" isn't a limitation, it's what makes the markup, the browser, and the privacy story all fall into place at once.&lt;br&gt;
The stdlib is bigger than you think. socket, threading, dataclasses, re, pathlib, fnmatch, ipaddress — that's the entire dependency list, and it's plenty to build a protocol stack.&lt;br&gt;
You understand a thing when you rebuild it. I've used firewalls for years; I only really got rule-order shadowing and default-deny posture after writing 380 lines that enforce them.&lt;br&gt;
Try it&lt;br&gt;
It runs on your machine in one command (./run.sh launches the three nodes and drops you into the browser). The write-up, the live firewall simulator, and links to the code are here:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://abouttime-d5a.pages.dev/project-weft" rel="noopener noreferrer"&gt;https://abouttime-d5a.pages.dev/project-weft&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd genuinely love to hear how you'd have designed the transport layer differently — the half-close-as-framing trick feels elegant but I'm sure it has sharp edges I haven't hit yet.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>python</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
