The home segment moves off 192.168.1.0/24

It is the commonest home LAN range there is, so on an ordinary workstation the
lab's private segment and the machine's own network are the same addresses. The
scenario routes an egress machine explicitly and marks the rest unreachable, so
nothing leaked — but that guard was carrying the whole weight of a collision
nobody chose, and a guard is a bad place for that.

10.99.1.0/24 is still RFC 1918, so the bed still models a home LAN behind an
access point. It is simply far from what this kind of machine already has:
192.168.1 is the LAN, 172.16-31 and 192.168.16-95 are container bridges, and
10.10/10.42/10.208 are a tunnel, the mesh overlay and the virtualisation daemon.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-11 00:00:19 +02:00
parent a4c2a9b90b
commit 94e617915c
12 changed files with 48 additions and 48 deletions
+2 -2
View File
@@ -11,7 +11,7 @@ segments:
home: home:
kind: private kind: private
cidr: [192.168.1.0/24] cidr: [10.99.1.0/24]
gateway: gateway:
to: hosting to: hosting
address: [192.0.2.50] # what the world sees the household as address: [192.0.2.50] # what the world sees the household as
@@ -25,7 +25,7 @@ machines:
inbound: allow inbound: allow
home-server: # a dash in the name, on purpose home-server: # a dash in the name, on purpose
at: { segment: home, address: [192.168.1.135] } at: { segment: home, address: [10.99.1.135] }
published: published:
- { port: 8080, on: home } - { port: 8080, on: home }
inbound: allow inbound: allow
+2 -2
View File
@@ -15,7 +15,7 @@ segments:
home: home:
kind: private kind: private
cidr: [192.168.1.0/24] cidr: [10.99.1.0/24]
gateway: gateway:
to: hosting to: hosting
address: [192.0.2.50] address: [192.0.2.50]
@@ -53,7 +53,7 @@ machines:
inbound: allow inbound: allow
home-server: home-server:
at: { segment: home, address: [192.168.1.135] } at: { segment: home, address: [10.99.1.135] }
inbound: allow inbound: allow
thermostat: thermostat:
+4 -4
View File
@@ -21,7 +21,7 @@ segments:
home: home:
kind: private kind: private
cidr: [192.168.1.0/24, "2001:db8:b:1::/64"] cidr: [10.99.1.0/24, "2001:db8:b:1::/64"]
mtu: 1492 mtu: 1492
gateway: gateway:
to: isp-home to: isp-home
@@ -61,17 +61,17 @@ machines:
inbound: allow inbound: allow
home-server: home-server:
at: { segment: home, address: [192.168.1.135, "2001:db8:b:1::135"] } at: { segment: home, address: [10.99.1.135, "2001:db8:b:1::135"] }
published: published:
- { port: 443, on: home } - { port: 443, on: home }
inbound: allow inbound: allow
workstation: workstation:
at: { segment: home, address: [192.168.1.250, "2001:db8:b:1::250"] } at: { segment: home, address: [10.99.1.250, "2001:db8:b:1::250"] }
inbound: deny inbound: deny
laptop: laptop:
at: { segment: home, address: [192.168.1.98, "2001:db8:b:1::98"] } at: { segment: home, address: [10.99.1.98, "2001:db8:b:1::98"] }
inbound: deny inbound: deny
# No `place:` yet. The node host it would place does not exist — this lab is being built to # No `place:` yet. The node host it would place does not exist — this lab is being built to
+7 -7
View File
@@ -8,9 +8,9 @@
# — the access point — reachable from the outside only through what they dial out to. # — the access point — reachable from the outside only through what they dial out to.
# #
# hosting (public, routable) home (private, behind the access point) # hosting (public, routable) home (private, behind the access point)
# novox 192.0.2.20 ── anchor ace 192.168.1.10 home server, media/IoT set # novox 192.0.2.20 ── anchor ace 10.99.1.10 home server, media/IoT set
# substrate + novox set shanks 192.168.1.20 workstation (light) # substrate + novox set shanks 10.99.1.20 workstation (light)
# overlay hub, ingress g14 192.168.1.30 workstation (light) # overlay hub, ingress g14 10.99.1.30 workstation (light)
# #
# The `home` gateway masquerades v4 outbound and forwards inbound (an ordinary household router). # The `home` gateway masquerades v4 outbound and forwards inbound (an ordinary household router).
# Home nodes reach novox's public 192.0.2.20 by dialling OUT through it: the substrate broker (5671), # Home nodes reach novox's public 192.0.2.20 by dialling OUT through it: the substrate broker (5671),
@@ -58,7 +58,7 @@ segments:
# is exactly the NAT hole a WireGuard keepalive has to hold open. # is exactly the NAT hole a WireGuard keepalive has to hold open.
home: home:
kind: private kind: private
cidr: [192.168.1.0/24] cidr: [10.99.1.0/24]
gateway: gateway:
to: hosting to: hosting
address: [192.0.2.50] # what the world sees the household as address: [192.0.2.50] # what the world sees the household as
@@ -100,7 +100,7 @@ machines:
# The home server: the whole ace media/home set — 24 modules, ~50 containers, several heavy # The home server: the whole ace media/home set — 24 modules, ~50 containers, several heavy
# (Plex, Home Assistant, Letta, Baserow, the UniFi JVM, mssql). Behind the gateway. # (Plex, Home Assistant, Letta, Baserow, the UniFi JVM, mssql). Behind the gateway.
ace: ace:
at: { segment: home, address: [192.168.1.10] } at: { segment: home, address: [10.99.1.10] }
egress: true egress: true
inbound: allow inbound: allow
memory: 18GiB memory: 18GiB
@@ -140,7 +140,7 @@ machines:
# nodes with no overlay endpoint of their own hairpin the hub rather than peering directly, which # nodes with no overlay endpoint of their own hairpin the hub rather than peering directly, which
# is the normal case and is fine. # is the normal case and is fine.
shanks: shanks:
at: { segment: home, address: [192.168.1.20] } at: { segment: home, address: [10.99.1.20] }
egress: true egress: true
inbound: allow inbound: allow
memory: 3GiB memory: 3GiB
@@ -151,7 +151,7 @@ machines:
# everywhere" was never a description of anything real. # everywhere" was never a description of anything real.
images: [mesh-runtime-portainer:development] images: [mesh-runtime-portainer:development]
g14: g14:
at: { segment: home, address: [192.168.1.30] } at: { segment: home, address: [10.99.1.30] }
egress: true egress: true
inbound: allow inbound: allow
memory: 3GiB memory: 3GiB
+1 -1
View File
@@ -109,7 +109,7 @@ test("segments are ordered public first, then by depth behind them", () => {
}); });
test("a declared address appears on the machine that holds it", () => { test("a declared address appears on the machine that holds it", () => {
assert.match(xml, /192\.168\.1\.135/); assert.match(xml, /10\.99\.1\.135/);
assert.match(xml, /198\.51\.100\.7/); assert.match(xml, /198\.51\.100\.7/);
}); });
+7 -7
View File
@@ -30,7 +30,7 @@ segments:
cidr: [192.0.2.0/24] cidr: [192.0.2.0/24]
home: home:
kind: private kind: private
cidr: [192.168.1.0/24] cidr: [10.99.1.0/24]
gateway: gateway:
to: hosting to: hosting
address: [192.0.2.50] address: [192.0.2.50]
@@ -41,10 +41,10 @@ machines:
at: { segment: hosting, address: [192.0.2.20] } at: { segment: hosting, address: [192.0.2.20] }
egress: true egress: true
ace: ace:
at: { segment: home, address: [192.168.1.10] } at: { segment: home, address: [10.99.1.10] }
egress: true egress: true
sealed: sealed:
at: { segment: home, address: [192.168.1.99] } at: { segment: home, address: [10.99.1.99] }
`; `;
test("a machine behind a gateway still reaches the scenario through that gateway", () => { test("a machine behind a gateway still reaches the scenario through that gateway", () => {
@@ -52,16 +52,16 @@ test("a machine behind a gateway still reaches the scenario through that gateway
// handshake has to survive it. An egress machine that stopped using its gateway would be // handshake has to survive it. An egress machine that stopped using its gateway would be
// testing a flat network with extra steps. // testing a flat network with extra steps.
const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "ace"); const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "ace");
assert.deepEqual(routes, [{ cidr: "192.0.2.0/24", via: "192.168.1.1" }]); assert.deepEqual(routes, [{ cidr: "192.0.2.0/24", via: "10.99.1.1" }]);
}); });
test("a machine's own segment gets no route — it is already on-link", () => { test("a machine's own segment gets no route — it is already on-link", () => {
const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "ace"); const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "ace");
assert.ok(!routes.some((r) => r.cidr === "192.168.1.0/24"), JSON.stringify(routes)); assert.ok(!routes.some((r) => r.cidr === "10.99.1.0/24"), JSON.stringify(routes));
}); });
/** /**
* **The dangerous one.** `home` is 192.168.1.0/24 — a documentation range in spirit, an ordinary * **The dangerous one.** `home` is 10.99.1.0/24 — a documentation range in spirit, an ordinary
* private one in fact, and very possibly the network the workstation itself is on. * private one in fact, and very possibly the network the workstation itself is on.
* *
* With one public segment there is no transit router, so novox has no path to `home` at all. Left * With one public segment there is no transit router, so novox has no path to `home` at all. Left
@@ -71,7 +71,7 @@ test("a machine's own segment gets no route — it is already on-link", () => {
*/ */
test("a range with no path inside the scenario is unreachable, not leaked to the uplink", () => { test("a range with no path inside the scenario is unreachable, not leaked to the uplink", () => {
const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "novox"); const routes = scenarioRoutesFor(parseScenario(HOUSEHOLD), "novox");
assert.deepEqual(routes, [{ cidr: "192.168.1.0/24", via: null }]); assert.deepEqual(routes, [{ cidr: "10.99.1.0/24", via: null }]);
}); });
test("a machine without egress is left to its default route, and states nothing", () => { test("a machine without egress is left to its default route, and states nothing", () => {
+3 -3
View File
@@ -45,7 +45,7 @@ test("ADR 0016 — the lab provides the underlay and NOTHING of the overlay", {
test("ADR 0016 — the declared address IS what the machine holds", { skip }, async () => { test("ADR 0016 — the declared address IS what the machine holds", { skip }, async () => {
const { stdout } = await exec(instanceId, "home-server", ["ip", "-o", "-4", "addr", "show"]); const { stdout } = await exec(instanceId, "home-server", ["ip", "-o", "-4", "addr", "show"]);
assert.match(stdout, /192\.168\.1\.135\/24/); assert.match(stdout, /10\.99\.1\.135\/24/);
}); });
test("design — raise waits for USABLE, not for the call to return", { skip, timeout: 120_000 }, async () => { test("design — raise waits for USABLE, not for the call to return", { skip, timeout: 120_000 }, async () => {
@@ -75,7 +75,7 @@ test("ADR 0016 — a router is scenery: containers, while machines are virtual m
test("design — NAT: a private address is not reachable from outside", { skip, timeout: 120_000 }, async () => { test("design — NAT: a private address is not reachable from outside", { skip, timeout: 120_000 }, async () => {
const { stdout } = await exec(instanceId, "anchor", [ const { stdout } = await exec(instanceId, "anchor", [
"sh", "-c", "ping -c1 -W2 192.168.1.135 >/dev/null 2>&1 && echo reachable || echo unreachable", "sh", "-c", "ping -c1 -W2 10.99.1.135 >/dev/null 2>&1 && echo reachable || echo unreachable",
]); ]);
assert.equal(stdout.trim(), "unreachable"); assert.equal(stdout.trim(), "unreachable");
}); });
@@ -139,7 +139,7 @@ test("the live diagram reads the hypervisor, and a VM's addresses are not lost",
assert.ok(server, "home-server missing from the live picture"); assert.ok(server, "home-server missing from the live picture");
assert.equal(server.kind, "machine"); assert.equal(server.kind, "machine");
assert.ok( assert.ok(
server.attachments.some((a) => a.addresses.some((address) => address.startsWith("192.168.1.135"))), server.attachments.some((a) => a.addresses.some((address) => address.startsWith("10.99.1.135"))),
`a virtual machine's addresses were not read back: ${JSON.stringify(server.attachments)}`, `a virtual machine's addresses were not read back: ${JSON.stringify(server.attachments)}`,
); );
}); });
+3 -3
View File
@@ -4,9 +4,9 @@
* anchor, everything on one public segment) into what production actually is: * anchor, everything on one public segment) into what production actually is:
* *
* hosting (public) home (private, behind a NAT access point) * hosting (public) home (private, behind a NAT access point)
* novox 192.0.2.20 — the ANCHOR: ace 192.168.1.10 the home server, media/IoT set * novox 192.0.2.20 — the ANCHOR: ace 10.99.1.10 the home server, media/IoT set
* substrate (store/broker/ shanks 192.168.1.20 workstation (light: portainer only) * substrate (store/broker/ shanks 10.99.1.20 workstation (light: portainer only)
* control) + the whole novox g14 192.168.1.30 workstation (light: portainer only) * control) + the whole novox g14 10.99.1.30 workstation (light: portainer only)
* set + overlay hub + ingress * set + overlay hub + ingress
* *
* There is NO separate anchor: novox IS the anchor. The substrate runs on novox, and novox also * There is NO separate anchor: novox IS the anchor. The substrate runs on novox, and novox also
+2 -2
View File
@@ -23,8 +23,8 @@ test("the same address on different segments is NOT a conflict", () => {
// Every private network has its own `.1`. Reporting that would make the check useless. // Every private network has its own `.1`. Reporting that would make the check useless.
assert.deepEqual( assert.deepEqual(
duplicateAddresses([ duplicateAddresses([
{ machine: "gw-a", segment: "home", address: "192.168.1.1/24" }, { machine: "gw-a", segment: "home", address: "10.99.1.1/24" },
{ machine: "gw-b", segment: "cafe", address: "192.168.1.1/24" }, { machine: "gw-b", segment: "cafe", address: "10.99.1.1/24" },
]), ]),
[], [],
); );
+4 -4
View File
@@ -9,9 +9,9 @@ test("segments sharing a gateway declaration share ONE router", () => {
const scenario = parseScenario(`scenario: x const scenario = parseScenario(`scenario: x
segments: segments:
pub: { kind: public, cidr: [192.0.2.0/24] } pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } } home: { kind: private, cidr: [10.99.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
iot: { kind: private, cidr: [192.168.30.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } } iot: { kind: private, cidr: [192.168.30.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
machines: { a: { at: { segment: home, address: [192.168.1.9] } } }`); machines: { a: { at: { segment: home, address: [10.99.1.9] } } }`);
const plans = planRouters(scenario, "inst"); const plans = planRouters(scenario, "inst");
assert.equal(plans.length, 1, "one gateway declaration, one router"); assert.equal(plans.length, 1, "one gateway declaration, one router");
assert.deepEqual(plans[0]?.inside.sort(), ["home", "iot"]); assert.deepEqual(plans[0]?.inside.sort(), ["home", "iot"]);
@@ -21,9 +21,9 @@ test("different external addresses mean different routers", () => {
const scenario = parseScenario(`scenario: x const scenario = parseScenario(`scenario: x
segments: segments:
pub: { kind: public, cidr: [192.0.2.0/24] } pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } } home: { kind: private, cidr: [10.99.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
other: { kind: private, cidr: [192.168.30.0/24], gateway: { to: pub, address: [192.0.2.6], nat: [v4] } } other: { kind: private, cidr: [192.168.30.0/24], gateway: { to: pub, address: [192.0.2.6], nat: [v4] } }
machines: { a: { at: { segment: home, address: [192.168.1.9] } } }`); machines: { a: { at: { segment: home, address: [10.99.1.9] } } }`);
assert.equal(planRouters(scenario, "inst").length, 2); assert.equal(planRouters(scenario, "inst").length, 2);
}); });
+4 -4
View File
@@ -13,8 +13,8 @@ import { assertSupported, UnsupportedError } from "../src/lifecycle/supported.ts
const withGateway = `scenario: x const withGateway = `scenario: x
segments: segments:
pub: { kind: public, cidr: [192.0.2.0/24] } pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } } home: { kind: private, cidr: [10.99.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
machines: { a: { at: { segment: home, address: [192.168.1.9] } } }`; machines: { a: { at: { segment: home, address: [10.99.1.9] } } }`;
test("a plain scenario is raisable", () => { test("a plain scenario is raisable", () => {
const scenario = parseScenario(`scenario: x const scenario = parseScenario(`scenario: x
@@ -31,12 +31,12 @@ test("published ports and policy are implemented", () => {
const scenario = parseScenario(`scenario: x const scenario = parseScenario(`scenario: x
segments: segments:
pub: { kind: public, cidr: [192.0.2.0/24] } pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } } home: { kind: private, cidr: [10.99.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
iot: { kind: private, cidr: [192.168.30.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } } iot: { kind: private, cidr: [192.168.30.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
policy: [{ from: iot, to: home, allow: false }] policy: [{ from: iot, to: home, allow: false }]
machines: machines:
a: a:
at: { segment: home, address: [192.168.1.9] } at: { segment: home, address: [10.99.1.9] }
published: [{ port: 443, on: home }]`); published: [{ port: 443, on: home }]`);
assert.doesNotThrow(() => assertSupported(scenario)); assert.doesNotThrow(() => assertSupported(scenario));
}); });
+9 -9
View File
@@ -27,8 +27,8 @@ test("the shipped scenarios are valid", () => {
test("a public segment on a private range is refused — the mesh would silently never form", () => { test("a public segment on a private range is refused — the mesh would silently never form", () => {
refuses( refuses(
`scenario: x `scenario: x
segments: { net: { kind: public, cidr: [192.168.1.0/24] } } segments: { net: { kind: public, cidr: [10.99.1.0/24] } }
machines: { a: { at: { segment: net, address: [192.168.1.1] } } }`, machines: { a: { at: { segment: net, address: [10.99.1.1] } } }`,
/not documentation space/, /not documentation space/,
); );
}); });
@@ -71,8 +71,8 @@ test("a gateway address must be on the PARENT segment, not the one behind it", (
`scenario: x `scenario: x
segments: segments:
pub: { kind: public, cidr: [192.0.2.0/24] } pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.168.1.1], nat: [v4] } } home: { kind: private, cidr: [10.99.1.0/24], gateway: { to: pub, address: [10.99.1.1], nat: [v4] } }
machines: { a: { at: { segment: home, address: [192.168.1.9] } } }`, machines: { a: { at: { segment: home, address: [10.99.1.9] } } }`,
/is not within 'pub'/, /is not within 'pub'/,
); );
}); });
@@ -120,7 +120,7 @@ test("publishing on a segment the machine is not attached to is refused", () =>
`scenario: x `scenario: x
segments: segments:
pub: { kind: public, cidr: [192.0.2.0/24] } pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } } home: { kind: private, cidr: [10.99.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
machines: machines:
a: a:
at: { segment: pub, address: [192.0.2.10] } at: { segment: pub, address: [192.0.2.10] }
@@ -176,12 +176,12 @@ test("a multi-homed machine is valid", () => {
parseScenario(`scenario: x parseScenario(`scenario: x
segments: segments:
pub: { kind: public, cidr: [192.0.2.0/24] } pub: { kind: public, cidr: [192.0.2.0/24] }
home: { kind: private, cidr: [192.168.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } } home: { kind: private, cidr: [10.99.1.0/24], gateway: { to: pub, address: [192.0.2.5], nat: [v4] } }
machines: machines:
border: border:
at: at:
- { segment: pub, address: [192.0.2.60] } - { segment: pub, address: [192.0.2.60] }
- { segment: home, address: [192.168.1.2] }`), - { segment: home, address: [10.99.1.2] }`),
); );
}); });
@@ -206,7 +206,7 @@ segments:
cidr: [198.51.100.0/24, "2001:db8:b::/48"] cidr: [198.51.100.0/24, "2001:db8:b::/48"]
home: home:
kind: private kind: private
cidr: [192.168.1.0/24] cidr: [10.99.1.0/24]
gateway: { to: isp, address: [198.51.100.7, "2001:db8:b::7"], nat: [v4], forwardable: true, mapping_ttl: 120s } gateway: { to: isp, address: [198.51.100.7, "2001:db8:b::7"], nat: [v4], forwardable: true, mapping_ttl: 120s }
devices: devices:
kind: private kind: private
@@ -236,7 +236,7 @@ segments:
cidr: [198.51.100.0/24] cidr: [198.51.100.0/24]
home: home:
kind: private kind: private
cidr: [192.168.1.0/24] cidr: [10.99.1.0/24]
gateway: { to: isp, address: [198.51.100.7], nat: [v4], forwardable: true } gateway: { to: isp, address: [198.51.100.7], nat: [v4], forwardable: true }
devices: devices:
kind: private kind: private