fix(workers-bridge): refuse a subnet that swallows the default gateway (#74) #79

Merged
lz merged 1 commit from fix/subnet into main 2026-07-17 17:43:36 +02:00
Owner

Fixes #74.

Problem

When Nexus bootstraps inside a DinD worker that is itself attached to the outer nexus-workers bridge (172.30.0.0/16, gw 172.30.0.1), the inner daemon creates a bridge with the identical name and subnet. Because the subnet is pinned explicitly (IPAM.Config[0].Subnet) rather than IPAM-auto-allocated, Docker's own pool-collision avoidance never engages. The outer default gateway 172.30.0.1 gets claimed by the new inner bridge, two 172.30.0.0/16 routes compete, and egress dies — silently, since DNS keeps resolving so it doesn't look like a network-layer failure.

Fix

The minimal guardrail from the issue (option #4 — "that single check would have prevented this outage"): on the create path only, read the container's default gateway from /proc/net/route and refuse to create a bridge whose subnet contains it, throwing an actionable error that names the two escape hatches (WORKERS_BRIDGE_SUBNET, WORKER_NETWORK).

  • The guard sits inside the if (!network) block, so the steady state (bridge already exists) and the WORKER_NETWORK reuse path are untouched.
  • ensureWorkersBridge is only reached once per daemon lifetime (the caller caches in a WeakMap), so the tiny synchronous proc read is off any hot path.
  • Scoped to the default gateway deliberately: egress flows through the default route, which is exactly the collision class that caused the reported harm. Widening to all interface subnets would be scope creep.

New pure helpers (parseRouteTable, subnetContains, ipToInt) live beside their sole caller — no IP/CIDR library is installed and no shared net-utils module exists, so a first-of-kind is correct here.

Tests

  • Guard fires on collision, passes when the gateway is outside the subnet, and is skipped when reusing an existing network.
  • Unit coverage for parseRouteTable (little-endian parse, no-default-route) and subnetContains (containment, prefix length, malformed input).

Verification

  • pnpm test735 passed (75 files)
  • tsc --noEmit → clean
  • Ran pnpm dev: booted cleanly, ran migrations, served the app, /api/state200 needs-unlock, no errors in the log.

Reviewed with a 4-angle /simplify pass (reuse / simplification / efficiency / altitude); applied the one finding (split a pure parseRouteTable out of readDefaultGateway, dropping all temp-file machinery from the tests).

Fixes #74. ## Problem When Nexus bootstraps *inside* a DinD worker that is itself attached to the outer `nexus-workers` bridge (`172.30.0.0/16`, gw `172.30.0.1`), the inner daemon creates a bridge with the identical **name and subnet**. Because the subnet is pinned explicitly (`IPAM.Config[0].Subnet`) rather than IPAM-auto-allocated, Docker's own pool-collision avoidance never engages. The outer default gateway `172.30.0.1` gets claimed by the new inner bridge, two `172.30.0.0/16` routes compete, and egress dies — **silently**, since DNS keeps resolving so it doesn't look like a network-layer failure. ## Fix The minimal guardrail from the issue (option #4 — "that single check would have prevented this outage"): on the **create path only**, read the container's default gateway from `/proc/net/route` and refuse to create a bridge whose subnet contains it, throwing an actionable error that names the two escape hatches (`WORKERS_BRIDGE_SUBNET`, `WORKER_NETWORK`). - The guard sits inside the `if (!network)` block, so the steady state (bridge already exists) and the `WORKER_NETWORK` reuse path are untouched. - `ensureWorkersBridge` is only reached once per daemon lifetime (the caller caches in a `WeakMap`), so the tiny synchronous proc read is off any hot path. - Scoped to the **default** gateway deliberately: egress flows through the default route, which is exactly the collision class that caused the reported harm. Widening to all interface subnets would be scope creep. New pure helpers (`parseRouteTable`, `subnetContains`, `ipToInt`) live beside their sole caller — no IP/CIDR library is installed and no shared net-utils module exists, so a first-of-kind is correct here. ## Tests - Guard fires on collision, passes when the gateway is outside the subnet, and is skipped when reusing an existing network. - Unit coverage for `parseRouteTable` (little-endian parse, no-default-route) and `subnetContains` (containment, prefix length, malformed input). ## Verification - `pnpm test` → **735 passed** (75 files) - `tsc --noEmit` → clean - Ran `pnpm dev`: booted cleanly, ran migrations, served the app, `/api/state` → `200 needs-unlock`, no errors in the log. Reviewed with a 4-angle `/simplify` pass (reuse / simplification / efficiency / altitude); applied the one finding (split a pure `parseRouteTable` out of `readDefaultGateway`, dropping all temp-file machinery from the tests).
fix(workers-bridge): refuse a subnet that swallows the default gateway (#74)
All checks were successful
ci / nexus (pull_request) Successful in 9m53s
pr-image-cleanup / delete-pr-images (pull_request) Successful in 13s
ci / images (pull_request) Successful in 7m23s
36cc09cdd6
When Nexus bootstraps inside a DinD worker attached to the outer
nexus-workers bridge (172.30.0.0/16), the inner daemon creates a bridge
with the identical name AND subnet. Because the subnet is pinned
explicitly (not IPAM auto-allocated), Docker's pool-collision avoidance
never engages: 172.30.0.1 — the OUTER default gateway — is claimed by the
inner bridge, two 172.30.0.0/16 routes compete, and egress dies silently
(DNS still resolves, so it doesn't look network-layer).

Guard the create path: read the container's default gateway from
/proc/net/route and refuse to create a bridge whose subnet contains it,
with an actionable error pointing at WORKERS_BRIDGE_SUBNET / WORKER_NETWORK.
The check runs only when the bridge doesn't already exist, so the steady
state is untouched. Fixes #74.
lz merged commit 4217a07e68 into main 2026-07-17 17:43:36 +02:00
lz deleted branch fix/subnet 2026-07-17 17:43:37 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus!79
No description provided.