Adopt the predecessor's tunnel in place: its range, its address, its peers
On an adopted hub the private network takes over the tunnel it finds rather than running beside it (hq ADR 0105): two tunnels leave the mesh's unreachable through the provider's filter, so no machine can ever join. The node presents the found tunnel when it enrols, under the key it took as its own; the inventory records it (node.tunnel, tunnel_peer — migration 0031) and the mesh composes from it: the overlay's range is the adopted tunnel's, the hub is placed at the tunnel's address on the tunnel's port, and every peer the tunnel had is carried in the hub's peer list as a peer of the tunnel, not a node of the mesh, until a node enrols with that key — which then keeps the address the tunnel had for it. A fresh node never gets an address the tunnel holds. The hub's declaration tells the host which unit to take over; the host's account of carrying it is recorded and shown. Every reader of the range follows the setting; nothing stores it. A found tunnel under another key is recorded and not adopted, so ADR 0100's non-overlap rule keeps applying where a tunnel is left running beside the mesh's. A lab bed and test skeleton for "How it is checked" are under lab/.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# Lab bed: the hub adopts the predecessor's tunnel (novox/hq ADR 0105)
|
||||
|
||||
A scenario and an integration-test skeleton for the mesh-lab repository, kept here because this
|
||||
branch changes only the controller and the host. Move `adopt-the-tunnel.yml` to
|
||||
`mesh-lab/scenarios/` and `adopt-the-tunnel.test.ts` to `mesh-lab/test/integration/` when the
|
||||
feature lands; neither has been run. The skeleton follows `adoption.test.ts` and reuses its
|
||||
harness. Documentation addresses throughout; the bed is node-agnostic.
|
||||
|
||||
## The bed, precisely
|
||||
|
||||
Three machines on one public segment, `hosting` (192.0.2.0/24), inbound allowed on all (the
|
||||
anchor's firewall is the predecessor's, installed by the bed):
|
||||
|
||||
| machine | address | role |
|
||||
|---|---|---|
|
||||
| `anchor` | 192.0.2.10 | the machine in use: the predecessor's hub, then the mesh adopted on it |
|
||||
| `peer-a` | 192.0.2.20 | a predecessor machine: reaches a service on the anchor through the tunnel; later **enrols and keeps its address** |
|
||||
| `peer-b` | 192.0.2.30 | a second predecessor machine: reaches the same service; **never enrols** — the peer that must notice nothing throughout |
|
||||
| `fresh` | 192.0.2.40 | a new machine: enrols later and **gets a fresh address from the same range** |
|
||||
|
||||
**Prepared the way the predecessor leaves a hub** (before genesis, by the bed, on `anchor`):
|
||||
|
||||
- `wireguard-tools` installed; a keypair made on each of `anchor`, `peer-a`, `peer-b`.
|
||||
- `/etc/wireguard/wg0.conf` on the anchor: `[Interface]` `PrivateKey = <anchor's>`,
|
||||
`ListenPort = 51900`, `Address = 10.10.0.1/24`; two `[Peer]` sections — `peer-a`'s public
|
||||
key with `AllowedIPs = 10.10.0.2/32`, `peer-b`'s with `AllowedIPs = 10.10.0.3/32`. Raised with
|
||||
`systemctl enable --now wg-quick@wg0`. **10.10.0.0/24 is deliberately not the mesh's default
|
||||
range** (10.42.0.0/16), so a hub address in 10.10.0.0/24 can only have come from the tunnel.
|
||||
- `wg0.conf` on each peer: its own key, `Address = 10.10.0.2/24` (resp. `.3/24`), one
|
||||
`[Peer]` — the anchor's public key, `Endpoint = 192.0.2.10:51900`,
|
||||
`AllowedIPs = 10.10.0.0/24`, `PersistentKeepalive = 25`. Raised the same way.
|
||||
- A service on the anchor the peers reach **only over the tunnel**: a container publishing
|
||||
`10.10.0.1:8081:80` (bound to the tunnel address, so a call from 192.0.2.20 to 10.10.0.1:8081
|
||||
proves the tunnel carried it). Under a name no catalogue module uses — this bed is about the
|
||||
tunnel, not about taking a service.
|
||||
- The predecessor's firewall (`ufw`) allowing `51900/udp` and `22/tcp`, denying the rest — as ADR
|
||||
0100's bed prepares it.
|
||||
- A record of the anchor's `wg0` public key and of `sha256sum /etc/wireguard/wg0.conf`, taken
|
||||
before genesis, for the assertions below.
|
||||
|
||||
**Genesis**, adopted, on the anchor: `mesh-bootstrap --adopted --node anchor --site hosting
|
||||
--endpoint 192.0.2.10:51900 …` — no `--hub-port`, no `--overlay-range`, no `--tunnel`: the
|
||||
installer finds `wg0` itself (one interface besides `mesh0`) and takes its port and range. The
|
||||
bed asserts genesis **says** it found and took the tunnel.
|
||||
|
||||
## Assertions, in the record's order
|
||||
|
||||
- **T1 — the tunnel changes hands and the peers notice nothing.** After genesis and the push
|
||||
that raises the private network on the anchor:
|
||||
- `wg show interfaces` on the anchor lists `mesh0` and not `wg0`;
|
||||
`systemctl is-active wg-quick@wg0` is inactive and `is-enabled` disabled;
|
||||
- `/etc/wireguard/wg0.conf` is on disk with the recorded digest (kept, never flushed), and
|
||||
`node show anchor` names where its original was kept;
|
||||
- `wg show mesh0 public-key` is the anchor's recorded `wg0` public key; `wg show mesh0
|
||||
listen-port` is 51900; `ip -o addr show dev mesh0` carries `10.10.0.1`; `wg show mesh0 peers`
|
||||
lists both peers' public keys with their `/32` allowed addresses;
|
||||
- a loop on `peer-a` and `peer-b` calling `http://10.10.0.1:8081/` every second, started before
|
||||
genesis, records **no window of failure longer than one WireGuard re-handshake** (measure and
|
||||
assert an upper bound — the switch is one unit stop plus one unit start on the anchor); the
|
||||
peers' `wg0.conf` digests are unchanged; the peers' `wg show wg0 latest-handshakes` advance
|
||||
after the switch.
|
||||
- `overlay show` lists `anchor` as the hub over the tunnel it took over, and both peers under
|
||||
"peers of the tunnel … not nodes of the mesh", not yet enrolled.
|
||||
- **T2 — a peer enrols and keeps its address.** On `peer-a`: `node add peer-a --adopted`,
|
||||
token issued, `mesh-host enrol --token …` **with the broker reached over the tunnel** (the
|
||||
broker address in the token is `10.10.0.1:<bus>`, which only the tunnel routes); then `overlay
|
||||
place peer-a --site house` and a push. Assert: `node show peer-a` says a tunnel `wg0` was
|
||||
found and `overlay show` puts `peer-a` at **10.10.0.2**; the carried-peers list now says
|
||||
`enrolled as peer-a`; the anchor's `mesh0` still has exactly one entry for `peer-a`'s key;
|
||||
`peer-a`'s `wg0` is down and `mesh0` up with the same key; `peer-a` still reaches
|
||||
`10.10.0.1:8081` and `peer-b` still does too, uninterrupted.
|
||||
- **T3 — a new machine gets a fresh address from the same range.** `fresh` enrols converged
|
||||
(no tunnel), is placed, pushed. Assert its address is **10.10.0.4** (`.1` hub, `.2` and `.3`
|
||||
the tunnel's), that it reaches `10.10.0.1` (the hub) and `10.10.0.2` (the enrolled peer) —
|
||||
`ping -c1` over `mesh0` — and that `peer-b`, never enrolled, is still served.
|
||||
- **T4 — nothing derived from the address is stale.** `plan anchor --json` and `plan peer-a
|
||||
--json` (and the rendered `/etc/hosts` on each node) name `10.10.0.1` for the anchor and
|
||||
`10.10.0.2` for `peer-a`, and no address in `10.42.0.0/16`; the broker address handed to a
|
||||
module issued on `peer-a` is `anchor.internal:<bus>` resolving to `10.10.0.1`; the same after a
|
||||
second `push` of every node, byte for byte.
|
||||
- **N — the narrowed ADR 0100 check.** On `fresh`, a converged genesis dry-run with
|
||||
`--overlay-range 10.10.0.0/24` while a *second* tunnel the bed raises there (`wg1` at
|
||||
10.10.0.9/24, not adopted because the node is converged) is up, is refused naming `wg1` —
|
||||
the non-overlap rule still applies where a tunnel is not adopted.
|
||||
|
||||
## What is not asserted here
|
||||
|
||||
- Taking a service over the tunnel (ADR 0100's bed does that).
|
||||
- IPv6 tunnels: the parser reads them, the bed prepares only IPv4.
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* THE HUB ADOPTS THE PREDECESSOR'S TUNNEL (novox/hq ADR 0105). Skeleton — NOT YET RUN.
|
||||
*
|
||||
* The bed and every assertion are described in README.md beside this file; the numbered
|
||||
* assertions here are that document's. Follows adoption.test.ts: same harness, same `on`/`must`
|
||||
* helpers, same genesis wrapper.
|
||||
*
|
||||
* MESH_LAB_INCUS='sudo -n incus'
|
||||
* MESH_LAB_HOST_BINARY=.../mesh-host/mesh-host
|
||||
* MESH_LAB_BUNDLE=.../mesh-host/examples/foundation-first-node.lock
|
||||
* MESH_LAB_CATALOG=.../mesh-catalog/modules
|
||||
*/
|
||||
import { test, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync } from "node:fs";
|
||||
import { loadScenario } from "../../src/declaration/parse.ts";
|
||||
import { raise } from "../../src/lifecycle/raise.ts";
|
||||
import { destroy, exec } from "../../src/lifecycle/operate.ts";
|
||||
import { hostBinaryPath } from "../../src/lifecycle/place.ts";
|
||||
import { labIsUsable, destroyAll, catalogueIsPresent } from "./harness.ts";
|
||||
import { genesis } from "./genesis.ts";
|
||||
|
||||
const capability = await labIsUsable();
|
||||
const binary = hostBinaryPath();
|
||||
const bundle = process.env["MESH_LAB_BUNDLE"] ?? "";
|
||||
const skip = !capability.usable ? `lab not usable: ${capability.why}`
|
||||
: !binary || !existsSync(binary) ? "MESH_LAB_HOST_BINARY is not set to a built mesh-host"
|
||||
: !bundle || !existsSync(bundle) ? "MESH_LAB_BUNDLE is not set to a foundation bundle"
|
||||
: catalogueIsPresent();
|
||||
|
||||
const SCENARIO = "adopt-the-tunnel";
|
||||
const ANCHOR = "anchor", PEER_A = "peer-a", PEER_B = "peer-b", FRESH = "fresh";
|
||||
const TUNNEL = { port: 51900, range: "10.10.0.0/24", hub: "10.10.0.1", a: "10.10.0.2", b: "10.10.0.3", fresh: "10.10.0.4" };
|
||||
const SERVICE = `http://${TUNNEL.hub}:8081/`;
|
||||
|
||||
let instanceId = "";
|
||||
let wg0Key = ""; // the anchor's wg0 public key, recorded before genesis
|
||||
let wg0Digest = ""; // sha256 of /etc/wireguard/wg0.conf before genesis
|
||||
|
||||
async function on(machine: string, command: string, timeoutMs?: number): Promise<{ out: string; ok: boolean }> {
|
||||
const { stdout } = await exec(instanceId, machine, ["sh", "-c", `exec 2>&1\n${command}\necho "__exit=$?"`], timeoutMs);
|
||||
const marker = stdout.lastIndexOf("__exit=");
|
||||
if (marker < 0) return { out: stdout, ok: false };
|
||||
return { out: stdout.slice(0, marker), ok: stdout.slice(marker + 7).trim() === "0" };
|
||||
}
|
||||
async function must(machine: string, command: string, timeoutMs?: number): Promise<string> {
|
||||
const { out, ok } = await on(machine, command, timeoutMs);
|
||||
if (!ok) throw new Error(`${machine}: ${command}\n${out}`);
|
||||
return out;
|
||||
}
|
||||
/** The controller, a container on the anchor. */
|
||||
async function control(args: string): Promise<string> {
|
||||
return must(ANCHOR, `docker exec mesh-controller mesh-controller ${args}`);
|
||||
}
|
||||
|
||||
/** Prepares a machine the way the predecessor leaves it: a keypair and a wg0 — see README.md. */
|
||||
async function predecessorTunnelOn(machine: string, conf: (keys: Record<string, string>) => string, keys: Record<string, string>): Promise<void> {
|
||||
await must(machine, "apt-get install -y wireguard-tools >/dev/null 2>&1 || pacman -S --noconfirm wireguard-tools >/dev/null");
|
||||
await must(machine, `umask 077; printf '%s' '${conf(keys)}' > /etc/wireguard/wg0.conf`);
|
||||
await must(machine, "systemctl enable --now wg-quick@wg0");
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
if (skip) return;
|
||||
await destroyAll(SCENARIO);
|
||||
const scenario = loadScenario(`scenarios/${SCENARIO}.yml`);
|
||||
instanceId = (await raise(scenario)).instanceId;
|
||||
|
||||
// Keys for the three predecessor machines, made where they live and never moved.
|
||||
const keys: Record<string, string> = {};
|
||||
for (const m of [ANCHOR, PEER_A, PEER_B]) {
|
||||
await must(m, "umask 077; wg genkey > /etc/wireguard/predecessor.key");
|
||||
keys[m] = (await must(m, "wg pubkey < /etc/wireguard/predecessor.key")).trim();
|
||||
}
|
||||
await predecessorTunnelOn(ANCHOR, k => [
|
||||
"[Interface]", `PrivateKey = $(cat /etc/wireguard/predecessor.key)`, `ListenPort = ${TUNNEL.port}`, `Address = ${TUNNEL.hub}/24`,
|
||||
"[Peer]", `PublicKey = ${k[PEER_A]}`, `AllowedIPs = ${TUNNEL.a}/32`,
|
||||
"[Peer]", `PublicKey = ${k[PEER_B]}`, `AllowedIPs = ${TUNNEL.b}/32`,
|
||||
].join("\n"), keys);
|
||||
for (const [m, addr] of [[PEER_A, TUNNEL.a], [PEER_B, TUNNEL.b]] as const) {
|
||||
await predecessorTunnelOn(m, k => [
|
||||
"[Interface]", `PrivateKey = $(cat /etc/wireguard/predecessor.key)`, `Address = ${addr}/24`,
|
||||
"[Peer]", `PublicKey = ${k[ANCHOR]}`, `Endpoint = 192.0.2.10:${TUNNEL.port}`, `AllowedIPs = ${TUNNEL.range}`, "PersistentKeepalive = 25",
|
||||
].join("\n"), keys);
|
||||
}
|
||||
// A service reachable only over the tunnel, under a name no catalogue module uses.
|
||||
await must(ANCHOR, `docker run -d --name predecessor-page -p ${TUNNEL.hub}:8081:80 nginx:alpine`);
|
||||
// The predecessor's firewall: the tunnel's port and ssh, nothing else (as ADR 0100's bed).
|
||||
await must(ANCHOR, `ufw --force reset >/dev/null; ufw default deny incoming; ufw allow 22/tcp; ufw allow ${TUNNEL.port}/udp; ufw --force enable`);
|
||||
for (const m of [PEER_A, PEER_B]) assert.ok((await on(m, `curl -fsS --max-time 3 ${SERVICE}`)).ok, `${m} does not reach the service over the tunnel before genesis`);
|
||||
|
||||
wg0Key = (await must(ANCHOR, "wg show wg0 public-key")).trim();
|
||||
wg0Digest = (await must(ANCHOR, "sha256sum /etc/wireguard/wg0.conf")).split(" ")[0];
|
||||
// The probe the peers keep running through the switch: one call a second, failures counted.
|
||||
for (const m of [PEER_A, PEER_B]) await must(m, `nohup sh -c 'while :; do curl -fsS --max-time 1 ${SERVICE} >/dev/null 2>&1 || date +%s >> /tmp/failed; sleep 1; done' >/dev/null 2>&1 &`);
|
||||
});
|
||||
|
||||
after(async () => { if (instanceId) await destroy(instanceId); });
|
||||
|
||||
test("T1 — adopted, the tunnel changes hands and the peers notice nothing", { skip }, async () => {
|
||||
const ran = await genesis({ instanceId, machine: ANCHOR, node: ANCHOR, site: "hosting",
|
||||
flags: ["--adopted", "--endpoint", `192.0.2.10:${TUNNEL.port}`] } as never);
|
||||
assert.match(ran.said, /tunnel\s+wg0/i, `genesis did not say it found and took the tunnel:\n${ran.said}`);
|
||||
|
||||
const ifaces = await must(ANCHOR, "wg show interfaces");
|
||||
assert.match(ifaces, /\bmesh0\b/); assert.doesNotMatch(ifaces, /\bwg0\b/);
|
||||
assert.equal((await on(ANCHOR, "systemctl is-active wg-quick@wg0")).out.trim(), "inactive");
|
||||
assert.equal((await on(ANCHOR, "systemctl is-enabled wg-quick@wg0")).out.trim(), "disabled");
|
||||
assert.equal((await must(ANCHOR, "sha256sum /etc/wireguard/wg0.conf")).split(" ")[0], wg0Digest, "the found configuration was changed or flushed");
|
||||
assert.equal((await must(ANCHOR, "wg show mesh0 public-key")).trim(), wg0Key, "the mesh's interface is not up with the found key");
|
||||
assert.equal((await must(ANCHOR, "wg show mesh0 listen-port")).trim(), String(TUNNEL.port));
|
||||
assert.match(await must(ANCHOR, "ip -o addr show dev mesh0"), new RegExp(TUNNEL.hub.replaceAll(".", "\\.")));
|
||||
const peers = await must(ANCHOR, "wg show mesh0 allowed-ips");
|
||||
assert.match(peers, new RegExp(`${TUNNEL.a}/32`)); assert.match(peers, new RegExp(`${TUNNEL.b}/32`));
|
||||
|
||||
for (const m of [PEER_A, PEER_B]) {
|
||||
const failed = (await on(m, "cat /tmp/failed 2>/dev/null | wc -l")).out.trim();
|
||||
assert.ok(Number(failed) <= 5, `${m} lost the service for ${failed} seconds through the switch`);
|
||||
assert.ok((await on(m, `curl -fsS --max-time 3 ${SERVICE}`)).ok, `${m} does not reach the service after the switch`);
|
||||
}
|
||||
const shown = await control("overlay show");
|
||||
assert.match(shown, /anchor.*hub.*took over on wg0/);
|
||||
assert.match(shown, /peers of the tunnel/); assert.match(shown, /not yet enrolled/);
|
||||
assert.match(await control(`node show ${ANCHOR}`), /tunnel found\s+wg0 on port 51900/);
|
||||
});
|
||||
|
||||
test("T2 — a peer enrols over the tunnel and keeps its address", { skip }, async () => {
|
||||
await control(`node add ${PEER_A} --adopted`);
|
||||
const token = (await control(`token issue --node ${PEER_A}`)).match(/token\s+(\S+)/)?.[1] ?? "";
|
||||
// The token's broker address is the hub's tunnel address: only the tunnel routes it.
|
||||
await must(PEER_A, `mesh-host enrol --token '${token}'`);
|
||||
await control(`overlay place ${PEER_A} --site house`);
|
||||
await control(`assign ${PEER_A} networking`);
|
||||
await control(`push ${PEER_A} --wait 2m`);
|
||||
|
||||
assert.match(await control("overlay show"), new RegExp(`${PEER_A}\\s+${TUNNEL.a.replaceAll(".", "\\.")}`));
|
||||
assert.match(await control("overlay show"), new RegExp(`enrolled as ${PEER_A}`));
|
||||
const onHub = await must(ANCHOR, "wg show mesh0 allowed-ips");
|
||||
assert.equal(onHub.split("\n").filter(l => l.includes(`${TUNNEL.a}/32`)).length, 1, "the enrolled peer's key appears twice on the hub");
|
||||
assert.doesNotMatch(await must(PEER_A, "wg show interfaces"), /\bwg0\b/);
|
||||
assert.ok((await on(PEER_A, `curl -fsS --max-time 3 ${SERVICE}`)).ok);
|
||||
assert.ok((await on(PEER_B, `curl -fsS --max-time 3 ${SERVICE}`)).ok, "the peer that never enrols lost the service");
|
||||
});
|
||||
|
||||
test("T3 — a new machine gets a fresh address from the same range", { skip }, async () => {
|
||||
await control(`node add ${FRESH}`);
|
||||
const token = (await control(`token issue --node ${FRESH}`)).match(/token\s+(\S+)/)?.[1] ?? "";
|
||||
await must(FRESH, `mesh-host enrol --token '${token}'`);
|
||||
await control(`overlay place ${FRESH} --nothing`);
|
||||
await control(`assign ${FRESH} networking`);
|
||||
await control(`push ${FRESH} --wait 2m`);
|
||||
assert.match(await control("overlay show"), new RegExp(`${FRESH}\\s+${TUNNEL.fresh.replaceAll(".", "\\.")}`));
|
||||
assert.ok((await on(FRESH, `ping -c1 -W2 ${TUNNEL.hub}`)).ok, "the new machine does not reach the hub");
|
||||
assert.ok((await on(FRESH, `ping -c1 -W2 ${TUNNEL.a}`)).ok, "the new machine does not reach the enrolled peer");
|
||||
assert.ok((await on(PEER_B, `curl -fsS --max-time 3 ${SERVICE}`)).ok);
|
||||
});
|
||||
|
||||
test("T4 — nothing derived from the address is stale", { skip }, async () => {
|
||||
for (const n of [ANCHOR, PEER_A, FRESH]) {
|
||||
const plan = await control(`plan ${n} --json`);
|
||||
assert.doesNotMatch(plan, /10\.42\./, `${n}'s plan names the mesh's default range`);
|
||||
assert.match(plan, new RegExp(TUNNEL.hub.replaceAll(".", "\\.")));
|
||||
assert.match(await must(n, "cat /etc/hosts"), new RegExp(`${TUNNEL.hub.replaceAll(".", "\\.")}\\s+anchor\\.internal`));
|
||||
}
|
||||
const first = await control(`plan ${PEER_A} --json`);
|
||||
await control("push");
|
||||
assert.equal(await control(`plan ${PEER_A} --json`), first, "a push changed what the plan says");
|
||||
});
|
||||
|
||||
test("N — where a tunnel is not adopted, the ranges must still differ (ADR 0100)", { skip }, async () => {
|
||||
await must(FRESH, "umask 077; printf '[Interface]\\nPrivateKey = %s\\nAddress = 10.10.0.9/24\\n' \"$(wg genkey)\" > /etc/wireguard/wg1.conf; systemctl start wg-quick@wg1");
|
||||
const ran = await genesis({ instanceId, machine: FRESH, node: FRESH, flags: ["--dry-run", "--overlay-range", TUNNEL.range], attempts: 1, verify: false, hostService: false } as never);
|
||||
assert.match(ran.said, /wg1/, `a converged genesis did not refuse the overlapping tunnel it does not adopt:\n${ran.said}`);
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
# THE HUB ADOPTS THE PREDECESSOR'S TUNNEL (novox/hq ADR 0105). See README.md beside this file.
|
||||
#
|
||||
# hosting (public)
|
||||
# anchor 192.0.2.10 the predecessor's hub: wg0 on udp/51900, 10.10.0.1/24, two peers; then the
|
||||
# mesh adopted on it, taking the tunnel over
|
||||
# peer-a 192.0.2.20 a predecessor machine reaching a service on the anchor over the tunnel;
|
||||
# enrols later and keeps 10.10.0.2
|
||||
# peer-b 192.0.2.30 a predecessor machine that never enrols: must notice nothing, ever
|
||||
# fresh 192.0.2.40 a new machine: enrols later and gets 10.10.0.4
|
||||
#
|
||||
# inbound: allow on every machine — the anchor's firewall is the predecessor's, installed by the bed.
|
||||
scenario: adopt-the-tunnel
|
||||
|
||||
segments:
|
||||
hosting:
|
||||
kind: public
|
||||
cidr: [192.0.2.0/24]
|
||||
|
||||
machines:
|
||||
anchor:
|
||||
at: { segment: hosting, address: [192.0.2.10] }
|
||||
egress: true
|
||||
inbound: allow
|
||||
memory: 12GiB
|
||||
cpus: 6
|
||||
disk: 60GiB
|
||||
peer-a:
|
||||
at: { segment: hosting, address: [192.0.2.20] }
|
||||
egress: true
|
||||
inbound: allow
|
||||
memory: 3GiB
|
||||
cpus: 2
|
||||
disk: 20GiB
|
||||
peer-b:
|
||||
at: { segment: hosting, address: [192.0.2.30] }
|
||||
egress: true
|
||||
inbound: allow
|
||||
memory: 2GiB
|
||||
cpus: 2
|
||||
disk: 15GiB
|
||||
fresh:
|
||||
at: { segment: hosting, address: [192.0.2.40] }
|
||||
egress: true
|
||||
inbound: allow
|
||||
memory: 3GiB
|
||||
cpus: 2
|
||||
disk: 20GiB
|
||||
|
||||
place:
|
||||
all: [host, runtime]
|
||||
Reference in New Issue
Block a user