mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Two findings from PoBruno's Docker Engine on Linux run (#208), and one from re-running the suite in the container after rebasing: - The launcher pinned its own allowedHosts, including '.local'. Vite reads a leading dot as a suffix match, so `Host: anything.local` reached the page and the data proxies. Main has since moved the policy into build/allowedHosts.js, which drops suffix entries on purpose and applies before the app's routes, so the launcher's override is deleted rather than patched. As a side effect GEV_ALLOWED_HOSTS now also works from `.env`, as it does under `npm run dev`. - A checkout the container cannot write - typically a clone made as root on a NAS or Proxmox host - made Save Keys answer 500 and never created .gev-cache, with nothing logged. The launcher now warns at start, naming the owner, the ids it runs as, and the two fixes. - On Docker Desktop the bind-mounted repo always shows an owner other than the container user, so git refuses it as "dubious ownership" and the tests that shell out to git fail (src/data/bundledJson.test.mjs, new on main). The image now marks /app, and only /app, as a safe directory. Verified in a container on this base: `anything.local` 403 on the page and on /api routes, localhost 200, GEV_ALLOWED_HOSTS honoured from the environment and from .env, the warning on a root-owned /app run as 1000:1000, and bundledJson.test.mjs 6/6. Reported-by: PoBruno Co-Authored-By: Claude Opus 5.5 <[email protected]>
78 lines
3.6 KiB
JavaScript
78 lines
3.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import test from 'node:test';
|
|
import {
|
|
defaultGatewayFromRouteTable,
|
|
unwritableRootWarning,
|
|
resolveTrustedPeers,
|
|
} from '../scripts/docker-start.mjs';
|
|
|
|
// The shape of /proc/net/route inside a bridge-networked container:
|
|
// little-endian hex addresses, the default route first, an interface route
|
|
// after.
|
|
const BRIDGE_ROUTES = [
|
|
'Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT',
|
|
'eth0\t00000000\t0100A8C0\t0003\t0\t0\t0\t00000000\t0\t0\t0',
|
|
'eth0\t0000A8C0\t00000000\t0001\t0\t0\t0\t00F0FFFF\t0\t0\t0',
|
|
'',
|
|
].join('\n');
|
|
|
|
test('the container gateway is decoded from the kernel routing table', () => {
|
|
assert.equal(defaultGatewayFromRouteTable(BRIDGE_ROUTES), '192.168.0.1', 'little-endian hex, default route only');
|
|
assert.equal(
|
|
defaultGatewayFromRouteTable(BRIDGE_ROUTES.replace('0100A8C0', '011FAC')),
|
|
null,
|
|
'a malformed gateway field is not an address',
|
|
);
|
|
assert.equal(
|
|
defaultGatewayFromRouteTable(BRIDGE_ROUTES.replace('\t0003\t', '\t0002\t')),
|
|
null,
|
|
'a default route that is not up is ignored',
|
|
);
|
|
assert.equal(
|
|
defaultGatewayFromRouteTable(BRIDGE_ROUTES.replace('0100A8C0', '00000000')),
|
|
null,
|
|
'a zero gateway is not an address',
|
|
);
|
|
assert.equal(
|
|
defaultGatewayFromRouteTable(
|
|
BRIDGE_ROUTES.split('\n').filter((line) => !line.includes('00000000\t0100A8C0')).join('\n'),
|
|
),
|
|
null,
|
|
'--network none has no default route and must not invent one',
|
|
);
|
|
assert.equal(defaultGatewayFromRouteTable(''), null);
|
|
assert.equal(defaultGatewayFromRouteTable(undefined), null);
|
|
});
|
|
|
|
test('an operator list wins, and no gateway trusts nothing', () => {
|
|
assert.equal(resolveTrustedPeers({ env: {}, routeTable: BRIDGE_ROUTES }), '192.168.0.1');
|
|
assert.equal(resolveTrustedPeers({ env: { GEV_KEY_SETUP_TRUSTED_PEERS: ' 10.9.8.7 ' }, routeTable: BRIDGE_ROUTES }), '10.9.8.7');
|
|
assert.equal(
|
|
resolveTrustedPeers({ env: { GEV_KEY_SETUP_TRUSTED_PEERS: ' ' }, routeTable: BRIDGE_ROUTES }),
|
|
'192.168.0.1',
|
|
'a blank override falls through to the gateway',
|
|
);
|
|
assert.equal(resolveTrustedPeers({ env: {}, routeTable: '' }), '', 'loopback-only when there is no gateway');
|
|
});
|
|
|
|
test('an unwritable checkout is named at start instead of failing quietly', () => {
|
|
const ids = { owner: { uid: 0, gid: 0 }, runAs: { uid: 1000, gid: 1000 } };
|
|
assert.equal(unwritableRootWarning({ root: '/app', writable: true, ...ids }), null, 'a writable checkout says nothing');
|
|
const warning = unwritableRootWarning({ root: '/app', writable: false, ...ids });
|
|
assert.match(warning, /owned by 0:0/, 'names the owner');
|
|
assert.match(warning, /runs as 1000:1000/, 'names the ids the container runs as');
|
|
assert.match(warning, /chown -R 1000:1000/, 'offers handing the checkout to the container user first');
|
|
assert.match(warning, /env UID=0 GID=0 docker compose up/, 'or the env form, which bash does not ignore');
|
|
});
|
|
|
|
test('the Docker install path keeps its loopback-only, launcher-owned contract', () => {
|
|
const dockerfile = readFileSync(new URL('../Dockerfile', import.meta.url), 'utf8');
|
|
assert.match(dockerfile, /^CMD \["node", "scripts\/docker-start\.mjs"\]$/m, 'the launcher is the container entry point');
|
|
|
|
const compose = readFileSync(new URL('../compose.yaml', import.meta.url), 'utf8');
|
|
assert.match(compose, /^\s+- "127\.0\.0\.1:4173:4173"$/m, 'the port is published on the host loopback only');
|
|
assert.doesNotMatch(compose, /GEV_KEY_SETUP_TRUSTED_PEERS/, 'trust is derived at launch, never hard-coded');
|
|
assert.doesNotMatch(compose, /network_mode/, 'the container keeps its own network namespace');
|
|
});
|