docs(installer): lead the remote-website case with a reverse proxy
A TLS reverse proxy in front of the sidecar is a supported deployment already running on a real domain, not the fallback the guide framed it as. Recommend it first, keep [web] bind on loopback in that arrangement, and demote widen-the- bind-and-firewall to the trusted-LAN alternative - on that path the token and every event cross the network in the clear. Adds the four things a proxy must actually do, checked against web.rs: forward the WebSocket upgrade (/ws is the entire live feed, and losing it leaves REST working with no events - a confusing half-working state); pass headers through unmodified (auth is Authorization: Bearer or X-Api-Key, and a stripped X-UOLink-Version silently skips the 409 mismatch check); no buffering and long-lived connections (the sidecar pings every 30s, so a 60s+ read timeout is safe as it stands); and no query-string logging, since ?token= is an accepted auth form. Nothing needs X-Forwarded-For - the sidecar never uses the client IP for authorization. Includes an nginx block that satisfies all four, and notes Caddy and Traefik need no equivalent. The website gets the proxied https:// / wss:// URLs, not the pair the installer prints: those are composed from the sidecar's own bind address, which knows nothing about what fronts it. PLAN records that the installer deliberately does not try to detect a proxy - nothing visible from the sidecar's side says what is in front of it, so guessing would print a confidently wrong URL. Two new troubleshooting rows for the symptoms this causes: REST works but no events (upgrade not forwarded), and a feed that drops every minute or two (read timeout under the ping interval, or buffering). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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 <path>`; 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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user