# Full client air-gap — DNS + TLS transparent mirror ## Summary Goal: a guest laptop with **zero internet** can install + launch the modded instance entirely from the LAN. The chosen architecture is a **transparent mirror**: dnsmasq points the real upstream hostnames at our host, Caddy serves byte-exact copies of the files at the same paths, and `tls internal` mints certs those hostnames validate against (guests trust the Caddy root CA once). The launcher is **unmodified** — it requests the real Mojang/Prism/NeoForge URLs; DNS just resolves them to us. This is cleaner than rewriting Prism-format metadata, and it makes the `10-assets-mirror.md` "Assets Server" field **redundant** (the launcher hits the real resources host, which now resolves to the LAN). `10` is kept as the lighter objects-only alternative. ## Decision record - **TLS for LAN party = Caddy `tls internal`** (not step-ca). Zero extra infra; Caddy is the local CA. step-ca stays a future-homelab reminder (see `02-caddy` open item). Guests import the Caddy root CA once. - **Full air-gap chosen** over pre-seed. ## Hosts to mirror (NeoForge 1.21.1 instance) | Host | Serves | Notes | |---|---|---| | `meta.prismlauncher.org` | Prism-format component meta (MC, LWJGL, NeoForge) | path `/v1/...` | | `piston-meta.mojang.com` | version manifest + version json | | | `piston-data.mojang.com` | client.jar, asset index, some objects | | | `libraries.minecraft.net` | vanilla MC libraries (maven layout) | | | `maven.neoforged.net` | NeoForge libraries | more mavens may appear → capture catches them | | `resources.download.minecraft.net` | asset objects (~400 MB+) | the bulk | **Not mirrored — auth runtime** (`session.minecraft.net`, `textures.minecraft.net`, `api.mojang.com`, `sessionserver.mojang.com`): these are redirected to **drasl** by the authlib-injector account already; leave them to drasl. ## How it works ``` guest laptop (LAN DNS = our dnsmasq, trusts Caddy root CA) │ resolves meta.prismlauncher.org, libraries.minecraft.net, … → HOST_LAN_IP ▼ dnsmasq ── spoof A-records ──► Caddy (tls internal, cert per host) └─ static file_server, byte-exact path mirror ``` ### dnsmasq (extends 01) Add explicit spoof records alongside the `*.${BASE_DOMAIN}` wildcard: ``` address=/meta.prismlauncher.org/${HOST_LAN_IP} address=/piston-meta.mojang.com/${HOST_LAN_IP} address=/piston-data.mojang.com/${HOST_LAN_IP} address=/libraries.minecraft.net/${HOST_LAN_IP} address=/maven.neoforged.net/${HOST_LAN_IP} address=/resources.download.minecraft.net/${HOST_LAN_IP} ``` > Only effective while the guest uses our dnsmasq as resolver. Fully reversible — > off-LAN the real hosts resolve normally. Keep this list in sync with whatever > the capture step (below) actually observed. ### Caddy (extends 02) One vhost per spoofed host, each rooted at its mirror tree, all `tls internal`: ``` piston-data.mojang.com, libraries.minecraft.net, resources.download.minecraft.net, meta.prismlauncher.org, piston-meta.mojang.com, maven.neoforged.net { tls internal root * /srv/mirror/{host} file_server } ``` (Practically: a small snippet per host, or Caddy on-demand TLS + a path-routed `map`. Keep explicit per-host for clarity.) Add each as a **network alias** on Caddy too, so the `minecraft`/tooling containers resolve them internally. Mount `./mirror/upstream/:/srv/mirror/:ro`. ## Capture (the actual work) — `tooling/mirror-airgap.sh` Byte-exact mirroring needs the exact URL set. Capture it once, online: 1. **Record**: launch the instance once with the launcher pointed at a logging forward-proxy (mitmproxy `--mode reverse`/transparent, or even Caddy as a logging reverse proxy). Collect every requested absolute URL. 2. **Fetch**: for each URL, download to `mirror/upstream//`, preserving path exactly. Verify SHA1 where the source json provides it (libraries, assets). Halt on mismatch. 3. **Assets shortcut**: the objects (resources.download…) can be filled directly from the asset index (same logic as `10-assets-mirror.md`) without proxy capture — reuse that loop into `mirror/upstream/resources.download.minecraft.net/<2hex>/`. 4. **Verify offline**: re-run a launch on a second machine with internet cut and DNS set to our dnsmasq + CA trusted. Fix any 404 → missing URL → add to fetch. > Cautious by design: the proxy-capture enumerates whatever hosts/paths are *truly* > hit (incl. unforeseen maven mirrors), so the mirror is complete rather than > guessed. Re-capture if MC/NeoForge versions change. ## Guest one-time setup (offline) 1. Set laptop DNS → our dnsmasq (LAN IP). (Often via DHCP/router, or manual.) 2. Import the **Caddy root CA** into the OS/browser trust store. 3. Install Fjord, add authlib-injector account (`auth.${BASE_DOMAIN}`), import pack, launch. All downloads resolve to the LAN mirror. No launcher API-field overrides needed in this architecture. ## Open / reminders - [ ] **Reminder (kept):** graduate `tls internal` → step-ca for the persistent homelab; revisit cert distribution then. - [ ] Confirm exact `meta.prismlauncher.org` path layout Fjord requests (fork may pin a different meta host/version). - [ ] Verify Caddy `tls internal` certs validate in Fjord's Qt network stack with the root CA imported (no extra pinning). - [ ] mitmproxy vs Caddy-log for the capture step — pick one. - [ ] CA-trust UX per OS (Windows/macOS/Linux) — document for guests. ## Tasks - [ ] Write `tooling/mirror-airgap.sh` (capture + fetch + sha1 verify + offline re-test) - [ ] Add spoof `address=` lines to `dnsmasq/dnsmasq.conf.tmpl` (01) - [ ] Add per-host `tls internal` vhosts + network aliases + mounts to Caddy (02) - [ ] Export Caddy root CA; build a guest CA-import mini-guide (link from landing page) - [ ] Run capture online for MC 1.21.1 + NeoForge 21.1.x; populate `mirror/upstream/` - [ ] Offline acceptance test on a clean machine (internet cut, LAN DNS, CA trusted) - [ ] `.gitignore` already covers `mirror/` ```