Office LAN ingress — Caddy routing to LAN machines by request header (Home Port Forward Hosting)

**Note** from Bead: Home Port Forward Hosting · [canonical source](https://redfish.acequia.io/guerin/.agents/7049c694-e45b-4116-a887-49e64885ac8e/2026-06-13/notes/04-office-lan-ingress.md) · session 2026-06-13 · discussion: Talk: Home Port Forward Hosting

**Date:** 2026-06-14 (filed under the 2026-06-13 session folder). Stephen: *"we should probably setup a caddy server for the office that routes to local lan machines based on request headers."* This is the [router-as-bead / ingress](https://redfish.acequia.io/guerin/.agents/82bd6fa4-4d5a-435b-9deb-ca930a82aa89/2026-06-13/notes/00-router-bead-brief.md) pattern at LAN scope — the office's front door — generalizing the existing single-machine Caddyfile (`wiki.harvardviz.live → :3000`, `stephenguerin.live → :3500`) to route across LAN machines.

## Topology (observed 2026-06-14) - **Caddy box (this machine, `my-claude-vscode`):** `192.168.1.19` — office LAN `192.168.1.0/24`, gateway `192.168.1.1`. Caddy running as a service; admin API live on `:2019` (live route edits possible). - **NordVPN is installed but OFF** (confirmed by Stephen 2026-06-14). The NordLynx `10.5.0.2` / OpenVPN `10.100.0.2` interfaces are dormant adapters, not routing anything; `api.ipify` returns the real office static `50.243.20.254`. **No VPN interference — inbound is fully viable.** - **Office has 5 static public IPs** — the block is `50.243.20.250–.254`: - `node.redfish.com → 50.243.20.250` (its `:3334` WebDAV currently **not answering** — box down/firewalled). - this Caddy machine **egresses via `50.243.20.254`** (one of the 5). - The other statics (`.251`–`.253`) presumably NAT to other office machines. - **⇒ No tunnel, no dyndns needed at the office.** Point a domain's A record at the static that NATs to this box and serve directly. (Roaming/home still needs a tunnel; the static-IP office does not.) - **Unknown (needs external vantage):** whether `.254` *inbound*-NATs to `192.168.1.19`. The internal self-probe to `.254:443` returned `000`, but that's a NAT-hairpin test (unreliable). Confirm by hitting `https://50.243.20.254/` from off-network (phone on cellular) before pointing DNS at it.

## The routing (the easy part) Caddy matches on `Host` (or any header via matchers) and `reverse_proxy`s to a LAN `IP:port`: ```caddyfile # Host-based (virtual-host) routing to LAN machines boxA.lan { tls internal reverse_proxy 192.168.1.20:3000 } boxB.lan { tls internal reverse_proxy 192.168.1.30:8080 } # Header-based (non-Host) routing on a single hostname — "based on request headers" hub.lan { tls internal @toA header X-Route a @toB header X-Route b reverse_proxy @toA 192.168.1.20:3000 reverse_proxy @toB 192.168.1.30:8080 reverse_proxy 192.168.1.19:3500 # default backend } ``` `names.json`-as-mount-table, realized in Caddy config: each `Host`/header → a LAN backend. This is the node's dispatch table for the office (the [§8-Q7 router](https://redfish.acequia.io/guerin/.agents/fe7fbaf5-4c47-43b6-be1b-2f6ca15e8bfa/2026-06-11/notes/bead-as-webdav-handler.md), in Caddy form).

## The three decisions that actually gate it Routing is trivial; these determine whether LAN clients can *use* it: 1. **Name resolution — how does a LAN client resolve `boxA.lan` → `192.168.1.19` (the Caddy box)?** Options: (a) a hosts-file entry on each client; (b) a small LAN DNS server / the router's DNS pointing the names at `.19`; (c) a real (sub)domain with a **wildcard `A` → 192.168.1.19** so any name resolves to Caddy (works only on the LAN); (d) mDNS `*.local`. Without this, the `Host` header never arrives. 2. **TLS — `tls internal` vs real certs.** `tls internal` = Caddy's own CA: zero external dependency, perfect for LAN, but browsers warn unless the Caddy root CA is trusted on each client (one-time install). Real certs need the **DNS-01 challenge** (a DNS-provider API token — outbound, works behind VPN/NAT) since there's no inbound for HTTP-01. For LAN-only, start with `tls internal`. 3. **Scope — LAN-only or also public?** LAN-only = the config above + resolution + TLS, done. Public = add an **outbound tunnel** (`cloudflared`) terminating at this Caddy, which then header-routes to LAN backends. The tunnel is the entrance; Caddy is the distributor. (Given the VPN + no forward, tunnel is the only public option.)

## Recommended shape ``` [LAN clients] --Host/header--> Caddy @192.168.1.19 (tls internal) --reverse_proxy--> 192.168.1.x backends [public] --(optional)--> cloudflared tunnel --> same Caddy --> backends ``` Additive to the existing Caddyfile (one Caddy instance hosts many site blocks); the current `stephenguerin.live`/`wiki` blocks stay. Deploy by appending blocks + `caddy reload` (or POST to the :2019 admin API). **Not deployed yet — needs the backend list + the three decisions.**

## What I need from Stephen to build it - The **LAN backends**: for each, `hostname → 192.168.1.x:port` (which machines, which services). - **Resolution** choice (hosts file / LAN DNS / wildcard A / mDNS). - **TLS** choice (`tls internal` to start, vs DNS-01 real certs). - **Scope**: LAN-only now, or also public (→ I set up `cloudflared`). - Optionally: header keys beyond `Host` you want to route on (`X-Route`, an agent-id header, etc.).

## References (bead cross-links) - Bead: Stephenguerin.live Router Bead · [canonical](https://redfish.acequia.io/guerin/.agents/82bd6fa4-4d5a-435b-9deb-ca930a82aa89/) - Bead: As Webdav Handler · [canonical](https://redfish.acequia.io/guerin/.agents/fe7fbaf5-4c47-43b6-be1b-2f6ca15e8bfa/)