feat(map): add cache-first Cesium gateway and AMD egress

This commit is contained in:
Codex
2026-07-16 02:23:56 +03:00
parent 59e9c92415
commit 7b55d887bc
31 changed files with 4956 additions and 167 deletions
+3
View File
@@ -0,0 +1,3 @@
# Stable LAN address of the AMD Windows host. This is not the OpenVPN address.
AMD_CONNECTOR_BIND_IP=172.22.0.183
AMD_CONNECTOR_PORT=8791
+2
View File
@@ -0,0 +1,2 @@
.env
runtime/
+12
View File
@@ -0,0 +1,12 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
COPY server.mjs ./
USER node
EXPOSE 8791
CMD ["node", "server.mjs"]
+84
View File
@@ -0,0 +1,84 @@
# DC AMD Connector
DC AMD Connector runs on the adjacent AMD Windows machine. It is a restricted
HTTP `CONNECT` proxy for the NODE.DC map path:
```text
NAS DC AMD Proxy -> 172.22.0.183:8791 -> AMD VPN -> Cesium / Bing
```
It is not a general-purpose proxy:
- only authenticated `CONNECT` requests are accepted;
- only port `443` is accepted;
- only Cesium Ion and required Bing imagery hosts are accepted;
- ordinary HTTP proxy requests are rejected;
- the Ion bearer token stays inside the end-to-end TLS connection from NAS to
Cesium and is not processed or logged by this connector.
The Windows host owns the VPN. The NAS uses only the stable LAN address in
`.env`, never an OpenVPN adapter address or a changing public VPN exit IP.
## First run on the AMD machine
1. Start Docker Desktop and wait for its engine to be running.
2. Open **PowerShell as Administrator** and run `install.ps1` from this
package folder. It verifies source checksums, copies the package to
`C:\\NODEDC\\dc-amd-connector`, creates a local random access token without
printing it, binds port `8791` only to `172.22.0.183`, scopes Windows
Firewall to NAS `172.22.0.222`, constrains the container to one CPU, 256 MB,
64 processes and 4096 file descriptors, and starts the container. It also
adds a launcher to the current user's Windows Startup folder. At the next
user sign-in it uses the Docker Desktop CLI when available, otherwise its
installed application; the `unless-stopped` restart policy then restores
this container.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\\install.ps1
```
3. Do not send the runtime secret in chat or put it into a source file. The
next NAS patch will provision the same value in a root-owned secret file
through an operator-only transfer.
Docker Desktop on the WSL 2 backend is a desktop application, not an
unattended Windows boot daemon. Thus this recovery guarantee starts after the
configured Windows user signs in. Before a sign-in, the map must use its
offline/tile-cache path. No Windows route, VPN, DNS, proxy setting or other
application's traffic is modified.
## Operations and migration
Run these commands from an elevated PowerShell window:
```powershell
cd C:\\NODEDC\\dc-amd-connector
docker compose ps
docker compose logs --tail 100
docker compose restart
```
`docker compose down` is an intentional stop and removes the container; restore
it with `docker compose up -d`. The auto-start launcher is removed by
`uninstall.ps1`.
To move to a different adjacent Windows host: install this same package there
with its stable LAN address (`-BindAddress`) and verify its VPN tunnel first.
It creates a fresh local access secret. Only then may the separate NAS pairing
patch be changed to the new host; never run two active pairings against the
same NAS endpoint.
## Verification
The connector health check uses the local runtime secret inside the container.
It is expected to become `healthy` without making any external request. The
following local check proves a permitted TLS tunnel through the current AMD VPN
without revealing the access token:
```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`. If the AMD VPN is
off, this test must fail; it must never silently move live Cesium traffic to
the NAS.
+10
View File
@@ -0,0 +1,10 @@
36283c8f926cd5a2fdfec0adda20e1d30fefb47e8fb6f4c6a41ebdfdb8034357 ./.env.example
7e4d35564197224edd4899aae9525c5b3c0c904371bb2e635dcef7950bc776cd ./.gitignore
3d7464d3a7b97b3b9be1036dcf77a9fd31cd841043a64069870e001a7f526a7b ./Dockerfile
c967a21fbb804a61e467fd207702f328ccc5ed933beaf98a476490f033f6dfa4 ./README.md
08708bd47ddf85dadda552eeb2ac1bc8f8d1e5d119305ef41bb0327b4dbac36b ./VERSION
b00bbf2ec6b56d834b04543d4e65bda7eff97296d4d255df48c28d71369a3f0e ./docker-compose.yml
608e3c75b69567376019b6f9fdf274a5cf9afecdde07348b6f02c89b688e672a ./install.ps1
33d4c94fdec3b47bba25039306423dd9dea50d4b36014a5a34768e0a6a7ba174 ./package.json
e08d0f33b4749289a5adf897b8a4ef876d3ebad9aa377d992e9d33b1060fe698 ./server.mjs
f9bb0a9d862e9f78fd533eeb2a0750108392d73f6173aec2ccc8053120638e01 ./uninstall.ps1
+1
View File
@@ -0,0 +1 @@
dc-amd-connector-20260715-004
@@ -0,0 +1,47 @@
services:
dc-amd-connector:
build:
context: .
dockerfile: Dockerfile
container_name: dc-amd-connector
image: nodedc/dc-amd-connector:local
restart: unless-stopped
# This is a byte-forwarding gateway, not a map renderer. These are circuit
# breakers for the shared workstation, not normal throughput limits.
cpus: "1.00"
mem_limit: 256m
pids_limit: 64
ulimits:
nofile:
soft: 4096
hard: 4096
env_file:
- .env
environment:
PORT: "8791"
AMD_CONNECTOR_ACCESS_TOKEN_FILE: /run/dc-amd-secrets/connector-access
# Bind only to the stable LAN address of the AMD host, never all Windows
# interfaces and never the OpenVPN adapter address.
ports:
- "${AMD_CONNECTOR_BIND_IP:?set AMD_CONNECTOR_BIND_IP}:${AMD_CONNECTOR_PORT:-8791}:8791"
volumes:
- type: bind
source: ./runtime/connector-access
target: /run/dc-amd-secrets/connector-access
read_only: true
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=8m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
healthcheck:
test:
- CMD-SHELL
- >
node -e "const fs=require('fs'),http=require('http');const t=fs.readFileSync('/run/dc-amd-secrets/connector-access','utf8').trim();const r=http.get({host:'127.0.0.1',port:8791,path:'/healthz',headers:{'proxy-authorization':'Bearer '+t}},x=>process.exit(x.statusCode===200?0:1));r.on('error',()=>process.exit(1))"
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
+170
View File
@@ -0,0 +1,170 @@
[CmdletBinding()]
param(
[string]$Destination = 'C:\NODEDC\dc-amd-connector',
[string]$NasAddress = '172.22.0.222',
[string]$BindAddress = '172.22.0.183',
[int]$Port = 8791,
[bool]$EnableAutoStart = $true
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Require-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this installer from an elevated PowerShell window.'
}
}
function Test-PackageChecksums([string]$Root) {
$manifest = Join-Path $Root 'SHA256SUMS'
if (-not (Test-Path -LiteralPath $manifest -PathType Leaf)) {
throw "Checksum manifest not found: $manifest"
}
foreach ($line in Get-Content -LiteralPath $manifest) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
$parts = $line -split '\s{2,}', 2
if ($parts.Count -ne 2 -or $parts[0] -notmatch '^[a-f0-9]{64}$') {
throw "Invalid checksum line: $line"
}
$relative = $parts[1] -replace '^\./', ''
$file = Join-Path $Root $relative
$actual = (Get-FileHash -LiteralPath $file -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $parts[0]) { throw "Checksum mismatch: $relative" }
}
}
function Get-DockerDesktopStartCommand([string]$DockerCliPath) {
& $DockerCliPath desktop start --help 2>$null | Out-Null
if ($LASTEXITCODE -eq 0) {
return "`"$DockerCliPath`" desktop start --detach"
}
$dockerRoot = Split-Path (Split-Path (Split-Path $DockerCliPath -Parent) -Parent) -Parent
$candidates = @(
(Join-Path $dockerRoot 'Docker Desktop.exe'),
(Join-Path $env:ProgramFiles 'Docker\Docker\Docker Desktop.exe'),
(Join-Path $env:LOCALAPPDATA 'Programs\Docker\Docker\Docker Desktop.exe')
) | Select-Object -Unique
$desktopExe = $candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
if (-not $desktopExe) {
throw 'Unable to find Docker Desktop start command or Docker Desktop.exe for restart recovery.'
}
return "start `"`" `"$desktopExe`""
}
function New-DockerDesktopAutoStart([string]$StartCommand) {
$startup = [Environment]::GetFolderPath([Environment+SpecialFolder]::Startup)
if ([string]::IsNullOrWhiteSpace($startup)) { throw 'Unable to resolve the current user Startup folder.' }
$launcher = Join-Path $startup 'NODE.DC DC AMD Connector - Docker Desktop.cmd'
if (Test-Path -LiteralPath $launcher) {
throw "Docker Desktop auto-start launcher already exists: $launcher"
}
@(
'@echo off',
'rem NODE.DC DC AMD Connector - starts Docker Desktop after this user signs in.',
$StartCommand
) | Set-Content -LiteralPath $launcher -Encoding ascii
return $launcher
}
Require-Administrator
$source = Split-Path -Parent $PSCommandPath
if (-not (Test-Path -LiteralPath (Join-Path $source 'VERSION') -PathType Leaf)) {
throw 'Run install.ps1 from the DC AMD Connector package folder.'
}
Test-PackageChecksums $source
if (Test-Path -LiteralPath $Destination) {
throw "Destination already exists; refusing to overwrite: $Destination"
}
$null = [Net.IPAddress]::Parse($NasAddress)
$null = [Net.IPAddress]::Parse($BindAddress)
if ($Port -lt 1 -or $Port -gt 65535) {
throw "Port must be between 1 and 65535: $Port"
}
$dockerCli = (Get-Command docker -CommandType Application -ErrorAction Stop | Select-Object -First 1).Path
if ([string]::IsNullOrWhiteSpace($dockerCli) -or -not (Test-Path -LiteralPath $dockerCli -PathType Leaf)) {
throw 'Docker CLI executable could not be resolved.'
}
& $dockerCli version --format '{{.Server.Os}}/{{.Server.Arch}} {{.Server.Version}}' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw 'Docker Desktop engine is not available. Start it and wait for Engine running.'
}
$dockerDesktopStartCommand = Get-DockerDesktopStartCommand $dockerCli
$destinationCreated = $false
$firewallCreated = $false
$autoStartLauncher = $null
$parent = Split-Path -Parent $Destination
try {
New-Item -ItemType Directory -Force $parent | Out-Null
Copy-Item -LiteralPath $source -Destination $Destination -Recurse
$destinationCreated = $true
Test-PackageChecksums $Destination
Set-Location $Destination
New-Item -ItemType Directory -Force runtime | Out-Null
$tokenPath = Join-Path $Destination 'runtime\connector-access'
if (Test-Path -LiteralPath $tokenPath) { throw "Runtime token already exists: $tokenPath" }
$bytes = New-Object byte[] 48
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
try { $rng.GetBytes($bytes) } finally { $rng.Dispose() }
$token = [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
[IO.File]::WriteAllText($tokenPath, $token, [Text.UTF8Encoding]::new($false))
Copy-Item -LiteralPath (Join-Path $Destination '.env.example') -Destination (Join-Path $Destination '.env')
@(
"AMD_CONNECTOR_BIND_IP=$BindAddress",
"AMD_CONNECTOR_PORT=$Port"
) | Set-Content -LiteralPath (Join-Path $Destination '.env') -Encoding ascii
$ruleName = 'NODE.DC DC AMD Connector (NAS only)'
if (Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue) {
throw "Firewall rule already exists; refusing to replace: $ruleName"
}
New-NetFirewallRule -DisplayName $ruleName -Direction Inbound -Action Allow -Protocol TCP -Profile Any -LocalAddress $BindAddress -LocalPort $Port -RemoteAddress $NasAddress | Out-Null
$firewallCreated = $true
if ($EnableAutoStart) {
$autoStartLauncher = New-DockerDesktopAutoStart $dockerDesktopStartCommand
}
& $dockerCli compose up -d --build
$state = ''
foreach ($attempt in 1..30) {
$state = & $dockerCli inspect dc-amd-connector --format '{{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{end}}'
if ($LASTEXITCODE -ne 0) { throw 'Container inspection failed after Docker Compose apply.' }
if ($state -eq 'running health=healthy') { break }
Start-Sleep -Seconds 2
}
if ($state -ne 'running health=healthy') {
throw "Connector did not become healthy: $state"
}
Write-Host "DC AMD Connector installed: $state"
Write-Host "Bound to $BindAddress`:$Port; inbound firewall scope is NAS $NasAddress."
if ($autoStartLauncher) {
Write-Host "Docker Desktop will start at the next sign-in through: $autoStartLauncher"
}
Write-Host 'The connector access token remains only in runtime\connector-access. Do not print or send it in chat.'
}
catch {
if ($autoStartLauncher -and (Test-Path -LiteralPath $autoStartLauncher)) {
Remove-Item -LiteralPath $autoStartLauncher -Force -ErrorAction SilentlyContinue
}
if ($firewallCreated) {
Remove-NetFirewallRule -DisplayName 'NODE.DC DC AMD Connector (NAS only)' -ErrorAction SilentlyContinue
}
if ($destinationCreated -and (Test-Path -LiteralPath $Destination)) {
Remove-Item -LiteralPath $Destination -Recurse -Force -ErrorAction SilentlyContinue
}
throw
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "dc-amd-connector",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start": "node server.mjs"
}
}
+130
View File
@@ -0,0 +1,130 @@
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { connect } from "node:net";
import { timingSafeEqual } from "node:crypto";
const allowedHosts = new Set([
"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",
]);
const config = await readConfig();
const server = createServer((request, response) => {
if (!isAuthorized(request.headers["proxy-authorization"])) return writeJson(response, 401, { ok: false, error: "connector_unauthorized" }, { "proxy-authenticate": "Bearer" });
if (request.method === "GET" && request.url === "/healthz") return writeJson(response, 200, { ok: true, service: "dc-amd-connector", mode: "restricted-connect", allowedHosts: allowedHosts.size });
return writeJson(response, 405, { ok: false, error: "connect_only" });
});
server.on("connect", (request, clientSocket, head) => {
if (!isAuthorized(request.headers["proxy-authorization"])) return rejectTunnel(clientSocket, 407, "Proxy Authentication Required", { "Proxy-Authenticate": "Bearer" });
let target;
try {
target = parseConnectTarget(request.url || "");
} catch (error) {
log("warn", "connect_rejected", { reason: error.message });
return rejectTunnel(clientSocket, 403, "Forbidden");
}
const upstream = connect({ host: target.host, port: target.port });
let settled = false;
const timeout = setTimeout(() => upstream.destroy(new Error("upstream_connect_timeout")), config.connectTimeoutMs);
clientSocket.setTimeout(config.idleTimeoutMs, () => clientSocket.destroy());
upstream.setTimeout(config.idleTimeoutMs, () => upstream.destroy());
upstream.once("connect", () => {
settled = true;
clearTimeout(timeout);
clientSocket.write("HTTP/1.1 200 Connection Established\r\nProxy-Agent: dc-amd-connector\r\n\r\n");
if (head?.length) upstream.write(head);
clientSocket.pipe(upstream);
upstream.pipe(clientSocket);
log("info", "connect_established", { host: target.host, port: target.port });
});
upstream.once("error", (error) => {
clearTimeout(timeout);
if (!settled) {
log("warn", "connect_failed", { host: target.host, port: target.port, reason: sanitizeError(error) });
rejectTunnel(clientSocket, 502, "Bad Gateway");
}
});
clientSocket.once("error", () => upstream.destroy());
clientSocket.once("close", () => upstream.destroy());
upstream.once("close", () => clientSocket.destroy());
});
server.on("clientError", (_error, socket) => rejectTunnel(socket, 400, "Bad Request"));
server.listen(config.port, "0.0.0.0", () => log("info", "connector_started", { port: config.port, allowedHosts: allowedHosts.size }));
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => server.close(() => process.exit(0)));
async function readConfig() {
const port = parsePort(process.env.PORT, 8791);
const tokenFile = String(process.env.AMD_CONNECTOR_ACCESS_TOKEN_FILE || "/run/dc-amd-secrets/connector-access").trim();
const token = String(await readFile(tokenFile, "utf8")).trim();
if (!/^[A-Za-z0-9_-]{48,256}$/.test(token)) throw new Error("connector_access_token_invalid");
return {
port,
token: Buffer.from(token, "utf8"),
connectTimeoutMs: parseDuration(process.env.AMD_CONNECTOR_CONNECT_TIMEOUT_SECONDS, 20),
idleTimeoutMs: parseDuration(process.env.AMD_CONNECTOR_IDLE_TIMEOUT_SECONDS, 90),
};
}
function parsePort(raw, fallback) {
const value = Number(String(raw || fallback).trim());
if (!Number.isInteger(value) || value < 1024 || value > 65535) throw new Error("connector_port_invalid");
return value;
}
function parseDuration(raw, fallbackSeconds) {
const seconds = Number(String(raw || fallbackSeconds).trim());
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 600) throw new Error("connector_timeout_invalid");
return seconds * 1000;
}
function isAuthorized(rawHeader) {
const match = /^Bearer\s+([A-Za-z0-9_-]{48,256})$/i.exec(String(rawHeader || "").trim());
if (!match) return false;
const candidate = Buffer.from(match[1], "utf8");
return candidate.length === config.token.length && timingSafeEqual(candidate, config.token);
}
function parseConnectTarget(raw) {
const match = /^([A-Za-z0-9.-]{1,253}):(443)$/.exec(String(raw || "").trim());
if (!match) throw new Error("connect_target_invalid");
const host = match[1].toLowerCase();
if (!allowedHosts.has(host)) throw new Error("connect_target_not_allowed");
return { host, port: Number(match[2]) };
}
function rejectTunnel(socket, status, message, headers = {}) {
if (!socket || socket.destroyed) return;
const extra = Object.entries(headers).map(([name, value]) => `${name}: ${value}\r\n`).join("");
socket.end(`HTTP/1.1 ${status} ${message}\r\n${extra}Connection: close\r\nContent-Length: 0\r\n\r\n`);
}
function writeJson(response, status, body, headers = {}) {
const payload = JSON.stringify(body);
response.writeHead(status, {
"content-type": "application/json; charset=utf-8",
"content-length": Buffer.byteLength(payload),
"cache-control": "no-store",
...headers,
});
response.end(payload);
}
function sanitizeError(error) {
const message = String(error?.message || "connector_error");
return message.replace(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 120);
}
function log(level, event, fields = {}) {
console.log(JSON.stringify({ ts: new Date().toISOString(), level, event, ...fields }));
}
+30
View File
@@ -0,0 +1,30 @@
[CmdletBinding()]
param(
[string]$Destination = 'C:\NODEDC\dc-amd-connector'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Require-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Run this uninstaller from an elevated PowerShell window.'
}
}
Require-Administrator
$docker = Get-Command docker -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if ($docker -and (Test-Path -LiteralPath (Join-Path $Destination 'docker-compose.yml'))) {
Push-Location $Destination
try { & $docker.Path compose down --remove-orphans } finally { Pop-Location }
}
Remove-NetFirewallRule -DisplayName 'NODE.DC DC AMD Connector (NAS only)' -ErrorAction SilentlyContinue
$startup = [Environment]::GetFolderPath([Environment+SpecialFolder]::Startup)
$launcher = Join-Path $startup 'NODE.DC DC AMD Connector - Docker Desktop.cmd'
Remove-Item -LiteralPath $launcher -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $Destination -Recurse -Force -ErrorAction SilentlyContinue
Write-Host 'DC AMD Connector removed. Docker Desktop and unrelated containers were not changed.'