diff --git a/installer/INSTALL.md b/installer/INSTALL.md index ef4bca6..2649952 100644 --- a/installer/INSTALL.md +++ b/installer/INSTALL.md @@ -325,20 +325,68 @@ Saving restarts the site's ingest client, so the change takes effect immediately AES-GCM encrypted at rest and **never returned to any client** — losing it means reading it back from `sidecar.toml` on the shard host, not from the website. +If the sidecar sits behind a reverse proxy, paste your **public** `https://` and `wss://` URLs +instead of the two the installer printed — it composes those from the sidecar's own bind address, +which knows nothing about what fronts it. Everything else on the page is unchanged. + ### If your website is on a different machine The sidecar binds `127.0.0.1:8080` by default, which is reachable only from the shard host. If your -website runs elsewhere, you must widen the bind — and then narrow the access: +website runs elsewhere, the recommended arrangement is a **TLS reverse proxy in front of the +sidecar** — this is a supported deployment and the one Runic Gateway itself runs, on a real domain +name. -1. Set `[web] bind` in `sidecar.toml` to `0.0.0.0:8080` (or a specific LAN address) and restart the - service. -2. **Firewall port 8080 to your website's address only.** The auth token is always on, but it - travels as a plain bearer token — the sidecar speaks HTTP, not HTTPS. -3. If the two hosts are not on a trusted network, put the sidecar behind a TLS reverse proxy or a - VPN/WireGuard link, and give the website the proxied `https://` / `wss://` URLs. +**Leave `[web] bind` on `127.0.0.1:8080`** and let the proxy be the only thing that talks to it. +Widening the bind and firewalling the port is the alternative, not the default (see below). -The `[shard] bind` line is a different matter: leave it on `127.0.0.1:7788`. That socket accepts -*inbound commands* to the game, and being loopback-only is what makes that safe. +Give the website the **proxied** URLs — `https://link.example.com` and +`wss://link.example.com/ws` — in place of the `http://` / `ws://` pair the installer prints. Those +values are the sidecar's own view of itself; the proxy is what the outside world sees. + +What the proxy must do: + +| Requirement | Why | +|---|---| +| **Forward the WebSocket upgrade** (`Upgrade` / `Connection` headers, HTTP/1.1 to the upstream) | `/ws` is the live event feed. Without it the site's REST calls work and events never arrive — a confusing half-working state. | +| **Pass request headers through unmodified** | Auth is `Authorization: Bearer` (or `X-Api-Key`), and the website sends `X-UOLink-Version`. A proxy that strips unknown headers turns into a `401`, and a stripped version header just silently skips the mismatch check. | +| **Do not buffer the WS connection, and allow long-lived ones** | The feed is idle between events. The sidecar sends a WebSocket **Ping every 30 s**, so a read timeout of 60 s or more is safe as it stands — but a proxy that buffers responses will hold events instead of streaming them. | +| **Do not log query strings** | The sidecar also accepts `?token=…` (for clients that cannot set headers). If anything in your stack uses that form, a default access-log format writes your auth token to disk on every request. | + +Nothing needs `X-Forwarded-For`: the sidecar never uses the client's IP for authorization, and the +browser IP that account provisioning cares about is supplied by the website in the request body. + +An nginx server block that satisfies all of the above: + +```nginx +server { + listen 443 ssl; + server_name link.example.com; + + # your certificate directives here + + location / { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; # "upgrade" for WS, "" otherwise + proxy_set_header Host $host; + proxy_buffering off; + proxy_read_timeout 300s; + } +} +``` + +(with the usual `map $http_upgrade $connection_upgrade { default upgrade; '' close; }` at `http` +level). Caddy and Traefik handle WebSocket upgrades automatically and need no equivalent stanza. + +**Without a proxy**, on a trusted network only: set `[web] bind` to `0.0.0.0:8080` or a specific LAN +address, restart the service, and **firewall the port to your website's address**. The auth token is +always required, but the sidecar speaks HTTP — on that path the token and every event cross the +network in the clear. Do not do this over the public internet. + +The `[shard] bind` line is a different matter entirely: leave it on `127.0.0.1:7788` and never proxy +it. That socket accepts *inbound commands* to the game, and being loopback-only is what makes that +safe. --- @@ -451,7 +499,9 @@ The report is also written to a file, so it survives the scrollback. | Shard boots clean but nothing reaches the site | The classic silent failure: ServUO ignores the script build's exit code and reloaded a **stale `Scripts.dll`**. Run `dotnet build Scripts/Scripts.csproj -c Release -p:Platform=x64` and read the errors it prints. | | `[bridge status` says `connected=False` | The sidecar is not listening on `127.0.0.1:7788`. Check the service is running, and that `[shard] bind` in `sidecar.toml` matches `Host`/`Port` in `Bridge.cfg`. | | `[bridge` is not a command | The plugin did not compile, or `Bridge.cfg` has the bridge disabled. See the row above. | -| Website says the shard is offline; `/health` is fine locally | The website cannot reach port 8080 — bind address or firewall. See [§5](#if-your-website-is-on-a-different-machine). Note that the site is *designed* to render normally with the shard offline, so this fails quietly by design. | +| Website says the shard is offline; `/health` is fine locally | The website cannot reach the sidecar — bind address, firewall, or proxy. See [§5](#if-your-website-is-on-a-different-machine). Note that the site is *designed* to render normally with the shard offline, so this fails quietly by design. | +| REST reads work but **no live events arrive** | The classic reverse-proxy symptom: the WebSocket upgrade is not being forwarded. Confirm the proxy sets `Upgrade`/`Connection` and speaks HTTP/1.1 upstream, and that the site's WebSocket URL is `wss://…/ws` — not `https://`. | +| The event feed connects, then drops every minute or two | A proxy read timeout below the sidecar's 30 s WebSocket ping interval, or response buffering. Raise the timeout and turn buffering off. | | Website logs `409` from the sidecar | Protocol mismatch: the number in Admin → Shard does not match the sidecar's. The sidecar rejects rather than mis-parsing. Set the field to what `/health` reports (`protocol`). If the *sidecar* and *overlay* disagree, you have a hand-assembled pair — reinstall from a bundle. | | `401` from the sidecar | Wrong or missing auth token. Read the live one back with `uo-link-sidecar --print-config --config `; do not retype it from a screenshot. | | A patch will not apply | Expected on a hand-modified shard. The base install is unaffected; you lose only the two features in [§4](#4-the-patch-tier-optional). Apply the hunks by hand if you want them. | diff --git a/installer/PLAN.md b/installer/PLAN.md index b0f4b91..753a042 100644 --- a/installer/PLAN.md +++ b/installer/PLAN.md @@ -358,11 +358,19 @@ Repo work that must land before an installer can exist. in §6's handoff has a non-interactive equivalent and an unattended install is expressible. - **A modified `Bridge.cfg` must survive an update** — see Phase 1, where this changes the sync rule inherited from `deploy.ps1`. - - **Remote-website deployments needed an answer.** `[web] bind` defaults to `127.0.0.1`, which - only works when the site runs on the shard host. The guide says to widen it, firewall it to - the website's address, and front it with TLS or a VPN off a trusted network — because the - token is always required but travels as a plain bearer token over HTTP. `[shard] bind` stays - on loopback, since that socket carries inbound commands *into* the game. + - **Remote-website deployments needed an answer, and it is a reverse proxy.** `[web] bind` + defaults to `127.0.0.1`, which only works when the site runs on the shard host. The guide's + recommended arrangement — already in production on a real domain — is to **leave the bind on + loopback** and put a TLS reverse proxy in front, giving the website the proxied `https://` / + `wss://` URLs in place of the pair the installer prints from the bind address. Four + requirements make that work and are stated with an nginx block that satisfies them: forward + the WebSocket upgrade (`/ws` is the whole live feed), pass headers through unmodified (auth is + `Authorization: Bearer`, and a stripped `X-UOLink-Version` silently skips the mismatch check), + do not buffer and allow long-lived connections (the sidecar pings every 30 s, so a ≥60 s read + timeout is safe), and do not log query strings (`?token=` is an accepted auth form). Widening + the bind and firewalling the port stays documented as the trusted-LAN alternative, not the + default, because on that path the token crosses the network in the clear. `[shard] bind` is + never proxied and never widened — that socket carries inbound commands *into* the game. ### Phase 1 — installer core @@ -482,6 +490,13 @@ Every value in that block except the host and the site URL comes from one `0.0.0.0`, which is not something to hand a website — so the installer composes the URLs from the host it detects or prompts for, rather than echoing the bind address. +**It does not attempt to detect a reverse proxy**, which is the recommended arrangement for a +website on another host (`INSTALL.md` §5). Nothing visible from the sidecar's side says what fronts +it, so guessing would produce a confidently wrong `https://` URL. The two printed URLs always +describe the sidecar itself, and the guide tells the operator to paste their public `https://` / +`wss://` pair instead when there is a proxy. `--host` accepting a full origin later is a cheap +improvement if this proves annoying in practice. + The installer prompts for the site URL only to build that link; it never contacts the website. A future "installer registers itself with the website" flow (claim code + authenticated endpoint) is explicitly **out of scope** — it is real backend work in a security-sensitive area and can be added