@takodotid/circuit 0.34.2 → 0.35.0
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 +13 -14
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11347 -9018
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +17 -11
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -25
- package/src/adapters/devices/raisecom-ros/index.ts +0 -693
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
|
@@ -2,577 +2,609 @@
|
|
|
2
2
|
/// <reference lib="dom" />
|
|
3
3
|
// Studio's tools, for finding out why something does or does not reach where it should: trace an address, find the VLAN path between two devices, test a packet against a firewall, and test a route against a route policy.
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { Dom } from "./dom.js";
|
|
6
|
+
import { Evaluate } from "./evaluate.js";
|
|
7
|
+
import { Graph } from "./graph.js";
|
|
8
|
+
import { Tables } from "./tables.js";
|
|
9
9
|
|
|
10
10
|
/** @typedef {import("../data").StudioData} StudioData */
|
|
11
11
|
/** @typedef {import("../../schema").Device} Device */
|
|
12
12
|
/** @typedef {import("./dom.js").Child} Child */
|
|
13
13
|
/** @typedef {import("./evaluate.js").Check} Check */
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const TOOLS = [
|
|
47
|
-
["trace", "Trace an address"],
|
|
48
|
-
["vlan", "VLAN path"],
|
|
49
|
-
["firewall", "Test a firewall"],
|
|
50
|
-
["policy", "Test a route policy"],
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* A labelled field.
|
|
55
|
-
*
|
|
56
|
-
* @param {string} label
|
|
57
|
-
* @param {HTMLElement} control
|
|
58
|
-
* @param {string} [hint]
|
|
59
|
-
*/
|
|
60
|
-
const field = (label, control, hint) =>
|
|
61
|
-
h("label", { class: "field" }, h("span", {}, label), control, hint ? h("small", { class: "muted" }, hint) : undefined);
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* A text box that keeps what is typed in `store[key]`.
|
|
65
|
-
*
|
|
66
|
-
* @param {Record<string, unknown>} store
|
|
67
|
-
* @param {string} key
|
|
68
|
-
* @param {string} [placeholder]
|
|
69
|
-
*/
|
|
70
|
-
function textBox(store, key, placeholder = "") {
|
|
71
|
-
const input = h("input", { type: "text", value: String(store[key] ?? ""), placeholder, spellcheck: "false" });
|
|
72
|
-
input.addEventListener("input", () => (store[key] = input.value));
|
|
73
|
-
return input;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* A choice that keeps what is chosen in `store[key]`.
|
|
78
|
-
*
|
|
79
|
-
* @param {Record<string, unknown>} store
|
|
80
|
-
* @param {string} key
|
|
81
|
-
* @param {[string, string][]} options value and text
|
|
82
|
-
* @param {() => void} [changed]
|
|
83
|
-
*/
|
|
84
|
-
function choice(store, key, options, changed) {
|
|
85
|
-
const select = h(
|
|
86
|
-
"select",
|
|
87
|
-
{},
|
|
88
|
-
...options.map(([value, text]) => h("option", { value, ...(value === store[key] ? { selected: "" } : {}) }, text))
|
|
89
|
-
);
|
|
90
|
-
if (!options.some(([value]) => value === store[key])) store[key] = options[0]?.[0] ?? "";
|
|
91
|
-
select.value = String(store[key]);
|
|
92
|
-
select.addEventListener("change", () => {
|
|
93
|
-
store[key] = select.value;
|
|
94
|
-
changed?.();
|
|
95
|
-
});
|
|
96
|
-
return select;
|
|
97
|
-
}
|
|
15
|
+
export class Tools {
|
|
16
|
+
/** What Studio shows, set each time a tool is drawn. @type {StudioData} */
|
|
17
|
+
static #data;
|
|
18
|
+
|
|
19
|
+
/** What was typed into each tool, kept while the page is open, so a tool drawn again shows its last answer. */
|
|
20
|
+
static #typed = {
|
|
21
|
+
vlan: { vlan: "", from: "", to: "" },
|
|
22
|
+
firewall: {
|
|
23
|
+
device: "",
|
|
24
|
+
chain: "input",
|
|
25
|
+
protocol: "tcp",
|
|
26
|
+
src: "",
|
|
27
|
+
dst: "",
|
|
28
|
+
srcPort: "",
|
|
29
|
+
dstPort: "",
|
|
30
|
+
inInterface: "",
|
|
31
|
+
outInterface: "",
|
|
32
|
+
state: "new",
|
|
33
|
+
flags: /** @type {string[]} */ ([]),
|
|
34
|
+
},
|
|
35
|
+
policy: {
|
|
36
|
+
device: "",
|
|
37
|
+
policy: "",
|
|
38
|
+
prefix: "",
|
|
39
|
+
asPathLength: "",
|
|
40
|
+
rpki: "",
|
|
41
|
+
communities: "",
|
|
42
|
+
largeCommunities: "",
|
|
43
|
+
inRegistrySets: /** @type {string[]} */ ([]),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
98
46
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
47
|
+
static #TOOLS = [
|
|
48
|
+
["trace", "Trace an address"],
|
|
49
|
+
["vlan", "VLAN path"],
|
|
50
|
+
["firewall", "Test a firewall"],
|
|
51
|
+
["policy", "Test a route policy"],
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A labelled field.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} label
|
|
58
|
+
* @param {HTMLElement} control
|
|
59
|
+
* @param {string} [hint]
|
|
60
|
+
*/
|
|
61
|
+
static #field(label, control, hint) {
|
|
62
|
+
return Dom.h(
|
|
63
|
+
"label",
|
|
64
|
+
{ class: "field" },
|
|
65
|
+
Dom.h("span", {}, label),
|
|
66
|
+
control,
|
|
67
|
+
hint ? Dom.h("small", { class: "muted" }, hint) : undefined
|
|
68
|
+
);
|
|
69
|
+
}
|
|
123
70
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
prose(
|
|
136
|
-
"Where an address is in the network, how each device would reach it from what the files say, and every firewall rule and ACL that names it."
|
|
137
|
-
),
|
|
138
|
-
h(
|
|
139
|
-
"form",
|
|
140
|
-
{ class: "tool-form" },
|
|
141
|
-
field("Address", input),
|
|
142
|
-
button("Trace", () => store.address.trim() && go(`tools/trace/${encodeURIComponent(store.address.trim())}`))
|
|
143
|
-
)
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
if (!asked) return;
|
|
147
|
-
|
|
148
|
-
const found = trace(data, asked);
|
|
149
|
-
if (!found) {
|
|
150
|
-
container.append(empty(`${asked} is not an address, such as 10.0.10.5 or 2001:db8::1.`));
|
|
151
|
-
return;
|
|
71
|
+
/**
|
|
72
|
+
* A text box that keeps what is typed in `store[key]`.
|
|
73
|
+
*
|
|
74
|
+
* @param {Record<string, unknown>} store
|
|
75
|
+
* @param {string} key
|
|
76
|
+
* @param {string} [placeholder]
|
|
77
|
+
*/
|
|
78
|
+
static #textBox(store, key, placeholder = "") {
|
|
79
|
+
const input = Dom.h("input", { type: "text", value: String(store[key] ?? ""), placeholder, spellcheck: "false" });
|
|
80
|
+
input.addEventListener("input", () => (store[key] = input.value));
|
|
81
|
+
return input;
|
|
152
82
|
}
|
|
153
83
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
: found.space
|
|
168
|
-
? `It is in ${found.space.prefix}, ${found.space.owner}, where nothing uses it yet.`
|
|
169
|
-
: "No segment or block of the network holds it.";
|
|
170
|
-
|
|
171
|
-
container.append(heading(asked, "h2"), h("p", {}, holder, " ", where));
|
|
172
|
-
|
|
173
|
-
if (found.uses.length) {
|
|
174
|
-
container.append(
|
|
175
|
-
heading("Used by"),
|
|
176
|
-
table({
|
|
177
|
-
head: [{ label: "Address" }, { label: "Used by" }],
|
|
178
|
-
rows: found.uses.map((use) => ({ cells: [use.address, use.what] })),
|
|
179
|
-
})
|
|
84
|
+
/**
|
|
85
|
+
* A choice that keeps what is chosen in `store[key]`.
|
|
86
|
+
*
|
|
87
|
+
* @param {Record<string, unknown>} store
|
|
88
|
+
* @param {string} key
|
|
89
|
+
* @param {[string, string][]} options value and text
|
|
90
|
+
* @param {() => void} [changed]
|
|
91
|
+
*/
|
|
92
|
+
static #choice(store, key, options, changed) {
|
|
93
|
+
const select = Dom.h(
|
|
94
|
+
"select",
|
|
95
|
+
{},
|
|
96
|
+
...options.map(([value, text]) => Dom.h("option", { value, ...(value === store[key] ? { selected: "" } : {}) }, text))
|
|
180
97
|
);
|
|
98
|
+
if (!options.some(([value]) => value === store[key])) store[key] = options[0]?.[0] ?? "";
|
|
99
|
+
select.value = String(store[key]);
|
|
100
|
+
select.addEventListener("change", () => {
|
|
101
|
+
store[key] = select.value;
|
|
102
|
+
changed?.();
|
|
103
|
+
});
|
|
104
|
+
return select;
|
|
181
105
|
}
|
|
182
106
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
107
|
+
/**
|
|
108
|
+
* What a check found, in words.
|
|
109
|
+
*
|
|
110
|
+
* @param {Check} check
|
|
111
|
+
*/
|
|
112
|
+
static #verdictOf(check) {
|
|
113
|
+
return check.answer === "yes"
|
|
114
|
+
? "matches"
|
|
115
|
+
: check.answer === "no"
|
|
116
|
+
? `does not match: ${check.why.join("; ")}`
|
|
117
|
+
: `cannot tell without ${check.why.join(", ")}`;
|
|
118
|
+
}
|
|
189
119
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
route.prefix ?? "",
|
|
203
|
-
[route.via, route.through].filter(Boolean).join(", "),
|
|
204
|
-
route.note ?? "",
|
|
205
|
-
],
|
|
206
|
-
})),
|
|
207
|
-
})
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
container.append(heading("Rules that name it"));
|
|
211
|
-
container.append(
|
|
212
|
-
found.rules.length
|
|
213
|
-
? table({
|
|
214
|
-
head: ["Device", "Where", "Rule", "Description", "Action", "Names it as"].map((label) => ({ label })),
|
|
215
|
-
rows: found.rules.map((rule) => ({
|
|
216
|
-
cells: [deviceLink(rule.device), rule.where, String(rule.rule), rule.description ?? "", rule.action, rule.side],
|
|
217
|
-
})),
|
|
218
|
-
})
|
|
219
|
-
: empty("No firewall rule or ACL names it, by address, prefix or address set.")
|
|
220
|
-
);
|
|
221
|
-
}
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} text
|
|
122
|
+
* @param {() => void} action
|
|
123
|
+
*/
|
|
124
|
+
static #button(text, action) {
|
|
125
|
+
const element = Dom.h("button", { type: "submit", class: "primary" }, text);
|
|
126
|
+
element.addEventListener("click", (event) => {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
action();
|
|
129
|
+
});
|
|
130
|
+
return element;
|
|
131
|
+
}
|
|
222
132
|
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const result = h("div");
|
|
237
|
-
const find = () => {
|
|
238
|
-
result.replaceChildren();
|
|
239
|
-
|
|
240
|
-
const path = vlanPath(data, Number(store.vlan), store.from, store.to);
|
|
241
|
-
if (store.from === store.to) return result.append(empty("Pick two different devices."));
|
|
242
|
-
if (!path) return result.append(empty(`No cables carry VLAN ${store.vlan} from ${store.from} to ${store.to}.`));
|
|
243
|
-
|
|
244
|
-
const cableOf = (/** @type {import("../../core/graph").GraphEdge} */ edge) =>
|
|
245
|
-
data.graph.edges.find(
|
|
246
|
-
(other) =>
|
|
247
|
-
other.layer === "l1" &&
|
|
248
|
-
((other.from === edge.from && other.to === edge.to) || (other.from === edge.to && other.to === edge.from))
|
|
249
|
-
)?.label ?? "";
|
|
250
|
-
const nameOf = (/** @type {string} */ id) => id.replace(/^device:/, "");
|
|
251
|
-
|
|
252
|
-
result.append(
|
|
253
|
-
h(
|
|
254
|
-
"p",
|
|
255
|
-
{},
|
|
256
|
-
`${path.length} ${path.length === 1 ? "cable" : "cables"} carry it from ${store.from} to ${store.to}. `,
|
|
257
|
-
link("Show in the graph", () => {
|
|
258
|
-
lightPath(path);
|
|
259
|
-
go("graph/l2");
|
|
260
|
-
})
|
|
133
|
+
// Trace an address.
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string | undefined} asked
|
|
137
|
+
* @param {HTMLElement} container
|
|
138
|
+
*/
|
|
139
|
+
static #trace(asked, container) {
|
|
140
|
+
const store = { address: asked ?? "" };
|
|
141
|
+
const input = Tools.#textBox(store, "address", "10.0.10.5");
|
|
142
|
+
|
|
143
|
+
container.append(
|
|
144
|
+
Tables.prose(
|
|
145
|
+
"Where an address is in the network, how each device would reach it from what the files say, and every firewall rule and ACL that names it."
|
|
261
146
|
),
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}))
|
|
267
|
-
|
|
147
|
+
Dom.h(
|
|
148
|
+
"form",
|
|
149
|
+
{ class: "tool-form" },
|
|
150
|
+
Tools.#field("Address", input),
|
|
151
|
+
Tools.#button("Trace", () => store.address.trim() && Dom.go(`tools/trace/${encodeURIComponent(store.address.trim())}`))
|
|
152
|
+
)
|
|
268
153
|
);
|
|
269
|
-
};
|
|
270
154
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
container.append(
|
|
274
|
-
prose("The fewest cables that carry a VLAN from one device to another, from the ports' VLANs and links."),
|
|
275
|
-
h(
|
|
276
|
-
"form",
|
|
277
|
-
{ class: "tool-form" },
|
|
278
|
-
field("VLAN", choice(store, "vlan", vlanChoices)),
|
|
279
|
-
field("From", choice(store, "from", devices)),
|
|
280
|
-
field("To", choice(store, "to", devices)),
|
|
281
|
-
button("Find the path", find)
|
|
282
|
-
),
|
|
283
|
-
result
|
|
284
|
-
);
|
|
285
|
-
}
|
|
155
|
+
if (!asked) return;
|
|
286
156
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const devices = data.devices.filter((device) => Object.values(device.firewall?.filter ?? {}).some(Boolean));
|
|
293
|
-
if (!devices.length) return container.append(empty("No device has a firewall filter."));
|
|
294
|
-
|
|
295
|
-
const result = h("div");
|
|
296
|
-
const form = h("form", { class: "tool-form" });
|
|
297
|
-
|
|
298
|
-
const draw = () => {
|
|
299
|
-
form.replaceChildren();
|
|
300
|
-
const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
|
|
301
|
-
const chains = /** @type {[string, string][]} */ (Object.keys(device.firewall?.filter ?? {}).map((chain) => [chain, chain]));
|
|
302
|
-
const names = /** @type {[string, string][]} */ ([
|
|
303
|
-
["", "not given"],
|
|
304
|
-
...Object.keys({ ...device.ports, ...device.interfaces }).map((name) => [name, name]),
|
|
305
|
-
]);
|
|
306
|
-
const protocols = /** @type {[string, string][]} */ (
|
|
307
|
-
["tcp", "udp", "icmp", "gre", "esp", "ah", "ospf", "vrrp"].map((name) => [name, name])
|
|
308
|
-
);
|
|
309
|
-
const states = /** @type {[string, string][]} */ (
|
|
310
|
-
["new", "established", "related", "untracked", "invalid"].map((name) => [name, name])
|
|
311
|
-
);
|
|
157
|
+
const found = Evaluate.trace(Tools.#data, asked);
|
|
158
|
+
if (!found) {
|
|
159
|
+
container.append(Tables.empty(`${asked} is not an address, such as 10.0.10.5 or 2001:db8::1.`));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
312
162
|
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
163
|
+
const holder = found.held.length
|
|
164
|
+
? Dom.h(
|
|
165
|
+
"span",
|
|
166
|
+
{},
|
|
167
|
+
"It is held by ",
|
|
168
|
+
...found.held.flatMap((held, index) => [index ? ", " : "", Tables.deviceLink(held.device), ` ${held.where}`]),
|
|
169
|
+
"."
|
|
170
|
+
)
|
|
171
|
+
: "No device holds it.";
|
|
172
|
+
const where = found.segment
|
|
173
|
+
? `It is in ${found.segment.prefix}${found.space ? `, inside ${found.space.prefix}, ${found.space.owner}` : ""}.`
|
|
174
|
+
: found.block
|
|
175
|
+
? `It is in ${found.block.prefix}: ${found.block.what}.`
|
|
176
|
+
: found.space
|
|
177
|
+
? `It is in ${found.space.prefix}, ${found.space.owner}, where nothing uses it yet.`
|
|
178
|
+
: "No segment or block of the network holds it.";
|
|
179
|
+
|
|
180
|
+
container.append(Tables.heading(asked, "h2"), Dom.h("p", {}, holder, " ", where));
|
|
181
|
+
|
|
182
|
+
if (found.uses.length) {
|
|
183
|
+
container.append(
|
|
184
|
+
Tables.heading("Used by"),
|
|
185
|
+
Tables.of({
|
|
186
|
+
head: [{ label: "Address" }, { label: "Used by" }],
|
|
187
|
+
rows: found.uses.map((use) => ({ cells: [use.address, use.what] })),
|
|
188
|
+
})
|
|
319
189
|
);
|
|
320
|
-
flags.append(h("label", { class: "flag" }, box, flag));
|
|
321
190
|
}
|
|
322
191
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
192
|
+
const kinds = {
|
|
193
|
+
local: "it is its own address",
|
|
194
|
+
connected: "an address of its own is in the same prefix",
|
|
195
|
+
static: "a static route",
|
|
196
|
+
none: "no route in the files",
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
container.append(
|
|
200
|
+
Tables.heading("How each device reaches it"),
|
|
201
|
+
Tables.prose(
|
|
202
|
+
"Routes a device learns from BGP or OSPF are not in the files, so a device with no route here may still reach it."
|
|
332
203
|
),
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
204
|
+
Tables.of({
|
|
205
|
+
csv: `trace-${asked}`,
|
|
206
|
+
head: ["Device", "Table", "By", "Prefix", "Next hop or interface", "Note"].map((label) => ({ label })),
|
|
207
|
+
rows: found.routes.map((route) => ({
|
|
208
|
+
muted: route.kind === "none",
|
|
209
|
+
cells: [
|
|
210
|
+
Tables.deviceLink(route.device),
|
|
211
|
+
route.table,
|
|
212
|
+
kinds[route.kind],
|
|
213
|
+
route.prefix ?? "",
|
|
214
|
+
[route.via, route.through].filter(Boolean).join(", "),
|
|
215
|
+
route.note ?? "",
|
|
216
|
+
],
|
|
217
|
+
})),
|
|
218
|
+
})
|
|
344
219
|
);
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
const test = () => {
|
|
348
|
-
result.replaceChildren();
|
|
349
|
-
const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
|
|
350
|
-
const port = (/** @type {string} */ text) => (text.trim() === "" ? undefined : Number(text));
|
|
351
|
-
|
|
352
|
-
const outcome = filterPacket(device, /** @type {"input" | "forward" | "output"} */ (store.chain), {
|
|
353
|
-
protocol: store.protocol,
|
|
354
|
-
src: store.src.trim() || undefined,
|
|
355
|
-
dst: store.dst.trim() || undefined,
|
|
356
|
-
srcPort: port(store.srcPort),
|
|
357
|
-
dstPort: port(store.dstPort),
|
|
358
|
-
inInterface: store.inInterface || undefined,
|
|
359
|
-
outInterface: store.outInterface || undefined,
|
|
360
|
-
state: store.state,
|
|
361
|
-
flags: store.flags.length ? store.flags : undefined,
|
|
362
|
-
});
|
|
363
220
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
})
|
|
221
|
+
container.append(Tables.heading("Rules that name it"));
|
|
222
|
+
container.append(
|
|
223
|
+
found.rules.length
|
|
224
|
+
? Tables.of({
|
|
225
|
+
head: ["Device", "Where", "Rule", "Description", "Action", "Names it as"].map((label) => ({ label })),
|
|
226
|
+
rows: found.rules.map((rule) => ({
|
|
227
|
+
cells: [
|
|
228
|
+
Tables.deviceLink(rule.device),
|
|
229
|
+
rule.where,
|
|
230
|
+
String(rule.rule),
|
|
231
|
+
rule.description ?? "",
|
|
232
|
+
rule.action,
|
|
233
|
+
rule.side,
|
|
234
|
+
],
|
|
235
|
+
})),
|
|
236
|
+
})
|
|
237
|
+
: Tables.empty("No firewall rule or ACL names it, by address, prefix or address set.")
|
|
382
238
|
);
|
|
383
|
-
}
|
|
239
|
+
}
|
|
384
240
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
241
|
+
// The VLAN path between two devices.
|
|
242
|
+
|
|
243
|
+
/** @param {HTMLElement} container */
|
|
244
|
+
static #vlan(container) {
|
|
245
|
+
const store = Tools.#typed.vlan;
|
|
246
|
+
const vlans = new Map();
|
|
247
|
+
for (const edge of Tools.#data.graph.edges) for (const vlan of edge.vlans ?? []) vlans.set(vlan.id, vlan.name);
|
|
248
|
+
|
|
249
|
+
const devices = Tools.#data.devices.map((device) => /** @type {[string, string]} */ ([device.name, device.name]));
|
|
250
|
+
const vlanChoices = [...vlans]
|
|
251
|
+
.sort(([a], [b]) => a - b)
|
|
252
|
+
.map(([id, name]) => /** @type {[string, string]} */ ([String(id), `${name} (${id})`]));
|
|
253
|
+
|
|
254
|
+
const result = Dom.h("div");
|
|
255
|
+
const find = () => {
|
|
256
|
+
result.replaceChildren();
|
|
257
|
+
|
|
258
|
+
const path = Evaluate.vlanPath(Tools.#data, Number(store.vlan), store.from, store.to);
|
|
259
|
+
if (store.from === store.to) return result.append(Tables.empty("Pick two different devices."));
|
|
260
|
+
if (!path) return result.append(Tables.empty(`No cables carry VLAN ${store.vlan} from ${store.from} to ${store.to}.`));
|
|
261
|
+
|
|
262
|
+
const cableOf = (/** @type {import("../../core/topology").GraphEdge} */ edge) =>
|
|
263
|
+
Tools.#data.graph.edges.find(
|
|
264
|
+
(other) =>
|
|
265
|
+
other.layer === "l1" &&
|
|
266
|
+
((other.from === edge.from && other.to === edge.to) || (other.from === edge.to && other.to === edge.from))
|
|
267
|
+
)?.label ?? "";
|
|
268
|
+
const nameOf = (/** @type {string} */ id) => id.replace(/^device:/, "");
|
|
269
|
+
|
|
270
|
+
result.append(
|
|
271
|
+
Dom.h(
|
|
272
|
+
"p",
|
|
273
|
+
{},
|
|
274
|
+
`${path.length} ${path.length === 1 ? "cable" : "cables"} carry it from ${store.from} to ${store.to}. `,
|
|
275
|
+
Dom.link("Show in the graph", () => {
|
|
276
|
+
Graph.lightPath(path);
|
|
277
|
+
Dom.go("graph/l2");
|
|
278
|
+
})
|
|
279
|
+
),
|
|
280
|
+
Tables.of({
|
|
281
|
+
head: ["#", "From", "To", "Cable"].map((label) => ({ label })),
|
|
282
|
+
rows: path.map((edge, index) => ({
|
|
283
|
+
cells: [String(index + 1), Tables.deviceLink(nameOf(edge.from)), Tables.deviceLink(nameOf(edge.to)), cableOf(edge)],
|
|
284
|
+
})),
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
};
|
|
392
288
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
/** @param {HTMLElement} container */
|
|
396
|
-
function policyTool(container) {
|
|
397
|
-
const store = typed.policy;
|
|
398
|
-
const devices = data.devices.filter((device) => Object.keys(device.policies ?? {}).length);
|
|
399
|
-
if (!devices.length) return container.append(empty("No device has a route policy."));
|
|
400
|
-
|
|
401
|
-
const result = h("div");
|
|
402
|
-
const form = h("form", { class: "tool-form" });
|
|
403
|
-
|
|
404
|
-
const draw = () => {
|
|
405
|
-
form.replaceChildren();
|
|
406
|
-
const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
|
|
407
|
-
const neighbors = Object.entries(device.routing?.bgp?.neighbors ?? {});
|
|
408
|
-
const groups = device.routing?.bgp?.groups ?? {};
|
|
409
|
-
|
|
410
|
-
// A policy is listed with the sessions that use it, so it can be picked by neighbor.
|
|
411
|
-
const usedBy = (/** @type {string} */ policy) =>
|
|
412
|
-
neighbors.flatMap(([name, neighbor]) => {
|
|
413
|
-
const group = neighbor.group ? groups[neighbor.group] : undefined;
|
|
414
|
-
return [
|
|
415
|
-
(neighbor.import ?? group?.import) === policy ? `import from ${name}` : "",
|
|
416
|
-
(neighbor.export ?? group?.export) === policy ? `export to ${name}` : "",
|
|
417
|
-
].filter(Boolean);
|
|
418
|
-
});
|
|
289
|
+
if (!vlanChoices.length) return container.append(Tables.empty("No cable carries a VLAN."));
|
|
419
290
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
291
|
+
container.append(
|
|
292
|
+
Tables.prose("The fewest cables that carry a VLAN from one device to another, from the ports' VLANs and links."),
|
|
293
|
+
Dom.h(
|
|
294
|
+
"form",
|
|
295
|
+
{ class: "tool-form" },
|
|
296
|
+
Tools.#field("VLAN", Tools.#choice(store, "vlan", vlanChoices)),
|
|
297
|
+
Tools.#field("From", Tools.#choice(store, "from", devices)),
|
|
298
|
+
Tools.#field("To", Tools.#choice(store, "to", devices)),
|
|
299
|
+
Tools.#button("Find the path", find)
|
|
300
|
+
),
|
|
301
|
+
result
|
|
425
302
|
);
|
|
303
|
+
}
|
|
426
304
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
305
|
+
// Test a firewall.
|
|
306
|
+
|
|
307
|
+
/** @param {HTMLElement} container */
|
|
308
|
+
static #firewall(container) {
|
|
309
|
+
const store = Tools.#typed.firewall;
|
|
310
|
+
const devices = Tools.#data.devices.filter((device) => Object.values(device.firewall?.filter ?? {}).some(Boolean));
|
|
311
|
+
if (!devices.length) return container.append(Tables.empty("No device has a firewall filter."));
|
|
312
|
+
|
|
313
|
+
const result = Dom.h("div");
|
|
314
|
+
const form = Dom.h("form", { class: "tool-form" });
|
|
315
|
+
|
|
316
|
+
const draw = () => {
|
|
317
|
+
form.replaceChildren();
|
|
318
|
+
const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
|
|
319
|
+
const chains = /** @type {[string, string][]} */ (Object.keys(device.firewall?.filter ?? {}).map((chain) => [chain, chain]));
|
|
320
|
+
const names = /** @type {[string, string][]} */ ([
|
|
321
|
+
["", "not given"],
|
|
322
|
+
...Object.keys({ ...device.ports, ...device.interfaces }).map((name) => [name, name]),
|
|
323
|
+
]);
|
|
324
|
+
const protocols = /** @type {[string, string][]} */ (
|
|
325
|
+
["tcp", "udp", "icmp", "gre", "esp", "ah", "ospf", "vrrp"].map((name) => [name, name])
|
|
326
|
+
);
|
|
327
|
+
const states = /** @type {[string, string][]} */ (
|
|
328
|
+
["new", "established", "related", "untracked", "invalid"].map((name) => [name, name])
|
|
437
329
|
);
|
|
438
|
-
sets.append(h("label", { class: "flag" }, box, name));
|
|
439
|
-
}
|
|
440
330
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
"
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
331
|
+
const flags = Dom.h("span", { class: "flags" });
|
|
332
|
+
for (const flag of ["syn", "ack", "fin", "rst", "psh", "urg"]) {
|
|
333
|
+
const box = Dom.h("input", { type: "checkbox", ...(store.flags.includes(flag) ? { checked: "" } : {}) });
|
|
334
|
+
box.addEventListener(
|
|
335
|
+
"change",
|
|
336
|
+
() => (store.flags = box.checked ? [...store.flags, flag] : store.flags.filter((other) => other !== flag))
|
|
337
|
+
);
|
|
338
|
+
flags.append(Dom.h("label", { class: "flag" }, box, flag));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
form.append(
|
|
342
|
+
Tools.#field(
|
|
343
|
+
"Device",
|
|
344
|
+
Tools.#choice(
|
|
345
|
+
store,
|
|
346
|
+
"device",
|
|
347
|
+
devices.map((candidate) => [candidate.name, candidate.name]),
|
|
348
|
+
draw
|
|
349
|
+
)
|
|
350
|
+
),
|
|
351
|
+
Tools.#field("Chain", Tools.#choice(store, "chain", chains)),
|
|
352
|
+
Tools.#field("Protocol", Tools.#choice(store, "protocol", protocols)),
|
|
353
|
+
Tools.#field("Source", Tools.#textBox(store, "src", "203.0.113.9")),
|
|
354
|
+
Tools.#field("Destination", Tools.#textBox(store, "dst", "192.0.2.1")),
|
|
355
|
+
Tools.#field("Source port", Tools.#textBox(store, "srcPort")),
|
|
356
|
+
Tools.#field("Destination port", Tools.#textBox(store, "dstPort", "22")),
|
|
357
|
+
Tools.#field("Arrives on", Tools.#choice(store, "inInterface", names)),
|
|
358
|
+
Tools.#field("Leaves through", Tools.#choice(store, "outInterface", names)),
|
|
359
|
+
Tools.#field("State", Tools.#choice(store, "state", states)),
|
|
360
|
+
Tools.#field("TCP flags", flags, "Leave all unticked when not known."),
|
|
361
|
+
Tools.#button("Test", test)
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const test = () => {
|
|
366
|
+
result.replaceChildren();
|
|
367
|
+
const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
|
|
368
|
+
const port = (/** @type {string} */ text) => (text.trim() === "" ? undefined : Number(text));
|
|
369
|
+
|
|
370
|
+
const outcome = Evaluate.filterPacket(device, /** @type {"input" | "forward" | "output"} */ (store.chain), {
|
|
371
|
+
protocol: store.protocol,
|
|
372
|
+
src: store.src.trim() || undefined,
|
|
373
|
+
dst: store.dst.trim() || undefined,
|
|
374
|
+
srcPort: port(store.srcPort),
|
|
375
|
+
dstPort: port(store.dstPort),
|
|
376
|
+
inInterface: store.inInterface || undefined,
|
|
377
|
+
outInterface: store.outInterface || undefined,
|
|
378
|
+
state: store.state,
|
|
379
|
+
flags: store.flags.length ? store.flags : undefined,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const decided = outcome.verdict
|
|
383
|
+
? Dom.h("p", { class: `verdict ${outcome.verdict}` }, `${outcome.verdict}, by ${outcome.by}.`)
|
|
384
|
+
: Dom.h("p", { class: "verdict unknown" }, `Cannot tell: ${outcome.by} needs more about the packet.`);
|
|
385
|
+
const reached = outcome.steps.length;
|
|
386
|
+
const rules = device.firewall?.filter?.[/** @type {"input" | "forward" | "output"} */ (store.chain)]?.rules ?? [];
|
|
387
|
+
|
|
388
|
+
result.append(
|
|
389
|
+
decided,
|
|
390
|
+
Tables.of({
|
|
391
|
+
head: ["Rule", "Description", "Action", "This packet"].map((label) => ({ label })),
|
|
392
|
+
rows: rules.map((rule, index) => {
|
|
393
|
+
const step = outcome.steps[index];
|
|
394
|
+
return {
|
|
395
|
+
muted: index >= reached,
|
|
396
|
+
cells: [
|
|
397
|
+
String(index + 1),
|
|
398
|
+
rule.description ?? "",
|
|
399
|
+
rule.action,
|
|
400
|
+
step ? Tools.#verdictOf(step.check) : "not reached",
|
|
401
|
+
],
|
|
402
|
+
};
|
|
403
|
+
}),
|
|
404
|
+
})
|
|
405
|
+
);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
draw();
|
|
409
|
+
container.append(
|
|
410
|
+
Tables.prose(
|
|
411
|
+
"Which rule of a device's filter chain a packet meets, first match first, as the schema describes each condition."
|
|
462
412
|
),
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
...(registrySets.length
|
|
466
|
-
? [field("In prefix sets from a registry", sets, "Their prefixes are fetched by circuit refresh and are not in the files.")]
|
|
467
|
-
: []),
|
|
468
|
-
button("Test", test)
|
|
413
|
+
form,
|
|
414
|
+
result
|
|
469
415
|
);
|
|
470
|
-
}
|
|
416
|
+
}
|
|
471
417
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
418
|
+
// Test a route policy.
|
|
419
|
+
|
|
420
|
+
/** @param {HTMLElement} container */
|
|
421
|
+
static #policy(container) {
|
|
422
|
+
const store = Tools.#typed.policy;
|
|
423
|
+
const devices = Tools.#data.devices.filter((device) => Object.keys(device.policies ?? {}).length);
|
|
424
|
+
if (!devices.length) return container.append(Tables.empty("No device has a route policy."));
|
|
425
|
+
|
|
426
|
+
const result = Dom.h("div");
|
|
427
|
+
const form = Dom.h("form", { class: "tool-form" });
|
|
428
|
+
|
|
429
|
+
const draw = () => {
|
|
430
|
+
form.replaceChildren();
|
|
431
|
+
const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
|
|
432
|
+
const neighbors = Object.entries(device.routing?.bgp?.neighbors ?? {});
|
|
433
|
+
const groups = device.routing?.bgp?.groups ?? {};
|
|
434
|
+
|
|
435
|
+
// A policy is listed with the sessions that use it, so it can be picked by neighbor.
|
|
436
|
+
const usedBy = (/** @type {string} */ policy) =>
|
|
437
|
+
neighbors.flatMap(([name, neighbor]) => {
|
|
438
|
+
const group = neighbor.group ? groups[neighbor.group] : undefined;
|
|
439
|
+
return [
|
|
440
|
+
(neighbor.import ?? group?.import) === policy ? `import from ${name}` : "",
|
|
441
|
+
(neighbor.export ?? group?.export) === policy ? `export to ${name}` : "",
|
|
442
|
+
].filter(Boolean);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const policies = /** @type {[string, string][]} */ (
|
|
446
|
+
Object.keys(device.policies ?? {}).map((name) => {
|
|
447
|
+
const uses = usedBy(name);
|
|
448
|
+
return [name, uses.length ? `${name}: ${uses.join(", ")}` : name];
|
|
449
|
+
})
|
|
450
|
+
);
|
|
476
451
|
|
|
477
|
-
|
|
452
|
+
const registrySets = Object.entries(device.prefix_sets ?? {}).filter(([, set]) => "source" in set);
|
|
453
|
+
const sets = Dom.h("span", { class: "flags" });
|
|
454
|
+
for (const [name] of registrySets) {
|
|
455
|
+
const box = Dom.h("input", { type: "checkbox", ...(store.inRegistrySets.includes(name) ? { checked: "" } : {}) });
|
|
456
|
+
box.addEventListener(
|
|
457
|
+
"change",
|
|
458
|
+
() =>
|
|
459
|
+
(store.inRegistrySets = box.checked
|
|
460
|
+
? [...store.inRegistrySets, name]
|
|
461
|
+
: store.inRegistrySets.filter((other) => other !== name))
|
|
462
|
+
);
|
|
463
|
+
sets.append(Dom.h("label", { class: "flag" }, box, name));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
form.append(
|
|
467
|
+
Tools.#field(
|
|
468
|
+
"Device",
|
|
469
|
+
Tools.#choice(
|
|
470
|
+
store,
|
|
471
|
+
"device",
|
|
472
|
+
devices.map((candidate) => [candidate.name, candidate.name]),
|
|
473
|
+
draw
|
|
474
|
+
)
|
|
475
|
+
),
|
|
476
|
+
Tools.#field("Policy", Tools.#choice(store, "policy", policies)),
|
|
477
|
+
Tools.#field("Prefix", Tools.#textBox(store, "prefix", "198.51.100.0/24")),
|
|
478
|
+
Tools.#field("AS path length", Tools.#textBox(store, "asPathLength"), "As the route arrives."),
|
|
479
|
+
Tools.#field(
|
|
480
|
+
"RPKI",
|
|
481
|
+
Tools.#choice(store, "rpki", [
|
|
482
|
+
["", "not known"],
|
|
483
|
+
["valid", "valid"],
|
|
484
|
+
["invalid", "invalid"],
|
|
485
|
+
["not-found", "not found"],
|
|
486
|
+
])
|
|
487
|
+
),
|
|
488
|
+
Tools.#field("Communities", Tools.#textBox(store, "communities", "64500:100"), "Separated by spaces or commas."),
|
|
489
|
+
Tools.#field("Large communities", Tools.#textBox(store, "largeCommunities", "64500:1:1")),
|
|
490
|
+
...(registrySets.length
|
|
491
|
+
? [
|
|
492
|
+
Tools.#field(
|
|
493
|
+
"In prefix sets from a registry",
|
|
494
|
+
sets,
|
|
495
|
+
"Their prefixes are fetched by circuit refresh and are not in the files."
|
|
496
|
+
),
|
|
497
|
+
]
|
|
498
|
+
: []),
|
|
499
|
+
Tools.#button("Test", test)
|
|
500
|
+
);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const test = () => {
|
|
504
|
+
result.replaceChildren();
|
|
505
|
+
const device = devices.find((candidate) => candidate.name === store.device) ?? /** @type {Device} */ (devices[0]);
|
|
506
|
+
const list = (/** @type {string} */ text) => text.split(/[\s,]+/).filter(Boolean);
|
|
507
|
+
|
|
508
|
+
if (!store.prefix.trim()) return result.append(Tables.empty("Give the route's prefix, such as 198.51.100.0/24."));
|
|
509
|
+
|
|
510
|
+
const outcome = Evaluate.routeThrough(device, store.policy, {
|
|
511
|
+
prefix: store.prefix.trim(),
|
|
512
|
+
asPathLength: store.asPathLength.trim() === "" ? undefined : Number(store.asPathLength),
|
|
513
|
+
rpki: store.rpki || undefined,
|
|
514
|
+
communities: list(store.communities),
|
|
515
|
+
largeCommunities: list(store.largeCommunities),
|
|
516
|
+
inRegistrySets: store.inRegistrySets,
|
|
517
|
+
});
|
|
478
518
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
519
|
+
const decided =
|
|
520
|
+
outcome.decision === "unknown"
|
|
521
|
+
? Dom.h(
|
|
522
|
+
"p",
|
|
523
|
+
{ class: "verdict unknown" },
|
|
524
|
+
`Cannot tell: rule ${outcome.steps.at(-1)?.rule} of ${outcome.steps.at(-1)?.policy} needs more about the route.`
|
|
525
|
+
)
|
|
526
|
+
: Dom.h(
|
|
527
|
+
"p",
|
|
528
|
+
{ class: `verdict ${outcome.decision}` },
|
|
529
|
+
`${outcome.decision}ed${outcome.byDefault ? ", since no rule accepted it" : ""}.`
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const { attributes } = outcome;
|
|
533
|
+
const final = [
|
|
534
|
+
attributes.localPref !== undefined ? `local preference ${attributes.localPref}` : "",
|
|
535
|
+
attributes.med !== undefined ? `MED ${attributes.med}` : "",
|
|
536
|
+
attributes.prepend ? `our AS prepended ${attributes.prepend} ${attributes.prepend === 1 ? "time" : "times"}` : "",
|
|
537
|
+
`communities ${attributes.communities.join(", ") || "none"}`,
|
|
538
|
+
`large communities ${attributes.largeCommunities.join(", ") || "none"}`,
|
|
539
|
+
attributes.nextHop ? `next hop ${attributes.nextHop}` : "",
|
|
540
|
+
attributes.preferredSource ? `preferred source ${attributes.preferredSource}` : "",
|
|
541
|
+
attributes.blackhole ? "blackholed" : "",
|
|
542
|
+
].filter(Boolean);
|
|
543
|
+
|
|
544
|
+
result.append(
|
|
545
|
+
decided,
|
|
546
|
+
Tables.of({
|
|
547
|
+
head: ["Policy", "Rule", "Description", "This route", "Changes", "Decision"].map((label) => ({ label })),
|
|
548
|
+
rows: outcome.steps.map((step) => ({
|
|
549
|
+
cells: [
|
|
550
|
+
step.policy,
|
|
551
|
+
String(step.rule),
|
|
552
|
+
step.description ?? (step.call ? `calls ${step.call}` : ""),
|
|
553
|
+
Tools.#verdictOf(step.check),
|
|
554
|
+
step.changed.join("; "),
|
|
555
|
+
step.decision ?? "",
|
|
556
|
+
],
|
|
557
|
+
})),
|
|
558
|
+
}),
|
|
559
|
+
...(outcome.decision === "accept" ? [Dom.h("p", {}, "The route leaves with ", final.join(", "), ".")] : [])
|
|
560
|
+
);
|
|
561
|
+
};
|
|
487
562
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
: h(
|
|
496
|
-
"p",
|
|
497
|
-
{ class: `verdict ${outcome.decision}` },
|
|
498
|
-
`${outcome.decision}ed${outcome.byDefault ? ", since no rule accepted it" : ""}.`
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
const { attributes } = outcome;
|
|
502
|
-
const final = [
|
|
503
|
-
attributes.localPref !== undefined ? `local preference ${attributes.localPref}` : "",
|
|
504
|
-
attributes.med !== undefined ? `MED ${attributes.med}` : "",
|
|
505
|
-
attributes.prepend ? `our AS prepended ${attributes.prepend} ${attributes.prepend === 1 ? "time" : "times"}` : "",
|
|
506
|
-
`communities ${attributes.communities.join(", ") || "none"}`,
|
|
507
|
-
`large communities ${attributes.largeCommunities.join(", ") || "none"}`,
|
|
508
|
-
attributes.nextHop ? `next hop ${attributes.nextHop}` : "",
|
|
509
|
-
attributes.preferredSource ? `preferred source ${attributes.preferredSource}` : "",
|
|
510
|
-
attributes.blackhole ? "blackholed" : "",
|
|
511
|
-
].filter(Boolean);
|
|
512
|
-
|
|
513
|
-
result.append(
|
|
514
|
-
decided,
|
|
515
|
-
table({
|
|
516
|
-
head: ["Policy", "Rule", "Description", "This route", "Changes", "Decision"].map((label) => ({ label })),
|
|
517
|
-
rows: outcome.steps.map((step) => ({
|
|
518
|
-
cells: [
|
|
519
|
-
step.policy,
|
|
520
|
-
String(step.rule),
|
|
521
|
-
step.description ?? (step.call ? `calls ${step.call}` : ""),
|
|
522
|
-
verdictOf(step.check),
|
|
523
|
-
step.changed.join("; "),
|
|
524
|
-
step.decision ?? "",
|
|
525
|
-
],
|
|
526
|
-
})),
|
|
527
|
-
}),
|
|
528
|
-
...(outcome.decision === "accept" ? [h("p", {}, "The route leaves with ", final.join(", "), ".")] : [])
|
|
563
|
+
draw();
|
|
564
|
+
container.append(
|
|
565
|
+
Tables.prose(
|
|
566
|
+
"What a device's route policy does to a route: rule by rule, what matches, what changes, and the decision. A prefix set holds a route whose prefix is one of its own or inside one."
|
|
567
|
+
),
|
|
568
|
+
form,
|
|
569
|
+
result
|
|
529
570
|
);
|
|
530
|
-
}
|
|
571
|
+
}
|
|
531
572
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
)
|
|
540
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Draw a tool, such as `["tools", "trace", "10.0.10.5"]`, into a container.
|
|
575
|
+
*
|
|
576
|
+
* @param {string[]} route
|
|
577
|
+
* @param {HTMLElement} container
|
|
578
|
+
* @param {StudioData} next
|
|
579
|
+
*/
|
|
580
|
+
static show(route, container, next) {
|
|
581
|
+
Tools.#data = next;
|
|
582
|
+
container.replaceChildren();
|
|
583
|
+
|
|
584
|
+
const [, asked = "trace", argument] = route;
|
|
585
|
+
const tool = Tools.#TOOLS.find(([id]) => id === asked) ?? Tools.#TOOLS[0];
|
|
586
|
+
if (!tool) return;
|
|
587
|
+
|
|
588
|
+
const tabs = Dom.h("nav", { class: "tabs", "aria-label": "Tools" });
|
|
589
|
+
for (const [id, title] of Tools.#TOOLS) {
|
|
590
|
+
const tab = Dom.h("button", { "aria-pressed": String(id === tool[0]) }, title ?? id ?? "");
|
|
591
|
+
tab.addEventListener("click", () => Dom.go(`tools/${id}`));
|
|
592
|
+
tabs.append(tab);
|
|
593
|
+
}
|
|
541
594
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
container.replaceChildren();
|
|
552
|
-
|
|
553
|
-
const [, asked = "trace", argument] = route;
|
|
554
|
-
const tool = TOOLS.find(([id]) => id === asked) ?? TOOLS[0];
|
|
555
|
-
if (!tool) return;
|
|
556
|
-
|
|
557
|
-
const tabs = h("nav", { class: "tabs", "aria-label": "Tools" });
|
|
558
|
-
for (const [id, title] of TOOLS) {
|
|
559
|
-
const tab = h("button", { "aria-pressed": String(id === tool[0]) }, title ?? id ?? "");
|
|
560
|
-
tab.addEventListener("click", () => go(`tools/${id}`));
|
|
561
|
-
tabs.append(tab);
|
|
562
|
-
}
|
|
595
|
+
const body = Dom.h("div", { class: "section" });
|
|
596
|
+
container.append(
|
|
597
|
+
Tables.heading("Tools", "h2"),
|
|
598
|
+
Tables.prose(
|
|
599
|
+
"They read the files as the schema describes each field. A device can still behave otherwise; these say what the config means."
|
|
600
|
+
),
|
|
601
|
+
tabs,
|
|
602
|
+
body
|
|
603
|
+
);
|
|
563
604
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
),
|
|
570
|
-
tabs,
|
|
571
|
-
body
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
if (tool[0] === "trace") traceTool(argument, body);
|
|
575
|
-
else if (tool[0] === "vlan") vlanTool(body);
|
|
576
|
-
else if (tool[0] === "firewall") firewallTool(body);
|
|
577
|
-
else policyTool(body);
|
|
605
|
+
if (tool[0] === "trace") Tools.#trace(argument, body);
|
|
606
|
+
else if (tool[0] === "vlan") Tools.#vlan(body);
|
|
607
|
+
else if (tool[0] === "firewall") Tools.#firewall(body);
|
|
608
|
+
else Tools.#policy(body);
|
|
609
|
+
}
|
|
578
610
|
}
|