Skip to content
C.W.K.
Lesson 04 of 05 · published

Loopback Only — Who May Knock

~14 min · loopback, host-header, origin, x-forwarded-for, defense-in-depth

Level 0Idle Hands
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"The app door is loopback-only and has no public web service." — Tinker architecture

Three Kinds of Caller to Keep Out

An HTTP port on a Mac can be reached by more than the program you wrote it for. Another machine on the network, if the socket listens beyond loopback. A web page open in any browser on the same Mac — browsers will happily send requests to 127.0.0.1. And anything that arrives through a proxy, which can make a remote caller look local. Tinker's door is built to refuse all three, with checks at three layers that overlap wherever they can.

Layer One: the Listener

The listener binds to loopback and checks every request's Host header before routing. A loopback name with the listener's own port passes. Tinker also allow-lists the literal 127.0.0.1 at any port, deliberately: Pippa reaches a door through a forward that may present its own local port in Host, and refusing that would refuse her. The same layer refuses anything carrying a browser Origin header or Sec-Fetch-Site, which browsers attach to cross-site requests — so a web page cannot drive the door even though it is local. Bodies over 16 KiB are refused.

Layer Two: Tinker's Handler

Before routing, Tinker's own handler refuses any request that carries X-Forwarded-For, the header proxies add to say who the real client was: "Only local callers may use this door." Then it routes exactly three things: GET /health, GET /api/app-actions (the manifest) and POST /api/app-actions/<name>. Anything else is 404.

Layer Three: the Kit's Rule

Inside the worker, the kit's trusted_caller repeats the header checks in its own terms — no forwarding header, no Origin — before building the action registry. Its loopback test sees only the 127.0.0.1 the agent hands it, not the network peer, so keeping other machines out is the listener's job alone. And the kit's first refusal for any action is about who: an envelope whose actor kind is not pippa is refused with "this door is Pippa's; Dad's own screens write through their own routes." Dad does not need the door; he has the menu, his shortcuts and Settings.

/health Is the Exception That Proves It

The health route answers before any worker starts, without an actor, and it answers even when Tinker's saved state is held — "so a caller reads the reason instead of a refused connection." It reveals only whether the engine is up, the member name, the product version and the load problem if there is one. It changes nothing and lists nothing, which is what makes it safe to answer anyone the listener already admitted.

Code

Tinker's handler: refuse proxies, route three things·swift
if request.head.headers["x-forwarded-for"] != nil {
  throw HTTPListenerRefusal(403, "Only local callers may use this door.")
}
if request.head.path == "/health", request.head.method == "GET" {
  return try await .json(200, self.health())
}
let prefix = "/api/app-actions/"
let operation: String
if request.head.path == "/api/app-actions", request.head.method == "GET" { operation = "manifest" }
else if request.head.path.hasPrefix(prefix), request.head.method == "POST" { operation = "invoke" }
else { throw HTTPListenerRefusal(404, "Unknown route.") }
The kit's caller rule, and the actor rule·python
import ipaddress

def trusted_caller(request) -> bool:
    """R1: loopback, no proxy hop, no browser."""
    host = getattr(getattr(request, "client", None), "host", "") or ""
    try:
        loopback = ipaddress.ip_address(host).is_loopback
    except ValueError:
        return False
    headers = getattr(request, "headers", {}) or {}
    return loopback and not headers.get("x-forwarded-for") and not headers.get("origin")

# ...and, per action, before any handler runs (kit refusal, paraphrased):
# if actor.kind != "pippa":
#     refuse 403 "this door is Pippa's; Dad's own screens write through their own routes"
Probing the door on its own Mac (port from the family registry)·bash
PORT=4900   # Tinker's per-Mac loopback port in the family registry

# Health: answers without an actor.
curl -s "http://127.0.0.1:$PORT/health"

# Refused at the listener: looks like a browser.
curl -s -H 'Origin: https://example.com' "http://127.0.0.1:$PORT/api/app-actions"

# Refused by Tinker's handler: came through a proxy.
curl -s -H 'X-Forwarded-For: 203.0.113.7' "http://127.0.0.1:$PORT/api/app-actions"

External links

Exercise

For each of these, name the layer that refuses it first, or say that it is admitted: (a) a curl from another Mac to this Mac's LAN address and Tinker's port; (b) a fetch() from a page on example.com open in Safari on this Mac; (c) a local script posting preset.toggle with a request_id and actor kind 'dad'; (d) Pippa's forward arriving with Host 127.0.0.1 and a different port; (e) a request whose Host is 'localhost' but with a different port.
Hint
(a) never reaches a handler — the listener is bound to loopback. (b) the listener refuses the Origin. (c) passes caller checks but the kit refuses the actor kind. (d) is admitted on purpose by the 127.0.0.1 allow-list. (e) is refused by the Host check: only the literal 127.0.0.1 is allowed at any port; 'localhost' must carry the listener's own port.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.