# 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/<host>:/srv/mirror/<host>: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/<host>/<path>`,
   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>/<hash>`.
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/`
```
