Tech

LAN-Direct Routing for Local Dashboards

Akshay Sarode
Direct answer

The dashboard probes 127.0.0.1:33120 on every API call. If the local daemon answers, the request stays on your local network. If the probe fails (you're off the network), the dashboard falls back to a Firebase-gated tunnel transparently. The user sees no difference except latency.

Most "control your machine from a browser" tools route every keystroke through a cloud relay. SSH-via-browser products. Cloud-IDE backends. The pattern: browser → CDN → cloud relay → tunnel back to your machine. Even when you're sitting two feet from the machine, the control path still leaves the room.

For a terminal — where every keystroke is interactive — that latency is what you're going to remember about the product. Keep local traffic local and the experience feels direct. Send it through a relay and the delay becomes part of the product.

The probe

The dashboard's network layer wraps every fetch:

async function api(path, init) {
  // 1. Try local
  try {
    const r = await fetch(`http://127.0.0.1:33120${path}`, {
      ...init,
      signal: AbortSignal.timeout(150),
    });
    if (r.ok) return r;
  } catch { /* fall through */ }

  // 2. Fall back to tunnel
  return fetch(`https://${machineId}.bernato.dev${path}`, init);
}

150ms timeout is the budget. If the local daemon is reachable, it answers in <5ms. If not, the connection refuses or times out fast on a healthy network. The tunnel takes over.

Why 127.0.0.1 from a browser at all?

The dashboard runs at bernato.dev (Firebase Hosting). When the dashboard JS calls fetch('http://127.0.0.1:33120/...'), the browser opens a connection from your laptop to your laptop's loopback. Mixed-content is the gotcha — HTTPS pages can't open HTTP connections in modern browsers without a CORS pre-flight that allows it.

The fix: the daemon serves CORS headers that explicitly allow https://bernato.dev as origin. The browser permits the cross-protocol fetch because the daemon is on the loopback (localhost is treated as secure context).

// daemon-side
w.Header().Set("Access-Control-Allow-Origin", "https://bernato.dev")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")

What changes in practice

Local routing avoids the cloud relay on the happy path. Same-machine loopback is the fastest path, same-LAN remote daemons are still direct, and the tunnel is the fallback for off-LAN control. Exact numbers depend on hardware, Wi-Fi, browser, and relay geography.

Why this matters for terminals

A PTY pushes a byte for every keystroke. The browser pushes one back over WebSocket. Round-trip happens on every character, so keeping local sessions local changes how the terminal feels.

Tools that proxy through the cloud can't get below 50ms unless they edge-relay near you. Even then, they're paying for the edge nodes. LAN-direct is free; it's the network you already have.

What about WebSocket?

Same probe pattern. The dashboard first mints a short-lived terminal ticket over authenticated HTTP, then opens ws://127.0.0.1:33120/v1/terminal?agentId=X&ticket=.... If loopback fails, it uses the tunnel URL with the same ticket flow. No long-lived bearer goes into the WebSocket URL.

What about ports?

33120 is fixed. The daemon binds 127.0.0.1:33120 exclusively (not 0.0.0.0) — even if your firewall is misconfigured, the daemon is unreachable from another LAN box. The "remote daemon over LAN" case (laptop dashboard, Pi daemon) goes over the tunnel, not directly. That's a deliberate tradeoff: simpler security model, less direct same-LAN-remote routing.

The probe failure mode

What if you're on coffee-shop Wi-Fi and the dashboard probes 127.0.0.1 first? It's still your laptop. The daemon either answers or doesn't. If it does (you have the daemon running on your laptop), great. If not, the timeout fires and the tunnel takes over. No user-visible state.

Edge case: the user has a different process bound to 127.0.0.1:33120. The probe gets back a non-Bernato response (or a 401 with the wrong shape) and we fall back. We check for a magic header in the daemon's response to disambiguate.

FAQ

Does this work in Firefox / Safari?

Yes. CORS for localhost is consistent across modern browsers. Safari is sometimes pickier about mixed-content; the daemon's CORS headers handle it.

Why not bind to 0.0.0.0 and use TLS?

Because then the daemon is reachable from any machine on the LAN, and the security model has to assume that. Binding to loopback only means 'one user per machine' is the simple, correct posture.

What if I want a phone on the same LAN to skip the tunnel?

It can't — phones can't connect to your laptop's loopback. The mobile web dashboard uses the tunnel by design. LAN-direct is dashboard-on-the-same-machine only.

How does the probe avoid burning startup time?

It runs in parallel with the tunnel-resolve step the first time. If loopback wins, we cancel the tunnel resolution. Subsequent calls in the same session skip the probe and use the cached choice for 30 seconds.