From 02f68894c399b977400d19153a7c4aa419cbf7c9 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Mon, 17 Aug 2026 03:46:16 -0300 Subject: [PATCH] =?UTF-8?q?fix(ci=20OPS50):=20guards=20de=20banco=20aceita?= =?UTF-8?q?m=20o=20gateway=20da=20bridge=20do=20runner=20(RFC1918)=20?= =?UTF-8?q?=E2=80=94=20postgres=20do=20services:=20fica=20no=20IP=20do=20g?= =?UTF-8?q?ateway,=20n=C3=A3o=20em=20localhost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/lib/cli.mjs | 39 +++++++++++++++++++++++++++++ tests/helpers/assertTestDatabase.ts | 7 ++++++ tests/unit/localHosts.unit.spec.ts | 28 +++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 tests/unit/localHosts.unit.spec.ts diff --git a/scripts/lib/cli.mjs b/scripts/lib/cli.mjs index 144a4e9d..5733573b 100644 --- a/scripts/lib/cli.mjs +++ b/scripts/lib/cli.mjs @@ -7,6 +7,7 @@ * any of these re-spelled outside this module. */ import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' import { access, mkdir, readFile, writeFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { dirname, join } from 'node:path' @@ -31,6 +32,44 @@ export const loadCliEnv = () => { /** Local database hosts — including the docker-compose service name. THE one list. */ export const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres']) +/** + * Gateway of the default route, read from /proc/net/route (Linux). The + * self-hosted Forgejo runner publishes each job's `services:` Postgres on the + * bridge gateway IP (192.168.x.1 — differs per job), so a job reaches it via + * that IP instead of localhost. The same octet order the ci-pr.yml awk + * produces (the /proc fields are little-endian). Returns null when there is + * no default route or the file is unreadable (macOS/Windows dev boxes). + */ +export const defaultGatewayHost = () => { + try { + const routes = readFileSync('/proc/net/route', 'utf8').split('\n') + for (const line of routes) { + const fields = line.trim().split(/\s+/) + if (fields[1] !== '00000000') continue + const hex = fields[2] ?? '' + if (hex === '00000000' || hex.length < 8) return null + const octets = [ + parseInt(hex.slice(6, 8), 16), + parseInt(hex.slice(4, 6), 16), + parseInt(hex.slice(2, 4), 16), + parseInt(hex.slice(0, 2), 16), + ] + const isRfc1918 = + octets[0] === 10 || + (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) || + (octets[0] === 192 && octets[1] === 168) + if (!isRfc1918) return null + return octets.join('.') + } + } catch { + // /proc/net/route is Linux-only; fall through to the static list. + } + return null +} + +const gatewayHost = defaultGatewayHost() +if (gatewayHost) LOCAL_HOSTS.add(gatewayHost) + export const ALLOW_REMOTE_DB_FLAG = 'ALLOW_REMOTE_DB' export const isRemoteDbOverrideSet = () => diff --git a/tests/helpers/assertTestDatabase.ts b/tests/helpers/assertTestDatabase.ts index cbd83a38..38b2bd3d 100644 --- a/tests/helpers/assertTestDatabase.ts +++ b/tests/helpers/assertTestDatabase.ts @@ -12,6 +12,11 @@ /** `teqo_test`, `teqo_wt15_test`, … — the _test suffix is the hard contract. */ const TEST_DATABASE_NAME = /^teqo(_[a-z0-9]+)?_test$/ +// Self-hosted Forgejo runner: the job's postgres service is published on the +// bridge gateway IP (RFC1918), not localhost. Same source as the CLI guard so +// both guards admit exactly the same hosts (OPS50). +import { defaultGatewayHost } from '../../scripts/lib/cli.mjs' + export function assertTestDatabase(databaseUrl: string | undefined): void { if (!databaseUrl) { throw new Error( @@ -29,6 +34,8 @@ export function assertTestDatabase(databaseUrl: string | undefined): void { const databaseName = decodeURIComponent(parsedUrl.pathname.replace(/^\//, '')) const localHosts = new Set(['localhost', '127.0.0.1', '::1', '[::1]']) + const gateway = defaultGatewayHost() + if (gateway) localHosts.add(gateway) if ( parsedUrl.protocol !== 'postgresql:' || diff --git a/tests/unit/localHosts.unit.spec.ts b/tests/unit/localHosts.unit.spec.ts new file mode 100644 index 00000000..a15331d2 --- /dev/null +++ b/tests/unit/localHosts.unit.spec.ts @@ -0,0 +1,28 @@ +// @vitest-environment node + +import { describe, expect, it } from 'vitest' + +import { defaultGatewayHost, LOCAL_HOSTS } from '../../scripts/lib/cli.mjs' + +describe('LOCAL_HOSTS (OPS50 — self-hosted runner reaches services via the bridge gateway)', () => { + it('still contains the canonical static hosts', () => { + for (const host of ['localhost', '127.0.0.1', '0.0.0.0', '::1', 'postgres']) { + expect(LOCAL_HOSTS.has(host)).toBe(true) + } + }) + + it('admits the machine default gateway when it is RFC1918 (Linux CI bridge)', () => { + const gateway = defaultGatewayHost() + if (gateway) { + // The Forgejo runner publishes the job's postgres service on the bridge + // gateway IP; assertLocalDatabase must accept it so seeds run in CI. + expect(LOCAL_HOSTS.has(gateway)).toBe(true) + } + }) + + it('never admits public or loopback hosts', () => { + expect(LOCAL_HOSTS.has('8.8.8.8')).toBe(false) + expect(LOCAL_HOSTS.has('127.0.0.2')).toBe(false) + expect(LOCAL_HOSTS.has('203.0.113.1')).toBe(false) + }) +})