@takodotid/circuit 0.43.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -37
- package/package.json +21 -58
- package/src/checks/across.ts +402 -0
- package/src/checks/built-in.ts +256 -0
- package/src/checks/device.ts +436 -0
- package/src/checks/index.ts +53 -0
- package/src/cli/advisories.ts +108 -0
- package/src/cli/app.ts +71 -0
- package/src/cli/editor-types.generated.ts +30 -0
- package/src/cli/editor-types.ts +69 -0
- package/src/cli/format.ts +124 -0
- package/src/cli/git-hooks.ts +79 -0
- package/src/cli/index.ts +350 -1170
- package/src/cli/migrate.ts +554 -0
- package/src/cli/project-commands.ts +656 -0
- package/src/cli/releases.ts +374 -0
- package/src/cli/remember.ts +339 -0
- package/src/cli/rename.ts +27 -33
- package/src/cli/secrets-password.ts +60 -0
- package/src/cli/secrets.ts +258 -0
- package/src/cli/setup.ts +300 -0
- package/src/cli/signing.ts +23 -0
- package/src/cli/text-editor.ts +268 -0
- package/src/cli/ui.ts +298 -0
- package/src/cli/version.ts +2 -0
- package/src/cli/versions.ts +158 -0
- package/src/config/address.ts +102 -0
- package/src/config/circuit.d.ts +30 -0
- package/src/config/common.ts +65 -0
- package/src/config/communities.ts +479 -0
- package/src/config/define.ts +684 -0
- package/src/config/device.ts +164 -0
- package/src/config/firewall.ts +150 -0
- package/src/config/index.ts +22 -0
- package/src/{schema → config}/interfaces.ts +121 -85
- package/src/config/keys.ts +434 -0
- package/src/config/management.ts +61 -0
- package/src/config/merge.ts +335 -0
- package/src/config/models.ts +70 -0
- package/src/config/monitoring.ts +64 -0
- package/src/config/network.ts +180 -0
- package/src/config/presets.ts +149 -0
- package/src/config/routing.ts +401 -0
- package/src/config/services.ts +51 -0
- package/src/config/switching.ts +84 -0
- package/src/config/system.ts +103 -0
- package/src/{schema → config}/time-zones.ts +1 -1
- package/src/core/address.ts +1 -1
- package/src/core/cabling.ts +8 -43
- package/src/core/graph.ts +40 -0
- package/src/{integrations → core}/registries.ts +5 -3
- package/src/core/secrets-file.ts +539 -0
- package/src/core/secrets.ts +83 -64
- package/src/core/topology.ts +103 -146
- package/src/core/utils.ts +12 -1
- package/src/model.ts +83 -0
- package/src/platforms/derive.ts +64 -37
- package/src/platforms/finding.ts +9 -0
- package/src/platforms/fortios/firewall.ts +76 -64
- package/src/platforms/fortios/index.ts +189 -125
- package/src/platforms/fortios/names.ts +51 -18
- package/src/platforms/fortios/render.ts +69 -62
- package/src/platforms/fortios/routing.ts +89 -58
- package/src/platforms/fortios/vdoms.ts +47 -16
- package/src/platforms/junos/TRAPS.md +2 -2
- package/src/platforms/junos/bgp.ts +63 -42
- package/src/platforms/junos/index.ts +122 -89
- package/src/platforms/junos/render.ts +139 -104
- package/src/platforms/platform.ts +10 -8
- package/src/platforms/raisecom-ros/TRAPS.md +1 -1
- package/src/platforms/raisecom-ros/index.ts +89 -70
- package/src/platforms/raisecom-ros/render.ts +49 -45
- package/src/platforms/routeros/TRAPS.md +3 -3
- package/src/platforms/routeros/index.ts +121 -60
- package/src/platforms/routeros/render.ts +214 -195
- package/src/platforms/vrp/TRAPS.md +1 -1
- package/src/platforms/vrp/index.ts +74 -50
- package/src/platforms/vrp/render.ts +67 -56
- package/src/project/project.ts +725 -0
- package/src/studio/app/env.d.ts +9 -0
- package/src/studio/app/index.html +18 -0
- package/src/studio/app/src/App.vue +95 -0
- package/src/studio/app/src/components/AddressesView.vue +38 -0
- package/src/studio/app/src/components/CablesView.vue +29 -0
- package/src/studio/app/src/components/CompareView.vue +160 -0
- package/src/studio/app/src/components/DataTable.vue +49 -0
- package/src/studio/app/src/components/DeviceView.vue +237 -0
- package/src/studio/app/src/components/DevicesView.vue +32 -0
- package/src/studio/app/src/components/Faceplate.vue +75 -0
- package/src/studio/app/src/components/GraphView.vue +282 -0
- package/src/studio/app/src/components/LineDiff.vue +24 -0
- package/src/studio/app/src/components/ProblemsView.vue +42 -0
- package/src/studio/app/src/components/SearchView.vue +72 -0
- package/src/studio/app/src/components/SecretsView.vue +112 -0
- package/src/studio/app/src/components/SessionsView.vue +35 -0
- package/src/studio/app/src/components/Settings.vue +95 -0
- package/src/studio/app/src/components/ToolsView.vue +118 -0
- package/src/studio/app/src/components/VlansView.vue +50 -0
- package/src/studio/app/src/lib/csv.ts +19 -0
- package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
- package/src/studio/app/src/lib/format.ts +38 -0
- package/src/studio/app/src/lib/net.ts +68 -0
- package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
- package/src/studio/app/src/lib/store.ts +71 -0
- package/src/studio/app/src/lib/tools.ts +217 -0
- package/src/studio/app/src/main.ts +9 -0
- package/src/studio/{public → app/src}/style.css +14 -4
- package/src/studio/app/tsconfig.json +17 -0
- package/src/studio/app/vite.config.ts +16 -0
- package/src/studio/assets.generated.ts +3 -0
- package/src/studio/data.ts +96 -188
- package/src/studio/git.ts +27 -43
- package/src/studio/server.ts +196 -107
- package/src/studio/types.ts +141 -0
- package/src/transport/console.ts +3 -3
- package/src/transport/ssh.ts +1 -1
- package/dist/chunks/index-3b343xva.js +0 -177
- package/dist/chunks/index-6wb41q67.js +0 -62
- package/dist/chunks/index-n7w3tg5y.js +0 -149
- package/dist/chunks/index-yzw86rc8.js +0 -921
- package/dist/chunks/new-w3zys5y4.js +0 -172
- package/dist/chunks/server-cxr9ynxc.js +0 -259
- package/dist/cli/index.js +0 -12922
- package/dist/helpers/index.js +0 -881
- package/dist/index.js +0 -28
- package/dist/types/core/address-space.d.ts +0 -62
- package/dist/types/core/address.d.ts +0 -44
- package/dist/types/core/cabling.d.ts +0 -41
- package/dist/types/core/cross-checks.d.ts +0 -28
- package/dist/types/core/define.d.ts +0 -79
- package/dist/types/core/project-files.d.ts +0 -12
- package/dist/types/core/schema-docs.d.ts +0 -25
- package/dist/types/core/secrets.d.ts +0 -62
- package/dist/types/core/topology.d.ts +0 -96
- package/dist/types/core/utils.d.ts +0 -10
- package/dist/types/core/validator.d.ts +0 -23
- package/dist/types/helpers/bgp.d.ts +0 -195
- package/dist/types/helpers/cables.d.ts +0 -69
- package/dist/types/helpers/checks.d.ts +0 -36
- package/dist/types/helpers/exchange.d.ts +0 -55
- package/dist/types/helpers/filters.d.ts +0 -51
- package/dist/types/helpers/index.d.ts +0 -14
- package/dist/types/helpers/route-server.d.ts +0 -52
- package/dist/types/helpers/upstreams.d.ts +0 -192
- package/dist/types/index.d.ts +0 -10
- package/dist/types/integrations/librenms.d.ts +0 -60
- package/dist/types/integrations/netbox.d.ts +0 -89
- package/dist/types/integrations/peeringdb.d.ts +0 -24
- package/dist/types/integrations/prometheus.d.ts +0 -24
- package/dist/types/integrations/registries.d.ts +0 -8
- package/dist/types/platforms/bgp-wait.d.ts +0 -24
- package/dist/types/platforms/catalog.d.ts +0 -109
- package/dist/types/platforms/derive.d.ts +0 -17
- package/dist/types/platforms/fortios/firewall.d.ts +0 -47
- package/dist/types/platforms/fortios/index.d.ts +0 -140
- package/dist/types/platforms/fortios/models.d.ts +0 -14
- package/dist/types/platforms/fortios/names.d.ts +0 -52
- package/dist/types/platforms/fortios/plan.d.ts +0 -114
- package/dist/types/platforms/fortios/render.d.ts +0 -77
- package/dist/types/platforms/fortios/routing.d.ts +0 -56
- package/dist/types/platforms/fortios/tree.d.ts +0 -37
- package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
- package/dist/types/platforms/index.d.ts +0 -13
- package/dist/types/platforms/junos/bgp.d.ts +0 -44
- package/dist/types/platforms/junos/index.d.ts +0 -114
- package/dist/types/platforms/junos/models.d.ts +0 -14
- package/dist/types/platforms/junos/render.d.ts +0 -154
- package/dist/types/platforms/line-diff.d.ts +0 -67
- package/dist/types/platforms/platform.d.ts +0 -135
- package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
- package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
- package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
- package/dist/types/platforms/routeros/export.d.ts +0 -59
- package/dist/types/platforms/routeros/index.d.ts +0 -116
- package/dist/types/platforms/routeros/models.d.ts +0 -18
- package/dist/types/platforms/routeros/plan.d.ts +0 -58
- package/dist/types/platforms/routeros/render.d.ts +0 -250
- package/dist/types/platforms/vrp/index.d.ts +0 -83
- package/dist/types/platforms/vrp/models.d.ts +0 -12
- package/dist/types/platforms/vrp/render.d.ts +0 -96
- package/dist/types/schema/common.d.ts +0 -30
- package/dist/types/schema/device.d.ts +0 -126
- package/dist/types/schema/firewall.d.ts +0 -131
- package/dist/types/schema/index.d.ts +0 -8
- package/dist/types/schema/interfaces.d.ts +0 -205
- package/dist/types/schema/policy.d.ts +0 -107
- package/dist/types/schema/routing.d.ts +0 -161
- package/dist/types/schema/system.d.ts +0 -198
- package/dist/types/schema/time-zones.d.ts +0 -2
- package/dist/types/transport/console.d.ts +0 -115
- package/dist/types/transport/path-check.d.ts +0 -32
- package/dist/types/transport/ssh.d.ts +0 -27
- package/src/cli/graph-format.ts +0 -91
- package/src/cli/new.ts +0 -234
- package/src/core/address-space.ts +0 -320
- package/src/core/cross-checks.ts +0 -369
- package/src/core/define.ts +0 -124
- package/src/core/project-files.ts +0 -35
- package/src/core/schema-docs.ts +0 -174
- package/src/core/validator.ts +0 -508
- package/src/helpers/bgp.ts +0 -473
- package/src/helpers/cables.ts +0 -74
- package/src/helpers/checks.ts +0 -201
- package/src/helpers/exchange.ts +0 -123
- package/src/helpers/filters.ts +0 -107
- package/src/helpers/index.ts +0 -25
- package/src/helpers/route-server.ts +0 -212
- package/src/helpers/upstreams.ts +0 -369
- package/src/index.ts +0 -13
- package/src/integrations/librenms.ts +0 -291
- package/src/integrations/netbox.ts +0 -620
- package/src/integrations/peeringdb.ts +0 -173
- package/src/integrations/prometheus.ts +0 -90
- package/src/schema/common.ts +0 -41
- package/src/schema/device.ts +0 -145
- package/src/schema/firewall.ts +0 -140
- package/src/schema/index.ts +0 -8
- package/src/schema/policy.ts +0 -115
- package/src/schema/routing.ts +0 -173
- package/src/schema/system.ts +0 -203
- package/src/studio/public/app.js +0 -186
- package/src/studio/public/compare.js +0 -250
- package/src/studio/public/dom.js +0 -138
- package/src/studio/public/evaluate.js +0 -687
- package/src/studio/public/export.js +0 -130
- package/src/studio/public/graph.js +0 -1541
- package/src/studio/public/index.html +0 -83
- package/src/studio/public/net.js +0 -134
- package/src/studio/public/tables.js +0 -1289
- package/src/studio/public/tools.js +0 -610
- package/templates/colocation/checks.ts +0 -53
- package/templates/colocation/circuit.config.ts +0 -9
- package/templates/colocation/edge-01.ts +0 -133
- package/templates/colocation/routing.ts +0 -13
- package/templates/colocation/tenancy.ts +0 -76
- package/templates/colocation/tenants/acme.ts +0 -19
- package/templates/colocation/tenants/index.ts +0 -6
- package/templates/colocation/tor-01.ts +0 -33
- package/templates/edge-router/checks.ts +0 -20
- package/templates/edge-router/circuit.config.ts +0 -8
- package/templates/edge-router/edge-01.ts +0 -110
- package/templates/edge-router/routing.ts +0 -13
- package/templates/project/AGENTS.md +0 -51
- package/templates/project/CLAUDE.md +0 -1
- package/templates/project/README.md +0 -16
- package/templates/project/gitignore +0 -5
- package/templates/project/tsconfig.json +0 -13
- package/templates/single-site/checks.ts +0 -26
- package/templates/single-site/circuit.config.ts +0 -8
- package/templates/single-site/router.ts +0 -86
- package/templates/single-site/site.ts +0 -18
- package/templates/single-site/switch.ts +0 -34
- /package/src/studio/{public → app/public}/favicon.svg +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Client, type ClientChannel } from "ssh2";
|
|
2
|
-
import type { Device } from "../schema/index.js";
|
|
3
|
-
export type Login = {
|
|
4
|
-
host: string;
|
|
5
|
-
port?: number;
|
|
6
|
-
user: string;
|
|
7
|
-
password: string;
|
|
8
|
-
/** Host key algorithms to accept, for a device that negotiates one it then signs wrongly. */
|
|
9
|
-
hostKeys?: string[];
|
|
10
|
-
};
|
|
11
|
-
export declare class Ssh {
|
|
12
|
-
/** The address SSH reaches a device at. A device without one is reached only through its console. */
|
|
13
|
-
static hostOf(device: Device): string;
|
|
14
|
-
/** Open a session. */
|
|
15
|
-
static connect(login: Login): Promise<Client>;
|
|
16
|
-
/** Run one command and return what it printed, stderr included. */
|
|
17
|
-
static exec(client: Client, command: string, timeout?: number): Promise<string>;
|
|
18
|
-
/** An interactive shell, for platforms that take configuration only there. */
|
|
19
|
-
static shell(client: Client, columns?: number, rows?: number): Promise<ClientChannel>;
|
|
20
|
-
/** Collect output until `done` holds, then wait `settle` for stragglers, or give up at `timeout`. */
|
|
21
|
-
static readUntil(channel: ClientChannel, done: (output: string) => boolean, timeout: number, settle?: number): Promise<string>;
|
|
22
|
-
/** Send one line into an interactive shell and collect output until it goes quiet for `settle`. */
|
|
23
|
-
static send(channel: ClientChannel, line: string, timeout?: number, settle?: number): Promise<string>;
|
|
24
|
-
static sleep(milliseconds: number): Promise<unknown>;
|
|
25
|
-
/** Write a file to the device over SFTP. */
|
|
26
|
-
static upload(client: Client, path: string, content: string | Buffer): Promise<void>;
|
|
27
|
-
}
|
package/src/cli/graph-format.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
// What `circuit graph` prints: the graph of the network with what `validate` found and how each device compares with its last snapshot, as Mermaid, DOT or JSON.
|
|
2
|
-
|
|
3
|
-
import { Topology, type Annotated, type GraphNode } from "../core/topology";
|
|
4
|
-
|
|
5
|
-
export class GraphFormat {
|
|
6
|
-
/** `error` when validate found an error on the device, `warning` for a warning or a difference from its snapshot. */
|
|
7
|
-
static mark(graph: Annotated, node: GraphNode): "error" | "warning" | undefined {
|
|
8
|
-
if (node.kind !== "device") return undefined;
|
|
9
|
-
|
|
10
|
-
const findings = graph.findings.filter((finding) => finding.device && Topology.deviceId(finding.device) === node.id);
|
|
11
|
-
if (findings.some((finding) => finding.level === "error")) return "error";
|
|
12
|
-
|
|
13
|
-
const drift = graph.drift[node.label]?.status;
|
|
14
|
-
if (findings.length || drift === "differs" || drift === "cannot plan") return "warning";
|
|
15
|
-
|
|
16
|
-
return undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Mermaid, which GitHub draws wherever Markdown is shown. */
|
|
20
|
-
static mermaid(graph: Annotated): string {
|
|
21
|
-
const ids = new Map(graph.nodes.map((node, index) => [node.id, `n${index}`]));
|
|
22
|
-
// Lines of text, each escaped, as one quoted label.
|
|
23
|
-
const text = (...lines: string[]) =>
|
|
24
|
-
`"${lines.map((line) => line.replace(/"/g, "#quot;").replace(/</g, "#lt;").replace(/>/g, "#gt;")).join("<br/>")}"`;
|
|
25
|
-
|
|
26
|
-
const shapes: Record<GraphNode["kind"], (label: string) => string> = {
|
|
27
|
-
device: (label) => `[${label}]`,
|
|
28
|
-
segment: (label) => `([${label}])`,
|
|
29
|
-
outside: (label) => `{{${label}}}`,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const lines = ["graph LR"];
|
|
33
|
-
|
|
34
|
-
for (const node of graph.nodes) {
|
|
35
|
-
const label = node.kind === "device" ? text(node.label, node.detail[0] ?? "") : text(node.label);
|
|
36
|
-
lines.push(` ${ids.get(node.id)}${shapes[node.kind](label)}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
for (const edge of graph.edges) {
|
|
40
|
-
const arrow = edge.oneSided ? "-.-" : "---";
|
|
41
|
-
lines.push(` ${ids.get(edge.from)} ${arrow}|${text(edge.label)}| ${ids.get(edge.to)}`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
lines.push(
|
|
45
|
-
" classDef error stroke:#d1242f,stroke-width:3px",
|
|
46
|
-
" classDef warning stroke:#bf8700,stroke-width:3px",
|
|
47
|
-
" classDef outside stroke-dasharray:4 3"
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
for (const node of graph.nodes) {
|
|
51
|
-
const mark = GraphFormat.mark(graph, node) ?? (node.kind === "outside" ? "outside" : undefined);
|
|
52
|
-
if (mark) lines.push(` class ${ids.get(node.id)} ${mark}`);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return lines.join("\n") + "\n";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** DOT, which Graphviz turns into an image: `circuit graph --format=dot | dot -Tsvg > network.svg`. */
|
|
59
|
-
static dot(graph: Annotated): string {
|
|
60
|
-
const text = (value: string) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
61
|
-
const colors = { error: "#d1242f", warning: "#bf8700" };
|
|
62
|
-
|
|
63
|
-
const shapes: Record<GraphNode["kind"], string> = {
|
|
64
|
-
device: "shape=box, style=rounded",
|
|
65
|
-
segment: "shape=ellipse",
|
|
66
|
-
outside: "shape=box, style=dashed",
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const lines = [
|
|
70
|
-
"graph network {",
|
|
71
|
-
" rankdir=LR;",
|
|
72
|
-
' node [fontname="Helvetica"];',
|
|
73
|
-
' edge [fontname="Helvetica", fontsize=10];',
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
for (const node of graph.nodes) {
|
|
77
|
-
const label = node.kind === "device" ? `${node.label}\n${node.detail[0] ?? ""}` : node.label;
|
|
78
|
-
const mark = GraphFormat.mark(graph, node);
|
|
79
|
-
const color = mark ? `, color=${text(colors[mark])}, penwidth=2` : "";
|
|
80
|
-
lines.push(` ${text(node.id)} [label=${text(label)}, ${shapes[node.kind]}${color}];`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const edge of graph.edges) {
|
|
84
|
-
const style = edge.oneSided ? ", style=dashed" : "";
|
|
85
|
-
lines.push(` ${text(edge.from)} -- ${text(edge.to)} [label=${text(edge.label)}${style}];`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
lines.push("}");
|
|
89
|
-
return lines.join("\n") + "\n";
|
|
90
|
-
}
|
|
91
|
-
}
|
package/src/cli/new.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
// `circuit new`: start a project from one of the patterns in templates/. In a terminal it asks what it needs; the same answers can be given as options, for a script or an AI agent.
|
|
2
|
-
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import * as prompts from "@clack/prompts";
|
|
8
|
-
|
|
9
|
-
type Manager = keyof typeof Scaffold.MANAGERS;
|
|
10
|
-
|
|
11
|
-
type Pattern = keyof typeof Scaffold.PATTERNS;
|
|
12
|
-
|
|
13
|
-
/** The answers given as options. A question is asked only for an answer missing here. */
|
|
14
|
-
export type ScaffoldOptions = {
|
|
15
|
-
pattern?: string;
|
|
16
|
-
asn?: string;
|
|
17
|
-
onePassword?: boolean;
|
|
18
|
-
use?: string;
|
|
19
|
-
install: boolean;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export class Scaffold {
|
|
23
|
-
private static readonly ROOT = Scaffold.packageRoot();
|
|
24
|
-
private static readonly TEMPLATES = join(Scaffold.ROOT, "templates");
|
|
25
|
-
private static readonly CIRCUIT = JSON.parse(readFileSync(join(Scaffold.ROOT, "package.json"), "utf8"));
|
|
26
|
-
|
|
27
|
-
/** How each package manager installs and runs. The templates are written for npm; the others are swapped in. */
|
|
28
|
-
static readonly MANAGERS = {
|
|
29
|
-
npm: { install: "npm install", run: "npx circuit", types: "node" },
|
|
30
|
-
pnpm: { install: "pnpm install", run: "pnpm circuit", types: "node" },
|
|
31
|
-
bun: { install: "bun install", run: "bun circuit", types: "bun" },
|
|
32
|
-
} as const;
|
|
33
|
-
|
|
34
|
-
static readonly PATTERNS = {
|
|
35
|
-
"single-site": {
|
|
36
|
-
title: "A single site",
|
|
37
|
-
fits: "a home, an office or an internal network: a router and a switch, no BGP",
|
|
38
|
-
asn: false,
|
|
39
|
-
},
|
|
40
|
-
"edge-router": { title: "An edge router", fits: "your own AS number, with an IP transit and an internet exchange", asn: true },
|
|
41
|
-
colocation: {
|
|
42
|
-
title: "Colocation with tenants",
|
|
43
|
-
fits: "a provider whose customers each get their own VLANs, addresses and ports",
|
|
44
|
-
asn: true,
|
|
45
|
-
},
|
|
46
|
-
} as const;
|
|
47
|
-
|
|
48
|
-
/** The AS number the templates use, reserved for documentation by RFC 5398. */
|
|
49
|
-
private static readonly EXAMPLE_ASN = "64500";
|
|
50
|
-
|
|
51
|
-
/** Circuit's own directory: this file runs from src/cli/ in Bun, and from dist/ once built. */
|
|
52
|
-
private static packageRoot(): string {
|
|
53
|
-
let directory = dirname(fileURLToPath(import.meta.url));
|
|
54
|
-
|
|
55
|
-
while (!existsSync(join(directory, "templates"))) {
|
|
56
|
-
const parent = dirname(directory);
|
|
57
|
-
if (parent === directory) throw new Error("Circuit's templates are missing from its package");
|
|
58
|
-
directory = parent;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return directory;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** The package manager that started this, from what it says about itself, such as `pnpm/9.1.0 npm/? node/v22`. */
|
|
65
|
-
private static detectManager(): Manager {
|
|
66
|
-
const agent = process.env.npm_config_user_agent ?? "";
|
|
67
|
-
if (agent.startsWith("pnpm/")) return "pnpm";
|
|
68
|
-
if (agent.startsWith("bun/") || process.versions.bun) return "bun";
|
|
69
|
-
return "npm";
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private static fail(message: string): never {
|
|
73
|
-
console.error(message);
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Every file under a directory, relative to it. */
|
|
78
|
-
private static filesIn(directory: string): string[] {
|
|
79
|
-
return readdirSync(directory, { recursive: true, withFileTypes: true })
|
|
80
|
-
.filter((entry) => entry.isFile())
|
|
81
|
-
.map((entry) => relative(directory, join(entry.parentPath, entry.name)));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Why a project cannot be created in this directory, or undefined when it can. */
|
|
85
|
-
private static notEmpty(directory: string): string | undefined {
|
|
86
|
-
const target = resolve(process.cwd(), directory);
|
|
87
|
-
return existsSync(target) && readdirSync(target).length ? `${target} is not empty` : undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** The answer to a question in a terminal, or the fallback when nobody is there to answer. Cancelling a question ends the run. */
|
|
91
|
-
private static async ask<T>(question: () => Promise<NoInfer<T> | symbol>, fallback: T): Promise<T> {
|
|
92
|
-
if (process.stdin.isTTY !== true) return fallback;
|
|
93
|
-
|
|
94
|
-
const answer = await question();
|
|
95
|
-
if (prompts.isCancel(answer)) {
|
|
96
|
-
prompts.cancel("Nothing created.");
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return answer as T;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
static async create(directoryArgument: string | undefined, options: ScaffoldOptions): Promise<void> {
|
|
104
|
-
const directory =
|
|
105
|
-
directoryArgument ??
|
|
106
|
-
(await Scaffold.ask(
|
|
107
|
-
() =>
|
|
108
|
-
prompts.text({
|
|
109
|
-
message: "Directory for the project",
|
|
110
|
-
placeholder: "my-network",
|
|
111
|
-
defaultValue: "my-network",
|
|
112
|
-
validate: (value) => Scaffold.notEmpty(value || "my-network"),
|
|
113
|
-
}),
|
|
114
|
-
"my-network"
|
|
115
|
-
));
|
|
116
|
-
|
|
117
|
-
const notEmpty = Scaffold.notEmpty(directory);
|
|
118
|
-
if (notEmpty) Scaffold.fail(notEmpty);
|
|
119
|
-
const target = resolve(process.cwd(), directory);
|
|
120
|
-
|
|
121
|
-
const names = Object.keys(Scaffold.PATTERNS) as Pattern[];
|
|
122
|
-
const pattern =
|
|
123
|
-
options.pattern ??
|
|
124
|
-
(await Scaffold.ask(
|
|
125
|
-
() =>
|
|
126
|
-
prompts.select({
|
|
127
|
-
message: "Which is closest to your network?",
|
|
128
|
-
options: names.map((name) => ({
|
|
129
|
-
value: name,
|
|
130
|
-
label: Scaffold.PATTERNS[name].title,
|
|
131
|
-
hint: Scaffold.PATTERNS[name].fits,
|
|
132
|
-
})),
|
|
133
|
-
}),
|
|
134
|
-
names[0]!
|
|
135
|
-
));
|
|
136
|
-
if (!Object.hasOwn(Scaffold.PATTERNS, pattern)) Scaffold.fail(`no pattern ${pattern}; there are ${names.join(", ")}`);
|
|
137
|
-
const chosen = Scaffold.PATTERNS[pattern as Pattern];
|
|
138
|
-
|
|
139
|
-
let asn = options.asn;
|
|
140
|
-
if (!asn && chosen.asn) {
|
|
141
|
-
asn = await Scaffold.ask(
|
|
142
|
-
() =>
|
|
143
|
-
prompts.text({
|
|
144
|
-
message: "Your AS number, or press enter to keep the documentation example",
|
|
145
|
-
placeholder: Scaffold.EXAMPLE_ASN,
|
|
146
|
-
defaultValue: Scaffold.EXAMPLE_ASN,
|
|
147
|
-
validate: (value) => (!value || /^\d+$/.test(value) ? undefined : `${value} is not an AS number`),
|
|
148
|
-
}),
|
|
149
|
-
Scaffold.EXAMPLE_ASN
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
if (asn && !/^\d+$/.test(asn)) Scaffold.fail(`${asn} is not an AS number`);
|
|
153
|
-
|
|
154
|
-
const manager = options.use ?? Scaffold.detectManager();
|
|
155
|
-
if (!Object.hasOwn(Scaffold.MANAGERS, manager)) {
|
|
156
|
-
Scaffold.fail(`no package manager ${manager}; there are ${Object.keys(Scaffold.MANAGERS).join(", ")}`);
|
|
157
|
-
}
|
|
158
|
-
const { install, run, types } = Scaffold.MANAGERS[manager as Manager];
|
|
159
|
-
|
|
160
|
-
const onePassword =
|
|
161
|
-
options.onePassword ||
|
|
162
|
-
(await Scaffold.ask(() => prompts.confirm({ message: "Keep the secrets in 1Password?", initialValue: false }), false));
|
|
163
|
-
|
|
164
|
-
// The pattern's files, then what every project has. npm leaves out a file named .gitignore, so it travels without the dot.
|
|
165
|
-
cpSync(join(Scaffold.TEMPLATES, pattern), target, { recursive: true });
|
|
166
|
-
cpSync(join(Scaffold.TEMPLATES, "project"), target, { recursive: true });
|
|
167
|
-
renameSync(join(target, "gitignore"), join(target, ".gitignore"));
|
|
168
|
-
|
|
169
|
-
// What the templates say about npm, said for the package manager in use.
|
|
170
|
-
for (const file of ["AGENTS.md", "README.md", "tsconfig.json"]) {
|
|
171
|
-
const path = join(target, file);
|
|
172
|
-
const text = readFileSync(path, "utf8")
|
|
173
|
-
.replaceAll("npx circuit", run)
|
|
174
|
-
.replaceAll("npm install", install)
|
|
175
|
-
.replaceAll('"types": ["node"]', `"types": ["${types}"]`);
|
|
176
|
-
writeFileSync(path, text);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const sources = Scaffold.filesIn(target).filter((file) => file.endsWith(".ts"));
|
|
180
|
-
const secrets = new Set<string>();
|
|
181
|
-
|
|
182
|
-
for (const file of sources) {
|
|
183
|
-
const path = join(target, file);
|
|
184
|
-
let text = readFileSync(path, "utf8");
|
|
185
|
-
|
|
186
|
-
if (asn && asn !== Scaffold.EXAMPLE_ASN) text = text.replaceAll(new RegExp(`\\b${Scaffold.EXAMPLE_ASN}\\b`, "g"), asn);
|
|
187
|
-
for (const [, name] of text.matchAll(/secret\("([A-Z0-9_]+)"\)/g)) secrets.add(name!);
|
|
188
|
-
|
|
189
|
-
writeFileSync(path, text);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const env = [
|
|
193
|
-
"# Secrets for Circuit. Never commit this file.",
|
|
194
|
-
"# A value is the secret itself, or a 1Password reference, op://vault/item/field, read with the 1Password CLI.",
|
|
195
|
-
"# Put quotes around a value that has a # in it: without them, # starts a comment.",
|
|
196
|
-
...[...secrets].sort().map((name) => (onePassword ? `${name}=op://Network/${name}/password` : `${name}=`)),
|
|
197
|
-
"",
|
|
198
|
-
];
|
|
199
|
-
writeFileSync(join(target, ".env"), env.join("\n"));
|
|
200
|
-
|
|
201
|
-
const name = basename(target)
|
|
202
|
-
.toLowerCase()
|
|
203
|
-
.replace(/[^a-z0-9-]+/g, "-");
|
|
204
|
-
const packageJson = {
|
|
205
|
-
name,
|
|
206
|
-
private: true,
|
|
207
|
-
type: "module",
|
|
208
|
-
dependencies: { "@takodotid/circuit": Scaffold.CIRCUIT.version },
|
|
209
|
-
devDependencies: { [`@types/${types}`]: Scaffold.CIRCUIT.devDependencies[`@types/${types}`] },
|
|
210
|
-
};
|
|
211
|
-
writeFileSync(join(target, "package.json"), JSON.stringify(packageJson, null, 4) + "\n");
|
|
212
|
-
|
|
213
|
-
if (options.install) {
|
|
214
|
-
// npm and pnpm are scripts on Windows, which only a shell runs.
|
|
215
|
-
const result = spawnSync(install, { cwd: target, stdio: "inherit", shell: true });
|
|
216
|
-
if (result.status !== 0) console.log(`${install} failed; run it again in the project.`);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const steps = [
|
|
220
|
-
`cd ${directory}`,
|
|
221
|
-
onePassword
|
|
222
|
-
? `Point each line of .env at the item that holds it in 1Password, then run: ${run} secrets`
|
|
223
|
-
: `Fill in each password in .env, then run: ${run} secrets`,
|
|
224
|
-
"Change the names, addresses and ports in the device files to your own.",
|
|
225
|
-
...(chosen.asn && (!asn || asn === Scaffold.EXAMPLE_ASN) ? ["Put your own AS number in routing.ts."] : []),
|
|
226
|
-
`${run} validate`,
|
|
227
|
-
`${run} snapshot, then ${run} diff, to see what would change on each device`,
|
|
228
|
-
];
|
|
229
|
-
|
|
230
|
-
console.log(`\nCreated ${directory} from the pattern "${chosen.title}".\n\nNext:`);
|
|
231
|
-
steps.forEach((step, index) => console.log(` ${index + 1}. ${step}`));
|
|
232
|
-
console.log(`\nWhat each file does: https://circuit.tako.id/patterns/${pattern}`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
// Every address the config uses, by prefix, and what is still free: what `circuit addresses` prints. It reads the config only, so an address a device gave out by itself, such as a DHCP lease, is not known.
|
|
2
|
-
|
|
3
|
-
import type { Device } from "../schema";
|
|
4
|
-
import { Address } from "./address";
|
|
5
|
-
import { Topology } from "./topology";
|
|
6
|
-
|
|
7
|
-
/** One address or range in a segment, and what holds it. */
|
|
8
|
-
export type Use = {
|
|
9
|
-
/** An address, or `first-last` for a range. */
|
|
10
|
-
address: string;
|
|
11
|
-
what: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
/** A prefix our devices have addresses in. */
|
|
15
|
-
export type Segment = {
|
|
16
|
-
prefix: string;
|
|
17
|
-
uses: Use[];
|
|
18
|
-
/** How many addresses no one uses, as a number written out, since an IPv6 count does not fit a JavaScript number. */
|
|
19
|
-
free: string;
|
|
20
|
-
/** The lowest free address, when there is one. */
|
|
21
|
-
next?: string;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/** A prefix in use without being a segment: a loopback's address, or a block routed elsewhere. */
|
|
25
|
-
export type Block = {
|
|
26
|
-
prefix: string;
|
|
27
|
-
what: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** Space the network owns: a prefix it originates in BGP, or one asked for. Everything in use inside it, and what is free. */
|
|
31
|
-
export type Space = {
|
|
32
|
-
prefix: string;
|
|
33
|
-
/** Why it is listed, such as `originated by edge-01`. */
|
|
34
|
-
owner: string;
|
|
35
|
-
segments: Segment[];
|
|
36
|
-
blocks: Block[];
|
|
37
|
-
/** The fewest prefixes that cover what nothing uses. */
|
|
38
|
-
free: string[];
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type AddressReport = {
|
|
42
|
-
spaces: Space[];
|
|
43
|
-
/** What is in use outside every space. Left out when a prefix is asked for. */
|
|
44
|
-
elsewhere: { segments: Segment[]; blocks: Block[] };
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/** A use as a range of numbers, for counting. */
|
|
48
|
-
type Taken = { first: bigint; last: bigint };
|
|
49
|
-
|
|
50
|
-
export class AddressSpace {
|
|
51
|
-
private static isHostRoute(cidr: string): boolean {
|
|
52
|
-
return Address.split(cidr).len === Address.bits(Address.split(cidr).family);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private static numberOf(address: string): bigint {
|
|
56
|
-
return Address.toNumber(address)!;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Addresses a segment can give out: IPv4 leaves out the network and broadcast addresses below /31, and IPv6 the subnet-router anycast address. */
|
|
60
|
-
private static usable(prefix: string): Taken {
|
|
61
|
-
const { first, last, family } = Address.range(prefix);
|
|
62
|
-
const { len } = Address.split(prefix);
|
|
63
|
-
|
|
64
|
-
if (family === "ipv4" && len <= 30) return { first: first + 1n, last: last - 1n };
|
|
65
|
-
if (family === "ipv6" && len < 127) return { first: first + 1n, last };
|
|
66
|
-
|
|
67
|
-
return { first, last };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Ranges merged, so overlapping ones count once. */
|
|
71
|
-
private static merged(ranges: Taken[]): Taken[] {
|
|
72
|
-
const sorted = [...ranges].sort((a, b) => (a.first < b.first ? -1 : a.first > b.first ? 1 : 0));
|
|
73
|
-
const result: Taken[] = [];
|
|
74
|
-
|
|
75
|
-
for (const range of sorted) {
|
|
76
|
-
const previous = result.at(-1);
|
|
77
|
-
if (previous && range.first <= previous.last + 1n) previous.last = range.last > previous.last ? range.last : previous.last;
|
|
78
|
-
else result.push({ ...range });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** What is inside `within` and not in any of the ranges. */
|
|
85
|
-
private static gaps(within: Taken, taken: Taken[]): Taken[] {
|
|
86
|
-
const result: Taken[] = [];
|
|
87
|
-
let cursor = within.first;
|
|
88
|
-
|
|
89
|
-
for (const range of AddressSpace.merged(taken)) {
|
|
90
|
-
if (range.last < within.first || range.first > within.last) continue;
|
|
91
|
-
if (range.first > cursor) result.push({ first: cursor, last: range.first - 1n });
|
|
92
|
-
if (range.last + 1n > cursor) cursor = range.last + 1n;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (cursor <= within.last) result.push({ first: cursor, last: within.last });
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** `12` or, for a count too long to read, `about 2^64`, to the nearest power of two. */
|
|
100
|
-
private static count(value: bigint): string {
|
|
101
|
-
if (value <= 1n << 20n) return String(value);
|
|
102
|
-
|
|
103
|
-
const power = value.toString(2).length - 1;
|
|
104
|
-
const nearer = value - (1n << BigInt(power)) >= 1n << BigInt(power - 1) ? power + 1 : power;
|
|
105
|
-
return `about 2^${nearer}`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
private static byAddress(a: Use, b: Use): number {
|
|
109
|
-
const first = (use: Use) => AddressSpace.numberOf(use.address.split("-")[0]!);
|
|
110
|
-
return first(a) < first(b) ? -1 : first(a) > first(b) ? 1 : 0;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** IPv4 before IPv6, then by first address. */
|
|
114
|
-
private static comparePrefixes(a: string, b: string): number {
|
|
115
|
-
const [ra, rb] = [Address.range(a), Address.range(b)];
|
|
116
|
-
if (ra.family !== rb.family) return ra.family === "ipv4" ? -1 : 1;
|
|
117
|
-
return ra.first < rb.first ? -1 : ra.first > rb.first ? 1 : 0;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
private static byPrefix(a: { prefix: string }, b: { prefix: string }): number {
|
|
121
|
-
return AddressSpace.comparePrefixes(a.prefix, b.prefix);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Every address in use, by segment, under the space the network owns. With `within`, only what falls inside that prefix, as the one space. */
|
|
125
|
-
static report(devices: readonly Device[], within?: string): AddressReport {
|
|
126
|
-
const segments = new Map<string, { uses: Use[]; taken: Taken[] }>();
|
|
127
|
-
const blocks: Block[] = [];
|
|
128
|
-
|
|
129
|
-
const segmentOf = (address: string) => [...segments.keys()].find((prefix) => Address.contains(prefix, address));
|
|
130
|
-
|
|
131
|
-
// Segments come from the addresses on interfaces; a host route, such as a loopback's, is a block of its own.
|
|
132
|
-
for (const holder of Topology.holders(devices)) {
|
|
133
|
-
const vrf = holder.device.ports?.[holder.name]?.vrf ?? holder.device.interfaces?.[holder.name]?.vrf;
|
|
134
|
-
const what = `${holder.device.name} ${holder.name}${vrf ? `, VRF ${vrf}` : ""}`;
|
|
135
|
-
|
|
136
|
-
if (AddressSpace.isHostRoute(holder.address) || holder.peer) {
|
|
137
|
-
blocks.push({ prefix: `${Address.host(holder.address)}/${Address.bits(Address.family(holder.address))}`, what });
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const prefix = Address.prefixOf(holder.address);
|
|
142
|
-
const segment = segments.get(prefix) ?? { uses: [], taken: [] };
|
|
143
|
-
const value = AddressSpace.numberOf(Address.host(holder.address));
|
|
144
|
-
|
|
145
|
-
segment.uses.push({ address: Address.host(holder.address), what });
|
|
146
|
-
segment.taken.push({ first: value, last: value });
|
|
147
|
-
segments.set(prefix, segment);
|
|
148
|
-
|
|
149
|
-
for (const group of holder.vrrp) {
|
|
150
|
-
if (!Address.isValid(group.address) || !Address.contains(prefix, group.address)) continue;
|
|
151
|
-
if (segment.uses.some((use) => use.address === group.address)) continue;
|
|
152
|
-
|
|
153
|
-
const holders = devices.filter((device) =>
|
|
154
|
-
Object.values({ ...device.ports, ...device.interfaces }).some((settings) =>
|
|
155
|
-
settings?.vrrp?.some((other) => other.id === group.id && other.address === group.address)
|
|
156
|
-
)
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
segment.uses.push({
|
|
160
|
-
address: group.address,
|
|
161
|
-
what: `VRRP ${group.id} on ${holders.map((device) => device.name).join(", ")}`,
|
|
162
|
-
});
|
|
163
|
-
segment.taken.push({ first: AddressSpace.numberOf(group.address), last: AddressSpace.numberOf(group.address) });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// A use inside a segment is an address taken; outside every segment, a block of its own. An address one of our interfaces holds is listed once, as that interface.
|
|
168
|
-
const take = (address: string, what: string, first = address, last = address) => {
|
|
169
|
-
const prefix = segmentOf(first);
|
|
170
|
-
if (!prefix) return false;
|
|
171
|
-
|
|
172
|
-
const segment = segments.get(prefix)!;
|
|
173
|
-
if (segment.uses.some((use) => use.address === address)) return true;
|
|
174
|
-
|
|
175
|
-
segment.uses.push({ address, what });
|
|
176
|
-
segment.taken.push({ first: AddressSpace.numberOf(first), last: AddressSpace.numberOf(last) });
|
|
177
|
-
return true;
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
for (const device of devices) {
|
|
181
|
-
for (const [name, server] of Object.entries(device.dhcp ?? {})) {
|
|
182
|
-
const [start, end] = server.pool;
|
|
183
|
-
if (Address.isValid(start) && Address.isValid(end))
|
|
184
|
-
take(`${start}-${end}`, `DHCP pool ${name} on ${device.name}`, start, end);
|
|
185
|
-
|
|
186
|
-
for (const reservation of server.reservations ?? []) {
|
|
187
|
-
if (Address.isValid(reservation.address))
|
|
188
|
-
take(reservation.address, `reserved for ${reservation.description ?? reservation.mac} by ${device.name}`);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
for (const route of device.routing?.static ?? []) {
|
|
193
|
-
if (route.via && Address.isValid(route.via)) take(route.via, `next hop for ${route.prefix} on ${device.name}`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
for (const [name, neighbor] of Object.entries(device.routing?.bgp?.neighbors ?? {})) {
|
|
197
|
-
if (Address.isValid(neighbor.address)) take(neighbor.address, `BGP neighbor ${name} of ${device.name}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
201
|
-
if (iface.type !== "wireguard") continue;
|
|
202
|
-
|
|
203
|
-
for (const peer of iface.peers ?? []) {
|
|
204
|
-
for (const allowed of peer.allowed_addresses) {
|
|
205
|
-
if (!Address.isCidr(allowed)) continue;
|
|
206
|
-
|
|
207
|
-
const what = `WireGuard peer ${peer.name} on ${device.name} ${name}`;
|
|
208
|
-
if (AddressSpace.isHostRoute(allowed) && take(Address.host(allowed), what)) continue;
|
|
209
|
-
if (!segmentOf(Address.host(allowed))) blocks.push({ prefix: Address.prefixOf(allowed), what });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Space the network owns: what it originates in BGP, or the prefix asked for.
|
|
216
|
-
const owners = new Map<string, string[]>();
|
|
217
|
-
for (const device of devices) {
|
|
218
|
-
for (const prefix of device.routing?.bgp?.networks ?? []) {
|
|
219
|
-
if (!Address.isCidr(prefix)) continue;
|
|
220
|
-
owners.set(Address.prefixOf(prefix), [...(owners.get(Address.prefixOf(prefix)) ?? []), device.name]);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// A static route to a prefix no segment is in hands that block to whoever is at the next hop. A route covering segments or owned space, such as an aggregate, is not a use.
|
|
225
|
-
for (const device of devices) {
|
|
226
|
-
for (const route of device.routing?.static ?? []) {
|
|
227
|
-
if (!Address.isCidr(route.prefix) || Address.split(route.prefix).len === 0) continue;
|
|
228
|
-
|
|
229
|
-
const overlapsSegment = [...segments.keys()].some(
|
|
230
|
-
(prefix) => Address.contains(route.prefix, prefix) || Address.contains(prefix, route.prefix)
|
|
231
|
-
);
|
|
232
|
-
const coversOwned = [...owners.keys()].some((owned) => Address.contains(route.prefix, owned));
|
|
233
|
-
if (overlapsSegment || coversOwned) continue;
|
|
234
|
-
|
|
235
|
-
const how = route.blackhole ? "discarded" : route.via ? `routed via ${route.via}` : `routed out ${route.interface}`;
|
|
236
|
-
blocks.push({ prefix: Address.prefixOf(route.prefix), what: `${how} by ${device.name}` });
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const asked = within ? Address.prefixOf(within) : undefined;
|
|
241
|
-
const spacePrefixes = asked ? [asked] : [...owners.keys()];
|
|
242
|
-
|
|
243
|
-
// A segment or block belongs to the smallest space it is inside.
|
|
244
|
-
const spaceOf = (prefix: string) =>
|
|
245
|
-
spacePrefixes.filter((space) => Address.contains(space, prefix)).sort((a, b) => Address.split(b).len - Address.split(a).len)[0];
|
|
246
|
-
|
|
247
|
-
const toSegment = (prefix: string): Segment => {
|
|
248
|
-
const { uses, taken } = segments.get(prefix)!;
|
|
249
|
-
const free = AddressSpace.gaps(AddressSpace.usable(prefix), taken);
|
|
250
|
-
const total = free.reduce((sum, range) => sum + range.last - range.first + 1n, 0n);
|
|
251
|
-
const family = Address.family(prefix);
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
prefix,
|
|
255
|
-
uses: uses.sort(AddressSpace.byAddress),
|
|
256
|
-
free: AddressSpace.count(total),
|
|
257
|
-
...(free[0] ? { next: Address.fromNumber(free[0].first, family) } : {}),
|
|
258
|
-
};
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
const uniqueBlocks = blocks.filter(
|
|
262
|
-
(block, index) => blocks.findIndex((other) => other.prefix === block.prefix && other.what === block.what) === index
|
|
263
|
-
);
|
|
264
|
-
|
|
265
|
-
const spaces: Space[] = spacePrefixes.sort(AddressSpace.comparePrefixes).map((space) => {
|
|
266
|
-
const inside = (prefix: string) => spaceOf(prefix) === space;
|
|
267
|
-
const spaceSegments = [...segments.keys()].filter(inside).sort(AddressSpace.comparePrefixes);
|
|
268
|
-
const spaceBlocks = uniqueBlocks.filter((block) => inside(block.prefix)).sort(AddressSpace.byPrefix);
|
|
269
|
-
|
|
270
|
-
// Smaller spaces inside this one are in use too.
|
|
271
|
-
const nested = spacePrefixes.filter((other) => other !== space && Address.contains(space, other));
|
|
272
|
-
const taken = [...spaceSegments, ...spaceBlocks.map((block) => block.prefix), ...nested].map((prefix) => Address.range(prefix));
|
|
273
|
-
const family = Address.family(space);
|
|
274
|
-
|
|
275
|
-
return {
|
|
276
|
-
prefix: space,
|
|
277
|
-
owner: asked && !owners.has(space) ? "asked for" : `originated by ${(owners.get(space) ?? []).join(", ")}`,
|
|
278
|
-
segments: spaceSegments.map(toSegment),
|
|
279
|
-
blocks: spaceBlocks,
|
|
280
|
-
free: AddressSpace.gaps(Address.range(space), taken).flatMap((range) =>
|
|
281
|
-
Address.prefixesBetween(range.first, range.last, family)
|
|
282
|
-
),
|
|
283
|
-
};
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
const outside = (prefix: string) => !spaceOf(prefix);
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
spaces,
|
|
290
|
-
elsewhere: asked
|
|
291
|
-
? { segments: [], blocks: [] }
|
|
292
|
-
: {
|
|
293
|
-
segments: [...segments.keys()].filter(outside).sort(AddressSpace.comparePrefixes).map(toSegment),
|
|
294
|
-
blocks: uniqueBlocks.filter((block) => outside(block.prefix)).sort(AddressSpace.byPrefix),
|
|
295
|
-
},
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Free prefixes of one length inside the free space, up to `limit`, and how many there are in all. */
|
|
300
|
-
static freeOfLength(free: readonly string[], length: number, limit: number): { prefixes: string[]; total: bigint } {
|
|
301
|
-
const prefixes: string[] = [];
|
|
302
|
-
let total = 0n;
|
|
303
|
-
|
|
304
|
-
for (const prefix of free) {
|
|
305
|
-
const { len, family } = Address.split(prefix);
|
|
306
|
-
if (len > length || length > Address.bits(family)) continue;
|
|
307
|
-
|
|
308
|
-
const { first } = Address.range(prefix);
|
|
309
|
-
const step = 1n << BigInt(Address.bits(family) - length);
|
|
310
|
-
const fits = 1n << BigInt(length - len);
|
|
311
|
-
total += fits;
|
|
312
|
-
|
|
313
|
-
for (let index = 0n; index < fits && prefixes.length < limit; index++) {
|
|
314
|
-
prefixes.push(`${Address.fromNumber(first + index * step, family)}/${length}`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return { prefixes, total };
|
|
319
|
-
}
|
|
320
|
-
}
|