771 lines
36 KiB
Markdown
771 lines
36 KiB
Markdown
# DC AMD Proxy — production operations and recovery canon
|
||
|
||
This document is the reproducible operations contract for the restricted
|
||
Cesium/Bing egress path through the neighbouring Windows AMD workstation. It
|
||
describes the implementation that is in source now. It is not a proposal for
|
||
turning the NAS or the workstation into a general-purpose proxy.
|
||
|
||
The two non-negotiable boundaries are:
|
||
|
||
1. the NAS keeps its own routes, DNS, VPN, Tailscale and Internet connectivity
|
||
unchanged;
|
||
2. only approved Cesium/Bing HTTPS traffic may leave through the AMD host.
|
||
|
||
Do not solve an incident by adding a system proxy, default route, transparent
|
||
NAT, browser-visible token, permissive CONNECT endpoint, `0.0.0.0` Windows
|
||
bind, or direct NAS provider fallback.
|
||
|
||
## 1. Canonical architecture
|
||
|
||
```text
|
||
Browser
|
||
-> Foundry BFF
|
||
-> Platform Map Gateway
|
||
|-- warm object: NAS live/offline cache -> browser
|
||
`-- cold/refresh object:
|
||
HTTP + x-proxy-token
|
||
-> NAS <NAS_LAN_IP>:<NAS_PROXY_PORT> dc-amd-proxy
|
||
-> authenticated HTTP CONNECT
|
||
-> AMD <AMD_LAN_IP>:<CONNECTOR_PORT> dc-amd-connector
|
||
-> AMD/Docker DNS
|
||
-> Windows VPN/default egress route
|
||
-> approved Cesium/Bing host:443
|
||
<- end-to-end provider TLS is terminated by dc-amd-proxy
|
||
<- streamed provider response
|
||
-> browser and, when enabled, atomic NAS cache fill
|
||
```
|
||
|
||
The AMD connector is a byte-forwarding CONNECT boundary. It does not parse an
|
||
Ion bearer and cannot read the provider TLS stream. `dc-amd-proxy` establishes
|
||
TLS through that tunnel, validates the provider certificate and SNI, follows
|
||
only approved redirects, and returns the response to Map Gateway.
|
||
|
||
This distinction is operationally important:
|
||
|
||
- a **warm cache hit does not traverse AMD, VPN or Cesium**;
|
||
- a **cold miss or refresh does traverse AMD and depends on the workstation's
|
||
Docker Desktop, DNS, routing and VPN**;
|
||
- objects already cached on NAS must remain usable when AMD or its VPN is
|
||
unavailable;
|
||
- `dc-amd-proxy` is not in the browser-to-NAS cache read path.
|
||
|
||
### Current components and ownership
|
||
|
||
| Component | Runtime | Canonical state | Responsibility |
|
||
| --- | --- | --- | --- |
|
||
| Map Gateway | Synology Docker | `/volume1/docker/nodedc-platform/platform/services/map-gateway` | Provider credential scope, cache-first reads, streaming cache fill and private egress calls |
|
||
| `dc-amd-proxy` | Synology Docker | `/volume1/docker/dc-amd-proxy` | Request authorization, target allowlist, CONNECT/TLS pool, redirect/retry policy and telemetry |
|
||
| Proxy pair state | Synology bind mount | `/volume1/docker/dc-amd-proxy/runtime/connector-access` | Paired connector credential; persistent across container recreation |
|
||
| Map egress credential | Synology root-owned file | `/volume1/docker/nodedc-platform/secrets/map-egress-proxy-token` | Authenticates Map Gateway to `dc-amd-proxy` |
|
||
| `dc-amd-connector` | AMD Windows Docker Desktop | `C:\NODEDC\dc-amd-connector` by default | Restricted authenticated CONNECT to approved hosts on port 443 |
|
||
| Connector credential | AMD local file | `C:\NODEDC\dc-amd-connector\runtime\connector-access` | Generated locally once; never printed or stored in source |
|
||
| Docker Desktop launcher | Current Windows user's Startup folder | `NODE.DC DC AMD Connector - Docker Desktop.cmd` | Starts Docker Desktop after that user signs in |
|
||
|
||
Current source defaults are NAS listener port `8790`, AMD connector port
|
||
`8791`, eight active sockets and four idle sockets per origin. IP addresses
|
||
are deployment values and must be treated as configuration, not universal
|
||
constants.
|
||
|
||
## 2. Trust, secrets and traffic boundaries
|
||
|
||
There are three different credentials. They are not interchangeable.
|
||
|
||
1. The Cesium Ion credential belongs to Map Gateway provider configuration.
|
||
It must not be placed in Foundry browser code, an artifact or this proxy's
|
||
Compose file.
|
||
2. The Map egress credential authenticates Map Gateway to `dc-amd-proxy` using
|
||
`x-proxy-token`. The canonical runner synchronizes it into the root-owned
|
||
file mounted read-only into the two services.
|
||
3. The connector credential authenticates `dc-amd-proxy` to the Windows
|
||
connector using `Proxy-Authorization: Bearer ...`. The Windows installer
|
||
generates it locally; the one-time pair request stores the same value in
|
||
the NAS runtime bind mount.
|
||
|
||
No command in this runbook prints, copies to chat, or accepts a secret as a
|
||
command-line argument.
|
||
|
||
### Current runner dependency that must not be mistaken for a traffic route
|
||
|
||
The deploy runner currently seeds the shared Map egress credential from the
|
||
root-owned `PROXY_TOKEN` in `/volume1/docker/proxy-contur/.env`. Therefore a
|
||
valid, safely permissioned historical `proxy-contur` environment file is a
|
||
**deploy-time precondition** for `dc-amd-proxy` and Map Gateway applies.
|
||
|
||
This does **not** mean Cesium traffic passes through `proxy-contur`. Runtime
|
||
traffic goes directly from Map Gateway to `dc-amd-proxy`, then to the AMD
|
||
connector. Removing this legacy secret-seeding dependency is an explicit
|
||
evolution item later in this document.
|
||
|
||
### NAS network boundary
|
||
|
||
`dc-amd-proxy` uses `network_mode: host` so the one-time pair endpoint can see
|
||
the real AMD source address. Synology's published-port bridge path would hide
|
||
that source behind Docker NAT. Host mode does not make the service broad:
|
||
|
||
- the process binds only `<NAS_LAN_IP>:<NAS_PROXY_PORT>`;
|
||
- `/api/pair` accepts only `<AMD_LAN_IP>` and only while unpaired;
|
||
- `/proxy/cesium/fetch` accepts only `GET`/`HEAD`, requires the private Map
|
||
egress credential and validates the complete target URL;
|
||
- `/healthz` and `/status` expose safe state/counters but no credentials or
|
||
target URLs;
|
||
- the container is read-only, drops all capabilities, enables
|
||
`no-new-privileges`, and has only a small no-exec tmpfs plus the two explicit
|
||
bind mounts.
|
||
|
||
The current Map Gateway URL is a NAS-LAN address, not a public hostname and
|
||
not a Docker service discovery name.
|
||
|
||
### Windows network boundary
|
||
|
||
The connector publishes only `<AMD_LAN_IP>:<CONNECTOR_PORT>`. The installer
|
||
creates one inbound Windows Firewall rule with all of these restrictions:
|
||
|
||
- TCP only;
|
||
- local address `<AMD_LAN_IP>`;
|
||
- local port `<CONNECTOR_PORT>`;
|
||
- remote address `<NAS_LAN_IP>`;
|
||
- no bind to the VPN adapter and no bind to all interfaces.
|
||
|
||
The connector accepts only authenticated `CONNECT <allowed-host>:443`.
|
||
Ordinary forward-proxy HTTP methods, other ports, IP literals and arbitrary
|
||
hosts are rejected. Its container is read-only, drops capabilities and is
|
||
bounded to one CPU, 256 MB memory, 64 processes and 4096 file descriptors.
|
||
These are circuit breakers for a shared production workstation, not bandwidth
|
||
throttles.
|
||
|
||
The installer does not change the Windows default route, DNS, system proxy,
|
||
VPN configuration or unrelated firewall rules. The connector's DNS lookup and
|
||
TCP connection occur inside Docker Desktop on the AMD host and therefore use
|
||
the egress path exposed to Docker by Windows/VPN.
|
||
|
||
### Exact provider allowlist
|
||
|
||
Both NAS proxy and Windows connector currently allow only:
|
||
|
||
- `api.cesium.com`
|
||
- `assets.ion.cesium.com`
|
||
- `dev.virtualearth.net`
|
||
- `ecn.t0.tiles.virtualearth.net`
|
||
- `ecn.t1.tiles.virtualearth.net`
|
||
- `ecn.t2.tiles.virtualearth.net`
|
||
- `ecn.t3.tiles.virtualearth.net`
|
||
|
||
Adding a provider hostname requires one reviewed change to both allowlists,
|
||
updated smoke coverage, a new Windows connector package, a new NAS app-overlay
|
||
artifact and the normal plan/apply acceptance. Never add a wildcard such as
|
||
`*.cesium.com` or `*.virtualearth.net`.
|
||
|
||
## 3. Request lifecycle
|
||
|
||
For a cache miss, the sequence is:
|
||
|
||
1. Map Gateway checks the NAS cache before acquiring provider credentials or
|
||
contacting egress.
|
||
2. Map Gateway calls
|
||
`http://<NAS_LAN_IP>:<NAS_PROXY_PORT>/proxy/cesium/fetch?url=<encoded-url>`
|
||
and authenticates with the private Map egress credential.
|
||
3. `dc-amd-proxy` validates method, URL scheme, host, port and credentials.
|
||
4. A per-origin HTTP/1.1 keep-alive agent either reuses an existing TLS tunnel,
|
||
opens a new one, or queues behind the bounded active-socket limit.
|
||
5. For a new connection, the NAS opens an authenticated CONNECT request to
|
||
`<AMD_LAN_IP>:<CONNECTOR_PORT>`.
|
||
6. The connector validates its credential, exact hostname and port 443, then
|
||
resolves/connects through the AMD network/VPN.
|
||
7. The NAS completes TLS over the byte tunnel and validates the provider
|
||
certificate.
|
||
8. Response headers and body stream back to Map Gateway. Client cancellation
|
||
propagates through queue, CONNECT, TLS, header wait, redirect drain, retry
|
||
and response streaming.
|
||
9. Map Gateway can stream the first bytes to the caller while its independent
|
||
branch completes an atomic cache fill. A failed/partial fill must not become
|
||
a valid cache object.
|
||
|
||
Provider headers forwarded upstream are deliberately narrow: `Accept`,
|
||
`Range`, `If-None-Match`, `If-Modified-Since`, scoped provider authorization
|
||
and the Map referer. `Accept-Encoding` is set to `identity`. Hop-by-hop headers
|
||
are not forwarded back downstream.
|
||
|
||
### Credential scope across redirects
|
||
|
||
- same-origin redirects retain the provider `Authorization` header;
|
||
- cross-origin redirects remove `Authorization` before the next request;
|
||
- every redirect target is parsed again through the exact HTTPS/443/host
|
||
allowlist;
|
||
- redirect bodies are drained with abort and idle-timeout protection;
|
||
- the chain is bounded and terminates with
|
||
`amd_upstream_redirect_limit` rather than looping indefinitely.
|
||
|
||
This is why the Ion API bearer cannot leak when the API redirects to the
|
||
assets or Bing origin.
|
||
|
||
### Pool, retry and timeout policy
|
||
|
||
| Control | Current default | Meaning |
|
||
| --- | ---: | --- |
|
||
| Active sockets | 8 per origin | Maximum concurrent provider tunnels for each approved origin |
|
||
| Idle sockets | 4 per origin | Reusable warm TLS tunnels retained for each origin |
|
||
| TCP keep-alive initial delay | 30 seconds | Socket keep-alive probe setting; actual idle lifetime is also controlled by provider/server socket closure |
|
||
| Connector/TLS timeout | 20 seconds | Bounds NAS-to-AMD CONNECT and provider TLS setup |
|
||
| Upstream header timeout | 30 seconds | Bounds one attempt until provider response headers |
|
||
| Response/redirect body idle timeout | 30 seconds | Resets on body progress; does not cap a healthy long stream |
|
||
| Slow request threshold | 2 seconds | Emits safe slow-request telemetry; does not cancel the request |
|
||
| Windows connector upstream connect timeout | 20 seconds | Bounds AMD-to-provider TCP connect |
|
||
| Windows connector tunnel idle timeout | 90 seconds | Closes a tunnel with no traffic |
|
||
|
||
The NAS duration values accept 1–120 seconds and pool sizes accept 1–32. Do
|
||
not increase them to hide a route/VPN problem: first interpret queue,
|
||
connection, TTFB and body timings separately.
|
||
|
||
There is at most one transport retry. It is allowed only when all conditions
|
||
are true:
|
||
|
||
- method is idempotent `GET` or `HEAD`;
|
||
- the failed attempt used a previously reused keep-alive socket;
|
||
- failure is `ECONNRESET`, `EPIPE`, `ETIMEDOUT` or `ECONNABORTED`;
|
||
- the request has not already retried and the client has not aborted.
|
||
|
||
A failure on a newly opened socket is not blindly replayed. A retry may open a
|
||
replacement socket after the stale reused socket is discarded. Both attempts
|
||
are included in telemetry and terminal accounting.
|
||
|
||
### Runtime configuration reference
|
||
|
||
All changes to these values require a reviewed Compose/source artifact; do not
|
||
inject an ad-hoc live container environment.
|
||
|
||
| NAS proxy variable | Default | Contract |
|
||
| --- | --- | --- |
|
||
| `PORT` | `8790` | Internal/listener port |
|
||
| `DC_AMD_PROXY_BIND_ADDRESS` | deployment NAS LAN IP | Exact listener address; IPv4 only |
|
||
| `DC_AMD_CONNECTOR_HOST` | deployment AMD LAN IP | Stable connector LAN address; IPv4 only |
|
||
| `DC_AMD_CONNECTOR_PORT` | `8791` | Windows published connector port |
|
||
| `DC_AMD_PAIR_ALLOWED_SOURCE` | deployment AMD LAN IP | Only source allowed to perform first pair |
|
||
| `DC_AMD_CONNECTOR_TOKEN_FILE` | `/var/lib/dc-amd-proxy/connector-access` | Persistent paired connector state |
|
||
| `DC_AMD_MAP_EGRESS_TOKEN_FILE` | `/run/nodedc-secrets/map-egress-proxy-token` | Read-only Map Gateway authentication file |
|
||
| `DC_AMD_CONNECT_TIMEOUT_SECONDS` | `20` | CONNECT and TLS setup timeout |
|
||
| `DC_AMD_UPSTREAM_TIMEOUT_SECONDS` | `30` | Per-attempt response-header timeout |
|
||
| `DC_AMD_BODY_IDLE_TIMEOUT_SECONDS` | `30` | Progress-based response/redirect body timeout |
|
||
| `DC_AMD_POOL_MAX_SOCKETS` | `8` | Active sockets per origin |
|
||
| `DC_AMD_POOL_MAX_FREE_SOCKETS` | `4` | Idle sockets per origin |
|
||
| `DC_AMD_SLOW_REQUEST_SECONDS` | `2` | Safe slow-event threshold |
|
||
|
||
The Windows Compose `.env` contains only `AMD_CONNECTOR_BIND_IP` and
|
||
`AMD_CONNECTOR_PORT`. The connector server also supports bounded
|
||
`AMD_CONNECTOR_CONNECT_TIMEOUT_SECONDS` (default 20) and
|
||
`AMD_CONNECTOR_IDLE_TIMEOUT_SECONDS` (default 90); changing them should be a
|
||
versioned connector-package change.
|
||
|
||
## 4. Reproducible initial installation
|
||
|
||
Replace every angle-bracket placeholder before executing a command. Do not
|
||
paste a credential into any placeholder.
|
||
|
||
### 4.1 Preconditions
|
||
|
||
- AMD workstation has a stable LAN address reserved in DHCP or statically
|
||
configured.
|
||
- NAS and AMD can reach each other on the trusted LAN.
|
||
- Docker Desktop is installed and its Linux engine is running.
|
||
- The intended Windows user can sign in after a reboot.
|
||
- The VPN is connected and exposes its route to Docker Desktop workloads.
|
||
- The reviewed Windows connector package contains a valid `SHA256SUMS`.
|
||
- The canonical NAS runner is installed at
|
||
`/usr/local/sbin/nodedc-deploy`.
|
||
- No other host is using the selected connector LAN address/port.
|
||
|
||
### 4.2 Install the Windows connector
|
||
|
||
Extract the reviewed connector package to a temporary local folder. Open
|
||
PowerShell **as Administrator**, enter that folder and run:
|
||
|
||
```powershell
|
||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\install.ps1" `
|
||
-Destination "C:\NODEDC\dc-amd-connector" `
|
||
-NasAddress "<NAS_LAN_IP>" `
|
||
-BindAddress "<AMD_LAN_IP>" `
|
||
-Port <CONNECTOR_PORT> `
|
||
-EnableAutoStart $true
|
||
```
|
||
|
||
The installer is fail-closed. It verifies `SHA256SUMS`, refuses to overwrite an
|
||
existing destination or firewall rule, generates a random connector credential
|
||
without printing it, builds/starts the container, waits for `healthy`, creates
|
||
the NAS-only firewall rule and writes the current-user Docker Desktop Startup
|
||
launcher. On failure it removes only the state it created.
|
||
|
||
Verify locally without displaying the credential:
|
||
|
||
```powershell
|
||
Set-Location "C:\NODEDC\dc-amd-connector"
|
||
docker compose ps
|
||
docker inspect dc-amd-connector --format '{{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{end}} restart={{.HostConfig.RestartPolicy.Name}}'
|
||
docker compose logs --tail 100
|
||
```
|
||
|
||
Expected state is `running health=healthy` and restart policy
|
||
`unless-stopped`.
|
||
|
||
This safe CONNECT probe reads the credential only inside the container and
|
||
prints only the HTTP status line:
|
||
|
||
```powershell
|
||
docker exec dc-amd-connector node -e "const fs=require('fs'),net=require('net');const t=fs.readFileSync('/run/dc-amd-secrets/connector-access','utf8').trim();const s=net.connect(8791,'127.0.0.1',()=>s.write('CONNECT api.cesium.com:443 HTTP/1.1\r\nHost: api.cesium.com:443\r\nProxy-Authorization: Bearer '+t+'\r\n\r\n'));s.once('data',d=>{const v=d.toString('ascii');console.log(v.split('\r\n')[0]);s.destroy();process.exit(v.startsWith('HTTP/1.1 200')?0:1)});s.on('error',e=>{console.error(e.message);process.exit(1)})"
|
||
```
|
||
|
||
Expected output is `HTTP/1.1 200 Connection Established`. This proves that the
|
||
connector can currently reach the API host; it does not validate an Ion token
|
||
or prove the VPN exit country.
|
||
|
||
### 4.3 Build and transfer the NAS artifact
|
||
|
||
From a clean `platform` repository checkout on the trusted build Mac:
|
||
|
||
```bash
|
||
node infra/deploy-runner/build-dc-amd-proxy-artifact.mjs \
|
||
dc-amd-proxy-<CHANGE_SLUG>-<YYYYMMDD>-<NNN>
|
||
|
||
cd infra/deploy-artifacts
|
||
shasum -a 256 nodedc-dc-amd-proxy-<CHANGE_SLUG>-<YYYYMMDD>-<NNN>.tgz \
|
||
> nodedc-dc-amd-proxy-<CHANGE_SLUG>-<YYYYMMDD>-<NNN>.tgz.sha256
|
||
```
|
||
|
||
Copy exactly the `.tgz` and `.tgz.sha256` through SMB into:
|
||
|
||
```text
|
||
/volume1/docker/nodedc-deploy/inbox
|
||
```
|
||
|
||
The builder includes only `Dockerfile`, `README.md`, `docker-compose.yml`,
|
||
`package.json` and `server.mjs`. Runtime, `.env`, credentials, logs, Windows
|
||
packages and this operations document are not deploy payload members.
|
||
|
||
### 4.4 Canonical NAS plan/apply
|
||
|
||
On Synology, from the canonical inbox:
|
||
|
||
```bash
|
||
cd /volume1/docker/nodedc-deploy/inbox
|
||
|
||
sha256sum -c \
|
||
nodedc-dc-amd-proxy-<CHANGE_SLUG>-<YYYYMMDD>-<NNN>.tgz.sha256
|
||
|
||
sudo /usr/local/sbin/nodedc-deploy verify-install
|
||
|
||
sudo /usr/local/sbin/nodedc-deploy plan \
|
||
/volume1/docker/nodedc-deploy/inbox/nodedc-dc-amd-proxy-<CHANGE_SLUG>-<YYYYMMDD>-<NNN>.tgz
|
||
```
|
||
|
||
Accept the plan only when all are true:
|
||
|
||
- `component=dc-amd-proxy`
|
||
- `type=app-overlay`
|
||
- `payload_root=/volume1/docker/dc-amd-proxy`
|
||
- `services=dc-amd-proxy`
|
||
- `runtime_secret=runner-synced:.../map-egress-proxy-token`
|
||
- `runtime_state=runner-prepared:/volume1/docker/dc-amd-proxy/runtime`
|
||
- `state=new`
|
||
- the file list is exactly the five allowlisted source files.
|
||
|
||
Then apply the exact planned filename; never use “latest” discovery:
|
||
|
||
```bash
|
||
sudo /usr/local/sbin/nodedc-deploy apply \
|
||
/volume1/docker/nodedc-deploy/inbox/nodedc-dc-amd-proxy-<CHANGE_SLUG>-<YYYYMMDD>-<NNN>.tgz
|
||
```
|
||
|
||
The runner preserves/prepares the private pair-state directory, synchronizes
|
||
the Map egress credential, creates a source backup, builds with Compose,
|
||
force-recreates only `dc-amd-proxy`, waits for container health, moves the
|
||
artifact to `applied`, and records the result. A new service should report
|
||
`state=awaiting_pair`.
|
||
|
||
### 4.5 Pair Windows to NAS
|
||
|
||
Pair only after NAS status says `awaiting_pair`. Place the reviewed pairing
|
||
tool on the AMD machine and run it from an elevated PowerShell window:
|
||
|
||
```powershell
|
||
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||
-File ".\pair-dc-amd-proxy.ps1" `
|
||
-NasAddress "<NAS_LAN_IP>" `
|
||
-Port <NAS_PROXY_PORT>
|
||
```
|
||
|
||
Expected output:
|
||
|
||
```text
|
||
AMD connector paired with NAS. No secret value was displayed.
|
||
```
|
||
|
||
The pair call is a one-time HTTP LAN handoff with the Windows proxy explicitly
|
||
disabled. Its security boundary is the trusted LAN plus exact AMD source IP.
|
||
After the first successful pair, later attempts return `409 already_paired`.
|
||
|
||
Verify on NAS:
|
||
|
||
```bash
|
||
curl -fsS http://<NAS_LAN_IP>:<NAS_PROXY_PORT>/status
|
||
|
||
sudo /usr/local/bin/docker inspect dc-amd-proxy \
|
||
--format 'status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{end}} network={{.HostConfig.NetworkMode}} read_only={{.HostConfig.ReadonlyRootfs}} restart={{.HostConfig.RestartPolicy.Name}}'
|
||
```
|
||
|
||
Expected properties are `state=paired`, `forwarding=amd_connector_only`,
|
||
`directEgress=false`, container `healthy`, network `host`, read-only `true`,
|
||
and restart `unless-stopped`.
|
||
|
||
Finally verify one real Map Gateway request using the product health workflow.
|
||
Do not construct a shell command that reads or prints either private runtime
|
||
credential.
|
||
|
||
For a first installation, switch Map Gateway to
|
||
`http://<NAS_LAN_IP>:<NAS_PROXY_PORT>` only through a separate reviewed
|
||
Platform artifact and only after the proxy is paired and its tunnel test
|
||
passes. This separation keeps a failed connector bootstrap from breaking an
|
||
already-working provider route.
|
||
|
||
## 5. Restart and reboot behavior
|
||
|
||
### Synology
|
||
|
||
The NAS container has `restart: unless-stopped`. A Docker daemon or NAS reboot
|
||
restarts it. Pair state survives because `./runtime` is a bind mount outside
|
||
the container. The Map egress credential is a separate root-owned bind mount.
|
||
|
||
If an operator intentionally runs `docker compose down`, the container is
|
||
removed and restart policy cannot recreate it. Restore it from the canonical
|
||
source directory:
|
||
|
||
```bash
|
||
cd /volume1/docker/dc-amd-proxy
|
||
sudo /usr/local/bin/docker compose -p dc-amd-proxy up -d --build --no-deps dc-amd-proxy
|
||
```
|
||
|
||
Use that command only for recovery of the already-deployed version. Source
|
||
changes still require a runner artifact.
|
||
|
||
### Windows AMD host
|
||
|
||
Docker Desktop on the WSL 2 backend is not guaranteed to be an unattended
|
||
Windows boot service. The installed launcher starts it when the configured
|
||
Windows user signs in. After Docker Engine starts, `restart: unless-stopped`
|
||
restores the existing connector container.
|
||
|
||
Therefore the actual recovery sequence is:
|
||
|
||
1. Windows boots;
|
||
2. the configured user signs in;
|
||
3. Startup launches Docker Desktop;
|
||
4. Docker Engine becomes ready;
|
||
5. the existing connector container restarts;
|
||
6. the VPN must be connected and usable from Docker;
|
||
7. cold Cesium requests recover.
|
||
|
||
Before user sign-in, or while Docker/VPN is unavailable, live misses fail and
|
||
Map Gateway must rely on existing NAS cache. If the connector container was
|
||
removed with `docker compose down`, restart policy alone cannot recreate it;
|
||
run from elevated PowerShell:
|
||
|
||
```powershell
|
||
Set-Location "C:\NODEDC\dc-amd-connector"
|
||
docker compose up -d --build
|
||
```
|
||
|
||
## 6. Routine operations and observability
|
||
|
||
### NAS commands
|
||
|
||
```bash
|
||
curl -fsS http://<NAS_LAN_IP>:<NAS_PROXY_PORT>/status
|
||
|
||
sudo /usr/local/bin/docker inspect dc-amd-proxy \
|
||
--format 'status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{end}} restart={{.HostConfig.RestartPolicy.Name}}'
|
||
|
||
sudo /usr/local/bin/docker logs --tail 200 dc-amd-proxy
|
||
|
||
cd /volume1/docker/dc-amd-proxy
|
||
sudo /usr/local/bin/docker compose -p dc-amd-proxy ps
|
||
```
|
||
|
||
### Windows commands
|
||
|
||
```powershell
|
||
Set-Location "C:\NODEDC\dc-amd-connector"
|
||
docker compose ps
|
||
docker compose logs --tail 200
|
||
docker inspect dc-amd-connector --format '{{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{end}} restart={{.HostConfig.RestartPolicy.Name}}'
|
||
```
|
||
|
||
### What `/status` means
|
||
|
||
`/healthz` and `/status` currently return the same safe operational body. They
|
||
prove process/local state, not end-to-end provider reachability.
|
||
|
||
Important fields:
|
||
|
||
- `state`: `awaiting_pair` or `paired`;
|
||
- `forwarding`: disabled until paired, then `amd_connector_only`;
|
||
- `directEgress`: must always be `false`;
|
||
- `pool.activeSockets`, `idleSockets`, `queuedRequests`, `maxQueuedRequests`;
|
||
- `pool.byOrigin`: safe active/idle/queued counts per allowlisted origin;
|
||
- `metrics.requests`, `terminalRequests`, `inFlightRequests`;
|
||
- `completedResponses`, `failedRequests`, `clientAborts`, `streamFailures`;
|
||
- `openedTunnels`, `tunnelFailures`, `reusedSocketRequests`, `retries`;
|
||
- complete and partial byte counts;
|
||
- average/max queue, connection, TTFB, attempt, retry and total duration;
|
||
- per-host counters and the last short transport error.
|
||
|
||
At all times:
|
||
|
||
```text
|
||
requests = terminalRequests + inFlightRequests
|
||
```
|
||
|
||
At quiescence, `inFlightRequests` should return to zero. A violation indicates
|
||
a lifecycle/accounting defect. Counters are process-local and reset after a
|
||
container restart; pair state does not reset.
|
||
|
||
Interpret latency by stage:
|
||
|
||
| Symptom | Likely stage |
|
||
| --- | --- |
|
||
| High `queuedRequests` / `queueMs` | Per-origin eight-socket pool saturated or requests not terminating |
|
||
| High `connectionMs` / tunnel failures | NAS-to-AMD LAN, Windows connector, DNS, CONNECT or TLS setup |
|
||
| Low connection but high `ttfbMs` | VPN/provider latency or provider throttling |
|
||
| Low TTFB but high total duration | Slow/stalled response body or downstream backpressure |
|
||
| Rising `partialBytes` / `streamFailures` | Provider/VPN body died after headers |
|
||
| Rising `clientAborts` | Browser/navigation cancellation or caller timeout; not automatically an upstream failure |
|
||
| Low `reusedSocketRequests` with high `openedTunnels` | Keep-alive churn, route instability or origin fan-out |
|
||
| Retry spikes | Reused sockets are being reset; one bounded recovery is working but transport is unstable |
|
||
|
||
JSON logs intentionally contain event names, approved hostname, short error
|
||
code and numeric timing only. They must not include a full URL, query string,
|
||
Ion bearer, connector credential or Map egress credential. Normal fast tile
|
||
requests are not logged individually. Useful events include:
|
||
|
||
- `cesium_egress_tunnel_opened`
|
||
- `cesium_egress_reused_socket_retry`
|
||
- `cesium_egress_retry_completed`
|
||
- `cesium_egress_retry_failed`
|
||
- `cesium_egress_slow_or_failed`
|
||
- `cesium_egress_stream_failed`
|
||
- `cesium_egress_request_failed`
|
||
- `amd_connector_paired`
|
||
|
||
A warm Map Gateway cache hit should not increase `dc-amd-proxy` request
|
||
counters. If it does, audit the cache-first ordering before tuning the proxy.
|
||
|
||
## 7. Failure matrix
|
||
|
||
| Observable result | Probable cause | Safe action |
|
||
| --- | --- | --- |
|
||
| NAS status unavailable | Container stopped/unhealthy or wrong NAS bind address | Inspect container/Compose state and logs; do not change NAS routes |
|
||
| `state=awaiting_pair` | No durable connector pair state | Verify intended AMD host, then run the one-time pairing tool |
|
||
| Pair HTTP 403 `pair_source_not_allowed` | Request did not originate from configured AMD LAN IP | Verify stable AMD IP, NAS host-mode listener and no proxy/NAT in the pair call |
|
||
| Pair HTTP 409 `already_paired` | Pair state already exists | Do not overwrite it; use the controlled migration/rotation procedure |
|
||
| `map_egress_unauthorized` | Map Gateway and NAS proxy do not share the runner-synced Map egress credential | Re-run canonical preflight/deploy investigation; never copy a token into `.env` by hand |
|
||
| `amd_connector_not_paired` | NAS runtime pair file absent/invalid | Pair from the allowed AMD source; inspect ownership only through canonical runner checks |
|
||
| Connector refused/timeout | Windows off, no user sign-in, Docker stopped, firewall/IP mismatch or connector unhealthy | Restore Windows/Docker/connector; warm cache should continue |
|
||
| `amd_connector_rejected` | Connector credential mismatch or CONNECT rejected | Stop; do not retry pairing blindly. Treat as pair-state/migration incident |
|
||
| `cesium_target_not_allowed` | Provider redirected/requested an unreviewed host or wrong scheme/port | Capture hostname safely, review necessity, update both allowlists through source |
|
||
| TLS timeout/certificate error | VPN path, DNS, time, CA or provider interception problem | Verify AMD route/VPN and NAS container time/CA; never disable TLS verification in production |
|
||
| `amd_upstream_timeout` | No provider headers within attempt timeout | Compare connection vs TTFB metrics; verify VPN/provider rather than increasing timeout first |
|
||
| Body/redirect idle timeout | Stream stopped making progress | Check VPN packet loss/provider; partial object must not enter Map cache |
|
||
| Provider HTTP 401/403 | Ion token, asset permission, referer restriction or provider account issue | Diagnose Map Gateway/provider configuration; transport is functioning if status is passed through |
|
||
| Provider HTTP 429 | Provider rate/throughput policy | Reduce cold fan-out and use cache; do not add unbounded sockets/retries |
|
||
| Rendering survives but cold areas fail | Expected cache-first degradation while AMD/VPN is down | Restore egress; preserve existing cache and avoid destructive cache resets |
|
||
| Queue grows and never drains | Saturation, hung lifecycle or insufficient abort propagation | Inspect in-flight/accounting invariant, body idle errors and client aborts before changing pool size |
|
||
| AMD host rebooted and does not recover | No Windows sign-in, Startup launcher missing, Docker not ready, container removed or VPN disconnected | Follow the explicit Windows reboot sequence |
|
||
|
||
### Important VPN caveat
|
||
|
||
The connector trusts the route Docker Desktop receives from Windows. It does
|
||
not cryptographically prove that a request used a particular VPN or country.
|
||
If Windows allows direct Internet egress when the VPN is off, the connector
|
||
may use that direct AMD-host route. It will still never fall back to direct NAS
|
||
egress, but that is a different guarantee.
|
||
|
||
If VPN-only provider egress is mandatory, enforce it on the AMD host/VPN with
|
||
an outbound kill switch scoped to the connector workload/approved destinations
|
||
and test it with VPN on and off. The current installer creates only an inbound
|
||
NAS-only firewall rule; it does not install this outbound policy.
|
||
|
||
## 8. Migration to another AMD machine
|
||
|
||
Migration has two independent identities: stable LAN endpoint and connector
|
||
credential. A clean install on another machine creates a new credential, while
|
||
the current NAS pair endpoint is intentionally write-once. Changing only the
|
||
IP in Compose is therefore insufficient.
|
||
|
||
### Required zero-guesswork migration sequence
|
||
|
||
1. Keep the old connector active while preparing the replacement.
|
||
2. Assign the replacement a reserved `<NEW_AMD_LAN_IP>` and verify VPN/Docker
|
||
routing.
|
||
3. Install the reviewed connector package on the replacement using
|
||
`<NEW_AMD_LAN_IP>` and the same NAS address/connector port.
|
||
4. Verify local connector health and the safe CONNECT probe.
|
||
5. Create a reviewed source change that updates NAS connector host and allowed
|
||
pair source to `<NEW_AMD_LAN_IP>`.
|
||
6. Add a runner-supported, audited pair-rotation transition that can retire the
|
||
old NAS pair state and admit exactly one new pair. The transition must back
|
||
up state, never print either credential, fail closed and be idempotent.
|
||
7. Build, checksum, SMB-transfer, `plan`, and `apply` the exact versioned
|
||
artifact/runner change through the deploy canon.
|
||
8. Pair from the replacement machine, verify an end-to-end cold request and
|
||
confirm `directEgress=false` plus clean accounting.
|
||
9. Disable the old connector, then uninstall it only after acceptance.
|
||
|
||
The current implementation does **not yet provide step 6 as a first-class
|
||
runner operation**. Until it exists, a fresh-secret migration is not fully
|
||
canonical. Do not work around this by deleting
|
||
`/volume1/docker/dc-amd-proxy/runtime/connector-access`, copying the credential
|
||
through chat/SMB, weakening `/api/pair`, or running two active pair sources.
|
||
|
||
If an urgent migration is required before controlled rotation exists, open an
|
||
Ops change and implement/review that transition first. This limitation is
|
||
preferable to an undocumented secret reset.
|
||
|
||
To remove a retired connector from Windows after the new path is accepted:
|
||
|
||
```powershell
|
||
powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||
-File "C:\NODEDC\dc-amd-connector\uninstall.ps1" `
|
||
-Destination "C:\NODEDC\dc-amd-connector"
|
||
```
|
||
|
||
The uninstaller removes only this container, its NAS-only firewall rule,
|
||
Startup launcher and installation directory. It does not remove Docker Desktop
|
||
or unrelated containers.
|
||
|
||
## 9. Deploy and rollback canon
|
||
|
||
### Source/change acceptance before packaging
|
||
|
||
Run from the service directory in a clean checkout:
|
||
|
||
```bash
|
||
node --check server.mjs
|
||
npm run test:connection-pool
|
||
docker build --no-cache -t nodedc/dc-amd-proxy:<VERIFY_TAG> .
|
||
git status --short
|
||
```
|
||
|
||
The smoke suite must cover pool reuse/saturation, aborts in each lifecycle
|
||
stage, exact terminal accounting, one safe retry, body idle timeout, redirect
|
||
credential boundaries and absence of secret markers in logs/status.
|
||
|
||
Commit source and documentation together. Build an artifact from the commit,
|
||
not from an unknown dirty tree. The normal deployment sequence is:
|
||
|
||
```text
|
||
source review -> tests -> commit -> artifact -> checksum -> SMB inbox
|
||
-> verify-install -> exact plan -> human plan review -> exact apply
|
||
-> container health -> proxy status -> end-to-end Map acceptance
|
||
```
|
||
|
||
Never put `.env`, `runtime`, tokens, logs, backup archives, Windows package
|
||
state or shell hooks inside a `dc-amd-proxy` app-overlay.
|
||
|
||
### What the runner records
|
||
|
||
Before applying, the runner creates:
|
||
|
||
```text
|
||
/volume1/docker/nodedc-deploy/backups/<BACKUP_ID>/
|
||
```
|
||
|
||
with manifest, file lists and `source-before.tgz`. Successful artifacts move
|
||
to `/volume1/docker/nodedc-deploy/applied`; failed artifacts move to
|
||
`/volume1/docker/nodedc-deploy/failed`. Applied and failed JSONL state lives
|
||
under `/volume1/docker/nodedc-deploy/state`.
|
||
|
||
For this component the current runner does **not** perform generic automatic
|
||
source rollback after a failed post-copy build/health check. The backup is
|
||
evidence/recovery input, not permission for an improvised live extraction.
|
||
|
||
### Canonical forward rollback
|
||
|
||
Use a new patch ID and package the known-good service source from a separate
|
||
clean worktree. Do not reset or overwrite the active developer worktree:
|
||
|
||
```bash
|
||
git worktree add "../NODEDC-rollback" <KNOWN_GOOD_COMMIT>
|
||
cd "../NODEDC-rollback"
|
||
|
||
node infra/deploy-runner/build-dc-amd-proxy-artifact.mjs \
|
||
dc-amd-proxy-rollback-<YYYYMMDD>-<NNN>
|
||
|
||
cd infra/deploy-artifacts
|
||
shasum -a 256 nodedc-dc-amd-proxy-rollback-<YYYYMMDD>-<NNN>.tgz \
|
||
> nodedc-dc-amd-proxy-rollback-<YYYYMMDD>-<NNN>.tgz.sha256
|
||
```
|
||
|
||
Transfer the exact pair to the inbox and run the same checksum,
|
||
`verify-install`, exact `plan`, review and exact `apply` sequence. The runtime
|
||
pair-state directory remains outside the source overlay and must be preserved.
|
||
|
||
If the failed version changed a runtime contract, Compose contract or secret
|
||
format, stop and design an explicit rollback transition instead of assuming a
|
||
source-only forward rollback is safe.
|
||
|
||
## 10. Capacity and product behavior
|
||
|
||
The default eight active sockets are **per approved origin**, not global. A
|
||
Cesium session can use more than one origin, but every origin remains bounded.
|
||
Twenty users do not overwrite one another's transport state; they share the
|
||
pool and may queue. Cache objects are owned by Map Gateway, not by this proxy.
|
||
|
||
The proxy has no tile index, no cache eviction and no “do not overwrite cache”
|
||
switch. Those belong to Map Gateway. It streams bytes and records transport
|
||
telemetry. Consequently:
|
||
|
||
- proxy/VPN speed affects only cold/refresh acquisition;
|
||
- NAS cache read speed should be independent from AMD/VPN speed;
|
||
- increasing pool size cannot fix slow warm cache reads;
|
||
- cache fill concurrency and capacity policy must be tuned in Map Gateway;
|
||
- a browser reload should reuse NAS cache even if local browser cache is empty.
|
||
|
||
## 11. Known limitations and evolution path
|
||
|
||
The current path is production-usable but not yet a general infrastructure
|
||
egress product. Track these changes explicitly rather than accumulating
|
||
one-off live fixes:
|
||
|
||
1. **Controlled pair rotation/migration.** Add runner-owned one-time rotation,
|
||
revocation and rollback state; optionally support overlapping old/new
|
||
credentials for a bounded cutover window.
|
||
2. **Remove the legacy `proxy-contur` secret source.** Generate/manage a
|
||
dedicated Map egress credential as first-class runner state.
|
||
3. **VPN-route enforcement.** Add a tested Windows outbound kill switch or a
|
||
dedicated egress appliance so VPN-off cannot become direct AMD egress.
|
||
4. **Unattended host recovery.** Replace user-sign-in-dependent Docker Desktop
|
||
with a managed Windows service, signed installer or dedicated Linux egress
|
||
node if 24/7 cold-cache availability is required.
|
||
5. **Pair-channel hardening.** Replace source-IP-only HTTP pairing with a
|
||
short-lived nonce plus authenticated/encrypted handoff or mutual TLS.
|
||
6. **Two-level health.** Keep local `/healthz`, add a rate-limited synthetic
|
||
egress readiness check that proves DNS/CONNECT/TLS without consuming or
|
||
exposing the master Ion credential.
|
||
7. **Durable metrics.** Export the safe counters to the platform metrics stack;
|
||
process-local `/status` currently resets on restart.
|
||
8. **Central allowlist contract.** Generate identical proxy/connector host
|
||
policies from one reviewed provider contract and test that they cannot
|
||
drift.
|
||
9. **Circuit breaking and backoff.** Add per-origin fail-fast state for a dead
|
||
AMD/VPN path so many cold misses do not occupy all queues until timeout.
|
||
10. **High availability.** Define an explicit active/standby connector model,
|
||
health-based selection and credential isolation before adding a second
|
||
workstation.
|
||
11. **Signed Windows distribution/update.** Provide a versioned package builder,
|
||
signature verification and in-place upgrade/rollback rather than manual
|
||
archive handling.
|
||
12. **SLOs and load tests.** Establish cache-hit, cold-TTFB, queue-depth,
|
||
abort-rate and recovery-time objectives using realistic concurrent Map
|
||
Page sessions.
|
||
|
||
Until those changes land, preserve the narrow boundary: official allowlisted
|
||
HTTPS only, no direct NAS fallback, no global workstation proxy, no secret in
|
||
source, and every NAS mutation through a reviewed data-only artifact plus the
|
||
canonical runner.
|