Adds platform/lab-router/ — a narrow service that runs on the Lab Host, authenticates the Gateway via mTLS, and resolves an opaque routing token to a local container purely from Docker label state (no Postgres access, no callback to the Provisioner). Replaces the Gateway's env.internal_ip assumption (only valid when Gateway and Lab share a Docker daemon) with a data-driven direct/router decision per lab_host (lab_hosts.routing_mode/router_endpoint, migration 004), resolved in one place (platform/gateway/src/lab-endpoint.js's labTarget()) that every proxy/fetch call site in server.js now goes through. Extends the existing Docker TLS CA (infra/scripts/docker-tls/generate-docker-tls.sh) with a second mTLS leaf pair (router-server/gateway-client) for this new data-plane connection, deployed by the docker-tls Ansible role into a separate directory from the control-plane pair. Firewall (rn-lab-host-01.rules) replaces the old speculative wide port-range allowance with one narrow rule: platform_ip -> the Router's port only. dev-local stays routing_mode: direct — zero behavior change for local dev, confirmed by the full existing regression suite passing unmodified.
74 lines
3.3 KiB
JavaScript
74 lines
3.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Fast, isolated unit test for resolve.js's token->container logic — no live Docker daemon, no
|
|
// TLS material, no network. Uses a fake dockerode-shaped client so this runs in milliseconds
|
|
// as part of every regression pass. The REAL cross-host proxying behavior (mTLS handshake,
|
|
// actual container resolution, HTTP+WS traffic) is covered by the two-host simulation harness
|
|
// instead — see infra/scripts/simulate-two-host/README.md and
|
|
// docs/PHASE4_1_PREPRODUCTION_HARDENING.md §6, not duplicated here.
|
|
|
|
const assert = require('assert');
|
|
const { resolveTarget, tokenFingerprint } = require('../src/resolve');
|
|
|
|
function fakeDocker(containers) {
|
|
return {
|
|
listContainers: async ({ filters }) => {
|
|
const parsed = JSON.parse(filters);
|
|
const wantLabel = parsed.label[0];
|
|
const wantStatus = parsed.status[0];
|
|
return containers.filter((c) => c.Labels && `${Object.keys(c.Labels)[0]}=${Object.values(c.Labels)[0]}` === wantLabel && c.State === wantStatus);
|
|
},
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const runningContainer = {
|
|
Labels: { 'rootnode.routing-token': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' },
|
|
State: 'running',
|
|
NetworkSettings: { Networks: { 'rootnode-lab-net-abc': { IPAddress: '172.20.0.5' } } },
|
|
};
|
|
|
|
{
|
|
const docker = fakeDocker([runningContainer]);
|
|
const target = await resolveTarget(docker, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
assert.strictEqual(target, 'http://172.20.0.5:80', 'known running token resolves to its container');
|
|
console.log('ok — known token resolves to the correct container address');
|
|
}
|
|
|
|
{
|
|
const docker = fakeDocker([runningContainer]);
|
|
const target = await resolveTarget(docker, 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb');
|
|
assert.strictEqual(target, null, 'unknown token resolves to nothing');
|
|
console.log('ok — unknown token resolves to null (404 at the HTTP layer)');
|
|
}
|
|
|
|
{
|
|
// Simulates a destroyed/reset environment: the container that used to match this label is
|
|
// gone (not present in the list at all — a real reset/destroy removes the old container).
|
|
const docker = fakeDocker([]);
|
|
const target = await resolveTarget(docker, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
assert.strictEqual(target, null, 'stale token (container destroyed) resolves to nothing');
|
|
console.log('ok — stale/destroyed-environment token resolves to null, same as unknown');
|
|
}
|
|
|
|
{
|
|
const docker = fakeDocker([runningContainer]);
|
|
for (const garbage of [undefined, null, '', 'short', 123, {}]) {
|
|
const target = await resolveTarget(docker, garbage);
|
|
assert.strictEqual(target, null, `garbage input ${JSON.stringify(garbage)} must not resolve`);
|
|
}
|
|
console.log('ok — garbage/malformed tokens are rejected before ever touching Docker');
|
|
}
|
|
|
|
{
|
|
assert.strictEqual(tokenFingerprint('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), 'aaaaaa…(32)');
|
|
assert.strictEqual(tokenFingerprint(undefined), '(none)');
|
|
const fp = tokenFingerprint('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
assert.ok(!fp.includes('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'), 'fingerprint never contains the full token — never logged in plaintext');
|
|
console.log('ok — token fingerprint for logs never reveals the full token');
|
|
}
|
|
|
|
console.log('\nAll lab-router unit tests passed.');
|
|
}
|
|
|
|
main().catch((e) => { console.error(e); process.exit(1); });
|