Gateways sharing an address are one gateway
Found by asking what gw-devices and gw-home actually were, in a picture that
finally made them easy to see side by side.
planRouters grouped on the exact address list, so `home` declaring a v4 and a v6
address and `devices` declaring only the v4 became two router containers — both
holding 198.51.100.7 on the same segment. The lab raised it without complaint.
Not theoretical. On the raised instance the transit router resolved that one
address to two different MACs across a cache flush:
198.51.100.7 -> 02:c9:16:70:23:29 (gw0, which HAS the :443 dnat)
198.51.100.7 -> 02:bd:75:0b:b0:75 (gw1, which has none)
So home-server's published port worked or did not depending on which container
answered ARP last — intermittent, and it would have presented as a flaky test
rather than as a broken scenario.
One public address is one box. Checked against the thing this models rather than
argued from the model: a bridged modem, a single gateway holding the public
address, one network behind it, and every port forward landing on one host at
that address. Two routers on one address is not a topology, it is a collision.
Gateways to the same segment sharing any address are now one router and their
address lists union, so a v6 address declared on only one of the segments it
serves is still carried. Where such declarations disagree on nat, forwardable or
mapping_ttl, validate refuses — one box cannot behave two ways.
the-ordinary-shape now raises 7 machines instead of 8, and gw0 holds the public
address on eth0 while serving home on eth1 and devices on eth2.
This commit is contained in:
@@ -149,6 +149,42 @@ export function validate(scenario: Scenario): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Two gateways sharing an address are the same box, and one box cannot behave two ways.
|
||||
// Left unchecked this raised two routers holding one address on one segment, where the
|
||||
// address resolved to whichever answered ARP last — so a published port worked or did
|
||||
// not, run to run, with nothing reporting a fault.
|
||||
const gateways = Object.entries(scenario.segments)
|
||||
.filter(([, segment]) => segment.gateway)
|
||||
.map(([name, segment]) => ({ name, gateway: segment.gateway! }));
|
||||
|
||||
for (let i = 0; i < gateways.length; i++) {
|
||||
for (let j = i + 1; j < gateways.length; j++) {
|
||||
const a = gateways[i]!;
|
||||
const b = gateways[j]!;
|
||||
if (a.gateway.to !== b.gateway.to) continue;
|
||||
const shared = a.gateway.address.filter((address) => b.gateway.address.includes(address));
|
||||
if (shared.length === 0) continue;
|
||||
|
||||
const differences: string[] = [];
|
||||
if ([...a.gateway.nat].sort().join(",") !== [...b.gateway.nat].sort().join(",")) {
|
||||
differences.push(`nat (${a.gateway.nat.join("+") || "none"} vs ${b.gateway.nat.join("+") || "none"})`);
|
||||
}
|
||||
if (a.gateway.forwardable !== b.gateway.forwardable) {
|
||||
differences.push(`forwardable (${a.gateway.forwardable} vs ${b.gateway.forwardable})`);
|
||||
}
|
||||
if (a.gateway.mappingTtl !== b.gateway.mappingTtl) {
|
||||
differences.push(`mapping_ttl (${a.gateway.mappingTtl ?? "none"} vs ${b.gateway.mappingTtl ?? "none"})`);
|
||||
}
|
||||
if (differences.length > 0) {
|
||||
problems.push(
|
||||
`segments '${a.name}' and '${b.name}' declare gateways on '${a.gateway.to}' sharing ` +
|
||||
`address '${shared[0]}', so they are one gateway — but they disagree on ` +
|
||||
`${differences.join(" and ")}. One box cannot behave two ways.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A gateway chain must terminate. A cycle would raise forever rather than fail.
|
||||
for (const name of segmentNames) {
|
||||
const seen = new Set<string>([name]);
|
||||
|
||||
Reference in New Issue
Block a user