@takodotid/circuit 0.39.0 → 0.40.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/dist/cli/index.js +299 -27
- package/dist/types/core/validator.d.ts +2 -0
- package/dist/types/helpers/upstreams.d.ts +2 -2
- package/dist/types/platforms/derive.d.ts +1 -1
- package/dist/types/platforms/fortios/firewall.d.ts +2 -0
- package/dist/types/platforms/fortios/index.d.ts +7 -1
- package/dist/types/platforms/fortios/render.d.ts +2 -0
- package/dist/types/platforms/fortios/vdoms.d.ts +50 -0
- package/dist/types/platforms/routeros/render.d.ts +7 -1
- package/dist/types/schema/device.d.ts +21 -1
- package/dist/types/schema/firewall.d.ts +1 -1
- package/package.json +1 -1
- package/src/core/validator.ts +25 -0
- package/src/helpers/upstreams.ts +4 -1
- package/src/platforms/derive.ts +3 -1
- package/src/platforms/fortios/firewall.ts +9 -0
- package/src/platforms/fortios/index.ts +90 -28
- package/src/platforms/fortios/render.ts +27 -5
- package/src/platforms/fortios/vdoms.ts +217 -0
- package/src/platforms/routeros/TRAPS.md +1 -0
- package/src/platforms/routeros/render.ts +36 -3
- package/src/schema/device.ts +22 -1
- package/src/schema/firewall.ts +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -542,6 +542,8 @@ class Derive {
|
|
|
542
542
|
const has = (feature) => proved.includes(feature);
|
|
543
543
|
if (routing?.bfd && !has("bfd-timers"))
|
|
544
544
|
found.push(`routing.bfd, BFD timers are ${unproved}`);
|
|
545
|
+
if (device.vdoms && !has("management-vdom"))
|
|
546
|
+
found.push(`vdoms, a management VDOM is ${unproved}`);
|
|
545
547
|
for (const route of routing?.static ?? []) {
|
|
546
548
|
if (route.bfd && !has("static-route-bfd"))
|
|
547
549
|
found.push(`routing.static ${route.prefix} bfd, BFD on a static route is ${unproved}`);
|
|
@@ -907,6 +909,11 @@ class FortiOsFirewall {
|
|
|
907
909
|
const state = rule.match?.state;
|
|
908
910
|
return rule.action === "accept" && state !== undefined && state.length > 0 && state.every((one) => one === "established" || one === "related");
|
|
909
911
|
}
|
|
912
|
+
static managementOnly(root, device) {
|
|
913
|
+
const localIns = { ipv4: 0, ipv6: 0 };
|
|
914
|
+
const localInEntry = (family) => root.block(family === "ipv4" ? "firewall local-in-policy" : "firewall local-in-policy6").entry(++localIns[family]);
|
|
915
|
+
FortiOsFirewall.management(localInEntry, new Objects(root, device), device);
|
|
916
|
+
}
|
|
910
917
|
static render(root, device) {
|
|
911
918
|
const fw = device.firewall;
|
|
912
919
|
const objects = new Objects(root, device);
|
|
@@ -2166,6 +2173,155 @@ class FortiOsPlan {
|
|
|
2166
2173
|
}
|
|
2167
2174
|
}
|
|
2168
2175
|
|
|
2176
|
+
// src/platforms/fortios/vdoms.ts
|
|
2177
|
+
class FortiOsVdoms {
|
|
2178
|
+
static ROOT = "root";
|
|
2179
|
+
static GLOBAL = new Set([
|
|
2180
|
+
"vpn certificate local",
|
|
2181
|
+
"vpn certificate ca",
|
|
2182
|
+
"system global",
|
|
2183
|
+
'system replacemsg admin "pre_admin-disclaimer-text"',
|
|
2184
|
+
"system accprofile",
|
|
2185
|
+
"system admin",
|
|
2186
|
+
"system interface",
|
|
2187
|
+
"system dns",
|
|
2188
|
+
"system ntp",
|
|
2189
|
+
"system snmp sysinfo",
|
|
2190
|
+
"system snmp community",
|
|
2191
|
+
"system snmp user",
|
|
2192
|
+
"log disk setting",
|
|
2193
|
+
...["", "2", "3", "4"].flatMap((number) => [`log syslogd${number} setting`, `log syslogd${number} filter`]),
|
|
2194
|
+
"system sflow",
|
|
2195
|
+
"system netflow",
|
|
2196
|
+
"system session-helper"
|
|
2197
|
+
]);
|
|
2198
|
+
static MANAGEMENT_SECTIONS = [
|
|
2199
|
+
"firewall address",
|
|
2200
|
+
"firewall address6",
|
|
2201
|
+
"firewall addrgrp",
|
|
2202
|
+
"firewall addrgrp6",
|
|
2203
|
+
"firewall service custom",
|
|
2204
|
+
"router static",
|
|
2205
|
+
"router static6",
|
|
2206
|
+
"firewall local-in-policy",
|
|
2207
|
+
"firewall local-in-policy6"
|
|
2208
|
+
];
|
|
2209
|
+
static managementOf(device) {
|
|
2210
|
+
return device.vdoms?.management.name;
|
|
2211
|
+
}
|
|
2212
|
+
static vdomOf(device, name) {
|
|
2213
|
+
const management = device.vdoms?.management;
|
|
2214
|
+
if (!management)
|
|
2215
|
+
return FortiOsVdoms.ROOT;
|
|
2216
|
+
const lag = device.ports?.[name]?.lag;
|
|
2217
|
+
const holder = lag ?? name;
|
|
2218
|
+
return management.interfaces.includes(holder) ? management.name : FortiOsVdoms.ROOT;
|
|
2219
|
+
}
|
|
2220
|
+
static key(vdom, section) {
|
|
2221
|
+
return `@${vdom} ${section}`;
|
|
2222
|
+
}
|
|
2223
|
+
static scopeOf(key) {
|
|
2224
|
+
const named = /^@(\S+) (.+)$/.exec(key);
|
|
2225
|
+
if (named)
|
|
2226
|
+
return { scope: { kind: "vdom", vdom: named[1] }, section: named[2] };
|
|
2227
|
+
return FortiOsVdoms.GLOBAL.has(key) ? { scope: { kind: "global" }, section: key } : { scope: { kind: "vdom", vdom: FortiOsVdoms.ROOT }, section: key };
|
|
2228
|
+
}
|
|
2229
|
+
static isMulti(tree) {
|
|
2230
|
+
return tree.blocks.get("system global")?.settings.get("vdom-mode") === "multi-vdom";
|
|
2231
|
+
}
|
|
2232
|
+
static wrap(scope, lines) {
|
|
2233
|
+
return scope.kind === "global" ? ["config global", ...lines, "end"] : ["config vdom", `edit ${scope.vdom}`, ...lines, "next", "end"];
|
|
2234
|
+
}
|
|
2235
|
+
static named(vdom, text) {
|
|
2236
|
+
return text.replace(/^config /gm, `config @${vdom} `);
|
|
2237
|
+
}
|
|
2238
|
+
static listed(text) {
|
|
2239
|
+
return [...text.matchAll(/^name=([^/\s]+)\//gm)].map((match) => match[1]);
|
|
2240
|
+
}
|
|
2241
|
+
static plan(desired, current, planOne) {
|
|
2242
|
+
const want = FortiOsTree.parse(desired);
|
|
2243
|
+
const have = FortiOsTree.parse(current);
|
|
2244
|
+
const wantMulti = FortiOsVdoms.isMulti(want);
|
|
2245
|
+
const haveMulti = FortiOsVdoms.isMulti(have);
|
|
2246
|
+
const refused = (problem) => ({ steps: [], problems: [problem] });
|
|
2247
|
+
if (wantMulti && !haveMulti) {
|
|
2248
|
+
return refused("the config has vdoms and the device runs one VDOM. Turn multi-VDOM mode on by hand, on the console: `config system global`, `set vdom-mode multi-vdom`, `end`. It moves everything into root and ends every session");
|
|
2249
|
+
}
|
|
2250
|
+
if (!wantMulti && haveMulti) {
|
|
2251
|
+
return refused("the device runs several VDOMs and the config has no vdoms. Write vdoms.management, or turn multi-VDOM mode off by hand");
|
|
2252
|
+
}
|
|
2253
|
+
if (!wantMulti)
|
|
2254
|
+
return planOne(desired, current);
|
|
2255
|
+
const moved = FortiOsVdoms.moved(want, have);
|
|
2256
|
+
if (moved.length) {
|
|
2257
|
+
return refused(`${moved.map(({ name, from, to }) => `${name} from ${from} to ${to}`).join(", ")}: move an interface into another VDOM by hand, on the console, since it may carry the way in`);
|
|
2258
|
+
}
|
|
2259
|
+
const scopes = new Map;
|
|
2260
|
+
const scopeFor = (scope) => {
|
|
2261
|
+
const name = scope.kind === "global" ? "" : scope.vdom;
|
|
2262
|
+
if (!scopes.has(name))
|
|
2263
|
+
scopes.set(name, { scope, want: new FortiOsBlock, have: new FortiOsBlock });
|
|
2264
|
+
return scopes.get(name);
|
|
2265
|
+
};
|
|
2266
|
+
for (const [side, tree] of [
|
|
2267
|
+
["want", want],
|
|
2268
|
+
["have", have]
|
|
2269
|
+
]) {
|
|
2270
|
+
for (const [key, block] of tree.blocks) {
|
|
2271
|
+
const { scope, section } = FortiOsVdoms.scopeOf(key);
|
|
2272
|
+
scopeFor(scope)[side].blocks.set(section, block);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
const global = scopes.get("");
|
|
2276
|
+
const globalPlan = global ? planOne(FortiOsTree.print(global.want), FortiOsTree.print(global.have)) : { steps: [], problems: [] };
|
|
2277
|
+
const inScope = (scope, step) => ({ ...step, send: FortiOsVdoms.wrap(scope, step.send) });
|
|
2278
|
+
const removal = (step) => step.title.endsWith(", removed");
|
|
2279
|
+
const vdomPlans = [...scopes.values()].filter(({ scope }) => scope.kind === "vdom").map(({ scope, want: wanted, have: had }) => ({ scope, plan: planOne(FortiOsTree.print(wanted), FortiOsTree.print(had)) }));
|
|
2280
|
+
const vdomSteps = vdomPlans.flatMap(({ scope, plan }) => plan.steps.map((step) => inScope(scope, { ...step, title: `${scope.kind === "vdom" ? scope.vdom : ""}: ${step.title}` })));
|
|
2281
|
+
return {
|
|
2282
|
+
problems: [...globalPlan.problems, ...vdomPlans.flatMap(({ plan }) => plan.problems)],
|
|
2283
|
+
steps: [
|
|
2284
|
+
...globalPlan.steps.filter((step) => !removal(step)).map((step) => inScope({ kind: "global" }, step)),
|
|
2285
|
+
...vdomSteps,
|
|
2286
|
+
...globalPlan.steps.filter(removal).map((step) => inScope({ kind: "global" }, step))
|
|
2287
|
+
]
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
static moved(want, have) {
|
|
2291
|
+
const wanted = want.blocks.get("system interface");
|
|
2292
|
+
const had = have.blocks.get("system interface");
|
|
2293
|
+
if (!wanted || !had)
|
|
2294
|
+
return [];
|
|
2295
|
+
return [...wanted.entries.entries()].flatMap(([name, { block }]) => {
|
|
2296
|
+
const from = had.entries.get(name)?.block.settings.get("vdom");
|
|
2297
|
+
const to = block.settings.get("vdom");
|
|
2298
|
+
return from && to && FortiOsTree.bare(from) !== FortiOsTree.bare(to) ? [{ name, from: FortiOsTree.bare(from), to: FortiOsTree.bare(to) }] : [];
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
2301
|
+
static gaps(device) {
|
|
2302
|
+
if (!device.vdoms)
|
|
2303
|
+
return [];
|
|
2304
|
+
const found = [];
|
|
2305
|
+
const tunnels = Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "gre" || iface.type === "vxlan");
|
|
2306
|
+
if (tunnels.length)
|
|
2307
|
+
found.push(`vdoms with GRE or VXLAN tunnels (${tunnels.map(([name]) => name).join(", ")}), whose tables belong to a VDOM`);
|
|
2308
|
+
if (Object.values(device.interfaces ?? {}).some((iface) => iface.type === "bridge"))
|
|
2309
|
+
found.push("vdoms with a bridge, which needs transparent mode");
|
|
2310
|
+
const management = device.vdoms.management;
|
|
2311
|
+
const inManagement = (name) => name !== undefined && management.interfaces.includes(name);
|
|
2312
|
+
for (const route of device.routing?.static ?? []) {
|
|
2313
|
+
if (inManagement(route.interface))
|
|
2314
|
+
found.push(`routing.static ${route.prefix} leaves through ${route.interface}, in vdoms.management; write it there`);
|
|
2315
|
+
}
|
|
2316
|
+
for (const name of management.interfaces) {
|
|
2317
|
+
const settings = device.interfaces?.[name] ?? device.ports?.[name];
|
|
2318
|
+
if (settings && "vrf" in settings && settings.vrf)
|
|
2319
|
+
found.push(`vdoms.management interface ${name} in VRF ${settings.vrf}`);
|
|
2320
|
+
}
|
|
2321
|
+
return found;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2169
2325
|
// src/platforms/fortios/render.ts
|
|
2170
2326
|
class FortiOsRender {
|
|
2171
2327
|
static BANNER = 'system replacemsg admin "pre_admin-disclaimer-text"';
|
|
@@ -2408,8 +2564,24 @@ class FortiOsRender {
|
|
|
2408
2564
|
FortiOsRouting.render(root, device);
|
|
2409
2565
|
FortiOsRender.helpers(section("system session-helper"), device);
|
|
2410
2566
|
FortiOsFirewall.render(root, device);
|
|
2567
|
+
FortiOsRender.managementVdom(root, device);
|
|
2411
2568
|
return FortiOsTree.print(root);
|
|
2412
2569
|
}
|
|
2570
|
+
static managementVdom(root, device) {
|
|
2571
|
+
const management = device.vdoms?.management;
|
|
2572
|
+
if (!management)
|
|
2573
|
+
return;
|
|
2574
|
+
const vdom = new FortiOsBlock;
|
|
2575
|
+
for (const section of FortiOsVdoms.MANAGEMENT_SECTIONS)
|
|
2576
|
+
vdom.block(section);
|
|
2577
|
+
FortiOsRender.staticRoutes(vdom.block("router static"), vdom.block("router static6"), {
|
|
2578
|
+
...device,
|
|
2579
|
+
routing: { static: management.static ?? [] }
|
|
2580
|
+
});
|
|
2581
|
+
FortiOsFirewall.managementOnly(vdom, device);
|
|
2582
|
+
for (const [section, block] of vdom.blocks)
|
|
2583
|
+
root.blocks.set(FortiOsVdoms.key(management.name, section), block);
|
|
2584
|
+
}
|
|
2413
2585
|
static certificates(local, trusted, device) {
|
|
2414
2586
|
for (const [name, certificate] of Object.entries(device.certificates ?? {})) {
|
|
2415
2587
|
const text = FortiOsTree.quote(certificate.certificate.trim());
|
|
@@ -2445,6 +2617,9 @@ class FortiOsRender {
|
|
|
2445
2617
|
block.set("pre-login-banner", "enable");
|
|
2446
2618
|
if (device.lldp === true)
|
|
2447
2619
|
block.set("lldp-transmission", "enable").set("lldp-reception", "enable");
|
|
2620
|
+
const vdom = FortiOsVdoms.managementOf(device);
|
|
2621
|
+
if (vdom)
|
|
2622
|
+
block.set("vdom-mode", "multi-vdom").set("management-vdom", FortiOsTree.quote(vdom));
|
|
2448
2623
|
}
|
|
2449
2624
|
static users(profiles, admins, device) {
|
|
2450
2625
|
const roles = new Set(Object.values(device.users ?? {}).map((user) => user.role));
|
|
@@ -2471,7 +2646,7 @@ class FortiOsRender {
|
|
|
2471
2646
|
for (const port of Catalog.ports(device.platform, device.model)) {
|
|
2472
2647
|
const settings = device.ports?.[port];
|
|
2473
2648
|
const entry = block.entry(Catalog.spell(device.platform, device.model, port));
|
|
2474
|
-
entry.set("vdom", FortiOsTree.quote(
|
|
2649
|
+
entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, port)));
|
|
2475
2650
|
entry.set("type", "physical");
|
|
2476
2651
|
if (!settings) {
|
|
2477
2652
|
FortiOsRender.redirects(entry, device);
|
|
@@ -2495,7 +2670,7 @@ class FortiOsRender {
|
|
|
2495
2670
|
continue;
|
|
2496
2671
|
const members = Object.entries(device.ports ?? {}).filter(([, port]) => port?.lag === name).map(([port]) => Catalog.spell(device.platform, device.model, port));
|
|
2497
2672
|
const entry = block.entry(name);
|
|
2498
|
-
entry.set("vdom", FortiOsTree.quote(
|
|
2673
|
+
entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
|
|
2499
2674
|
entry.set("type", "aggregate");
|
|
2500
2675
|
if (members.length)
|
|
2501
2676
|
entry.set("member", FortiOsTree.quoteAll(members));
|
|
@@ -2513,7 +2688,7 @@ class FortiOsRender {
|
|
|
2513
2688
|
continue;
|
|
2514
2689
|
const parent = FortiOsRender.vlanParent(device, iface.vlan);
|
|
2515
2690
|
const entry = block.entry(name);
|
|
2516
|
-
entry.set("vdom", FortiOsTree.quote(
|
|
2691
|
+
entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
|
|
2517
2692
|
if (parent)
|
|
2518
2693
|
entry.set("interface", FortiOsTree.quote(FortiOsNames.nameOf(device, parent)));
|
|
2519
2694
|
entry.set("vlanid", device.vlans?.[iface.vlan]?.id ?? 0);
|
|
@@ -2531,7 +2706,7 @@ class FortiOsRender {
|
|
|
2531
2706
|
if (iface.type !== "loopback")
|
|
2532
2707
|
continue;
|
|
2533
2708
|
const entry = block.entry(name);
|
|
2534
|
-
entry.set("vdom", FortiOsTree.quote(
|
|
2709
|
+
entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
|
|
2535
2710
|
entry.set("type", "loopback");
|
|
2536
2711
|
if (iface.description)
|
|
2537
2712
|
entry.set("description", FortiOsTree.quote(iface.description));
|
|
@@ -2560,7 +2735,7 @@ class FortiOsRender {
|
|
|
2560
2735
|
tunnel.set("dstport", iface.port);
|
|
2561
2736
|
}
|
|
2562
2737
|
const entry = block.entry(name);
|
|
2563
|
-
entry.set("vdom", FortiOsTree.quote(
|
|
2738
|
+
entry.set("vdom", FortiOsTree.quote(FortiOsVdoms.vdomOf(device, name)));
|
|
2564
2739
|
entry.set("type", iface.type === "gre" ? "tunnel" : "vxlan");
|
|
2565
2740
|
entry.set("interface", via);
|
|
2566
2741
|
if (iface.type === "vxlan" && iface.mac)
|
|
@@ -2949,6 +3124,7 @@ class FortiOs extends Platform {
|
|
|
2949
3124
|
if (device.stp)
|
|
2950
3125
|
found.push("stp, ports are routed");
|
|
2951
3126
|
found.push(...FortiOs.transparentGaps(device));
|
|
3127
|
+
found.push(...FortiOsVdoms.gaps(device));
|
|
2952
3128
|
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
2953
3129
|
if (iface.type === "vlan" && iface.mac)
|
|
2954
3130
|
found.push(`interfaces.${name}.mac, which only RouterOS sets for now`);
|
|
@@ -2971,7 +3147,8 @@ class FortiOs extends Platform {
|
|
|
2971
3147
|
neighbors(device, _network = [], options = {}) {
|
|
2972
3148
|
return FortiOs.withCli(device, async (cli) => {
|
|
2973
3149
|
await FortiOs.checkVersion(cli, device);
|
|
2974
|
-
|
|
3150
|
+
const details = await FortiOs.inGlobal(cli, device, () => cli.run("diagnose lldprx neighbor details"));
|
|
3151
|
+
return FortiOs.neighborsOf(details, device);
|
|
2975
3152
|
}, options);
|
|
2976
3153
|
}
|
|
2977
3154
|
static neighborsOf(text, device) {
|
|
@@ -2994,14 +3171,14 @@ class FortiOs extends Platform {
|
|
|
2994
3171
|
return found;
|
|
2995
3172
|
}
|
|
2996
3173
|
plan(desired, current, options) {
|
|
2997
|
-
return
|
|
3174
|
+
return FortiOsVdoms.plan(desired, current, (want, have) => FortiOsPlan.of(want, have, options));
|
|
2998
3175
|
}
|
|
2999
3176
|
async apply(device, network, changes, options) {
|
|
3000
3177
|
await FortiOs.applyOnce(device, changes, options);
|
|
3001
3178
|
const switched = changes.steps.some((step) => step.send.some((line) => line.startsWith("set opmode ")));
|
|
3002
3179
|
if (!switched)
|
|
3003
3180
|
return;
|
|
3004
|
-
const again =
|
|
3181
|
+
const again = this.plan(FortiOsRender.of(device), await this.read(device, network, options), options);
|
|
3005
3182
|
if (!again.steps.length)
|
|
3006
3183
|
return;
|
|
3007
3184
|
console.log(" the operation mode changed; applying what it added on its own");
|
|
@@ -3010,7 +3187,8 @@ class FortiOs extends Platform {
|
|
|
3010
3187
|
async fill(device, _network, set, family, prefixes) {
|
|
3011
3188
|
await FortiOs.withCli(device, async (cli) => {
|
|
3012
3189
|
await FortiOs.checkVersion(cli, device);
|
|
3013
|
-
|
|
3190
|
+
const lines = FortiOs.fillLines(set, family, prefixes);
|
|
3191
|
+
for (const line of device.vdoms ? FortiOsVdoms.wrap({ kind: "vdom", vdom: FortiOsVdoms.ROOT }, lines) : lines) {
|
|
3014
3192
|
let answer = await cli.run(line);
|
|
3015
3193
|
if (FortiOsCli.QUESTION.test(cli.prompt) && line === "purge")
|
|
3016
3194
|
answer = await cli.answer("y");
|
|
@@ -3143,6 +3321,18 @@ class FortiOs extends Platform {
|
|
|
3143
3321
|
if (`${version[1]}.${version[2]}` !== FortiOs.SUPPORTED) {
|
|
3144
3322
|
throw new Error(`${device.name} runs FortiOS ${version[1]}.${version[2]}.${version[3]}; this adapter renders for FortiOS ${FortiOs.SUPPORTED}`);
|
|
3145
3323
|
}
|
|
3324
|
+
return status;
|
|
3325
|
+
}
|
|
3326
|
+
static async inGlobal(cli, device, work) {
|
|
3327
|
+
const multi = typeof device === "boolean" ? device : Boolean(device.vdoms);
|
|
3328
|
+
if (!multi)
|
|
3329
|
+
return work();
|
|
3330
|
+
await cli.run("config global");
|
|
3331
|
+
try {
|
|
3332
|
+
return await work();
|
|
3333
|
+
} finally {
|
|
3334
|
+
await FortiOs.leave(cli);
|
|
3335
|
+
}
|
|
3146
3336
|
}
|
|
3147
3337
|
static scrub(text) {
|
|
3148
3338
|
const root = FortiOsTree.parse(text);
|
|
@@ -3166,11 +3356,14 @@ class FortiOs extends Platform {
|
|
|
3166
3356
|
return FortiOsTree.print(root);
|
|
3167
3357
|
}
|
|
3168
3358
|
static async readConfig(cli, device) {
|
|
3169
|
-
await FortiOs.checkVersion(cli, device);
|
|
3359
|
+
const status = await FortiOs.checkVersion(cli, device);
|
|
3360
|
+
const show = (section) => cli.run(`${FortiOsRender.FULL.includes(section) ? "show full-configuration" : "show"} ${section}`, 120000);
|
|
3170
3361
|
const parts = [];
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3362
|
+
if (/^Virtual domain configuration: (multiple|split-task)/m.test(status)) {
|
|
3363
|
+
parts.push(...await FortiOs.readVdoms(cli, device, show));
|
|
3364
|
+
} else {
|
|
3365
|
+
for (const section of FortiOsRender.SECTIONS)
|
|
3366
|
+
parts.push(await show(section));
|
|
3174
3367
|
}
|
|
3175
3368
|
const text = parts.join(`
|
|
3176
3369
|
`);
|
|
@@ -3178,26 +3371,53 @@ class FortiOs extends Platform {
|
|
|
3178
3371
|
throw new Error(`${device.name} returned no configuration`);
|
|
3179
3372
|
return FortiOs.scrub(FortiOsPlan.clean(text));
|
|
3180
3373
|
}
|
|
3374
|
+
static async readVdoms(cli, device, show) {
|
|
3375
|
+
const management = FortiOsVdoms.managementOf(device);
|
|
3376
|
+
if (!management)
|
|
3377
|
+
throw new Error(`${device.name} runs several VDOMs and its config has no vdoms; write vdoms.management`);
|
|
3378
|
+
const parts = [];
|
|
3379
|
+
const inVdom = async (vdom, sections) => {
|
|
3380
|
+
await cli.run("config vdom");
|
|
3381
|
+
await cli.run(`edit ${vdom}`);
|
|
3382
|
+
try {
|
|
3383
|
+
for (const section of sections) {
|
|
3384
|
+
const text = await show(section);
|
|
3385
|
+
parts.push(vdom === FortiOsVdoms.ROOT ? text : FortiOsVdoms.named(vdom, text));
|
|
3386
|
+
}
|
|
3387
|
+
} finally {
|
|
3388
|
+
await FortiOs.leave(cli);
|
|
3389
|
+
}
|
|
3390
|
+
};
|
|
3391
|
+
const listed = await FortiOs.inGlobal(cli, true, async () => {
|
|
3392
|
+
for (const section of FortiOsRender.SECTIONS.filter((one) => FortiOsVdoms.GLOBAL.has(one)))
|
|
3393
|
+
parts.push(await show(section));
|
|
3394
|
+
return FortiOsVdoms.listed(await cli.run("diagnose sys vd list"));
|
|
3395
|
+
});
|
|
3396
|
+
await inVdom(FortiOsVdoms.ROOT, FortiOsRender.SECTIONS.filter((one) => !FortiOsVdoms.GLOBAL.has(one)));
|
|
3397
|
+
if (listed.includes(management))
|
|
3398
|
+
await inVdom(management, FortiOsVdoms.MANAGEMENT_SECTIONS);
|
|
3399
|
+
return parts;
|
|
3400
|
+
}
|
|
3181
3401
|
static async leave(cli) {
|
|
3182
3402
|
for (let level = 0;level < 10 && /\(.*\) [#$] $/.test(cli.prompt); level++)
|
|
3183
3403
|
await cli.run("end");
|
|
3184
3404
|
}
|
|
3185
|
-
static async saving(cli, mode, seconds) {
|
|
3405
|
+
static async saving(cli, mode, multi, seconds) {
|
|
3186
3406
|
const lines = ["config system global", `set cfg-save ${mode}`, ...seconds ? [`set cfg-revert-timeout ${seconds}`] : [], "end"];
|
|
3187
|
-
for (const line of lines) {
|
|
3407
|
+
for (const line of multi ? FortiOsVdoms.wrap({ kind: "global" }, lines) : lines) {
|
|
3188
3408
|
const answer = await cli.run(line);
|
|
3189
3409
|
if (FortiOs.REFUSED.test(answer))
|
|
3190
3410
|
throw new Error(`the device refused ${line}: ${answer.trim().slice(0, 300)}`);
|
|
3191
3411
|
}
|
|
3192
3412
|
}
|
|
3193
|
-
static async configure(cli, plan, guard) {
|
|
3413
|
+
static async configure(cli, plan, guard, multi) {
|
|
3194
3414
|
const minutes = guard.seconds / 60;
|
|
3195
|
-
await FortiOs.saving(cli, "revert", guard.seconds);
|
|
3415
|
+
await FortiOs.saving(cli, "revert", multi, guard.seconds);
|
|
3196
3416
|
const restore = async (reason) => {
|
|
3197
3417
|
await FortiOs.leave(cli).catch(() => {
|
|
3198
3418
|
return;
|
|
3199
3419
|
});
|
|
3200
|
-
await cli.run("execute cfg reload").catch(() => {
|
|
3420
|
+
await FortiOs.inGlobal(cli, multi, () => cli.run("execute cfg reload")).catch(() => {
|
|
3201
3421
|
return;
|
|
3202
3422
|
});
|
|
3203
3423
|
return new Reloaded(`${reason}
|
|
@@ -3231,27 +3451,28 @@ class FortiOs extends Platform {
|
|
|
3231
3451
|
} catch (error) {
|
|
3232
3452
|
throw await restore(`a fresh login failed: ${error.message}`);
|
|
3233
3453
|
}
|
|
3234
|
-
const saved = await cli.run("execute cfg save", 120000);
|
|
3454
|
+
const saved = await FortiOs.inGlobal(cli, multi, () => cli.run("execute cfg save", 120000));
|
|
3235
3455
|
if (!/config saved|no config to be saved/.test(saved)) {
|
|
3236
3456
|
throw new Error(`access was proved but the save failed; the device reverts in ${minutes} minutes:
|
|
3237
3457
|
${saved.trim().slice(0, 300)}`);
|
|
3238
3458
|
}
|
|
3239
|
-
await FortiOs.saving(cli, "automatic");
|
|
3459
|
+
await FortiOs.saving(cli, "automatic", multi);
|
|
3240
3460
|
console.log(" logged in again, saved");
|
|
3241
3461
|
}
|
|
3242
3462
|
static async settle(device, options) {
|
|
3243
3463
|
for (let attempt = 0;attempt < FortiOs.RELOAD.attempts; attempt++) {
|
|
3244
3464
|
await new Promise((done) => setTimeout(done, FortiOs.RELOAD.interval));
|
|
3245
3465
|
const settled = await FortiOs.withCli(device, async (cli) => {
|
|
3246
|
-
|
|
3466
|
+
const reloaded = await FortiOs.inGlobal(cli, device, () => cli.run("execute cfg reload"));
|
|
3467
|
+
if (!/no config to be reloaded/.test(reloaded))
|
|
3247
3468
|
return false;
|
|
3248
|
-
await FortiOs.saving(cli, "automatic");
|
|
3469
|
+
await FortiOs.saving(cli, "automatic", Boolean(device.vdoms));
|
|
3249
3470
|
return true;
|
|
3250
3471
|
}, options).catch(() => false);
|
|
3251
3472
|
if (settled)
|
|
3252
3473
|
return "the device is back and saves changes on its own again";
|
|
3253
3474
|
}
|
|
3254
|
-
return "the device did not come back in time and still reverts every change; set `cfg-save automatic` under `config system global`";
|
|
3475
|
+
return "the device did not come back in time and still reverts every change; set `cfg-save automatic` under `config system global` (in `config global` on a device with VDOMs)";
|
|
3255
3476
|
}
|
|
3256
3477
|
static async applyOnce(device, plan, options) {
|
|
3257
3478
|
const seconds = options.rollback * 60;
|
|
@@ -3259,7 +3480,7 @@ class FortiOs extends Platform {
|
|
|
3259
3480
|
if (options.console) {
|
|
3260
3481
|
await FortiOs.consoleSession(device, async (cli, relogin) => {
|
|
3261
3482
|
await FortiOs.checkVersion(cli, device);
|
|
3262
|
-
await FortiOs.configure(cli, plan, { seconds, prove: relogin });
|
|
3483
|
+
await FortiOs.configure(cli, plan, { seconds, prove: relogin }, Boolean(device.vdoms));
|
|
3263
3484
|
});
|
|
3264
3485
|
return;
|
|
3265
3486
|
}
|
|
@@ -3271,7 +3492,7 @@ class FortiOs extends Platform {
|
|
|
3271
3492
|
const proof = await FortiOs.login(device);
|
|
3272
3493
|
proof.end();
|
|
3273
3494
|
}
|
|
3274
|
-
});
|
|
3495
|
+
}, Boolean(device.vdoms));
|
|
3275
3496
|
});
|
|
3276
3497
|
} catch (error) {
|
|
3277
3498
|
if (!(error instanceof Reloaded))
|
|
@@ -7901,9 +8122,23 @@ class RouterOsRender {
|
|
|
7901
8122
|
}
|
|
7902
8123
|
untracked(family) {
|
|
7903
8124
|
const menu = family === "ipv4" ? "/ip firewall raw" : "/ipv6 firewall raw";
|
|
7904
|
-
|
|
7905
|
-
|
|
8125
|
+
const untracked = (this.device.firewall?.untracked ?? []).filter((prefix) => Address2.family(prefix) === family);
|
|
8126
|
+
for (const address of RouterOsRender.natAddresses(this.device)) {
|
|
8127
|
+
if (Address2.family(address) !== family || !untracked.some((prefix) => Address2.contains(prefix, address)))
|
|
7906
8128
|
continue;
|
|
8129
|
+
for (const [direction, property] of [
|
|
8130
|
+
["to", "dst-address"],
|
|
8131
|
+
["from", "src-address"]
|
|
8132
|
+
]) {
|
|
8133
|
+
this.add(menu, "add", {
|
|
8134
|
+
action: "accept",
|
|
8135
|
+
chain: "prerouting",
|
|
8136
|
+
comment: `Tracked for NAT, ${direction} ${address}`,
|
|
8137
|
+
[property]: address
|
|
8138
|
+
});
|
|
8139
|
+
}
|
|
8140
|
+
}
|
|
8141
|
+
for (const prefix of untracked) {
|
|
7907
8142
|
for (const [direction, property] of [
|
|
7908
8143
|
["to", "dst-address"],
|
|
7909
8144
|
["from", "src-address"]
|
|
@@ -7917,6 +8152,17 @@ class RouterOsRender {
|
|
|
7917
8152
|
}
|
|
7918
8153
|
}
|
|
7919
8154
|
}
|
|
8155
|
+
static natAddresses(device) {
|
|
8156
|
+
const nat = device.firewall?.nat;
|
|
8157
|
+
const addresses = [
|
|
8158
|
+
...(nat?.source ?? []).flatMap((rule) => rule.action === "masquerade" ? [] : [rule.to]),
|
|
8159
|
+
...(nat?.destination ?? []).flatMap((rule) => [
|
|
8160
|
+
...rule.match?.dst ? [rule.match.dst] : [],
|
|
8161
|
+
...rule.action === "masquerade" ? [] : [rule.to]
|
|
8162
|
+
])
|
|
8163
|
+
];
|
|
8164
|
+
return [...new Set(addresses)];
|
|
8165
|
+
}
|
|
7920
8166
|
families(match) {
|
|
7921
8167
|
const sets = this.device.firewall?.address_sets ?? {};
|
|
7922
8168
|
const address = match?.src ?? match?.dst;
|
|
@@ -11006,6 +11252,32 @@ class Validator {
|
|
|
11006
11252
|
Validator.checkFirewall(device, names, cidr);
|
|
11007
11253
|
Validator.checkServices(device, names, report, cidr);
|
|
11008
11254
|
Validator.checkCertificates(device, names, report);
|
|
11255
|
+
Validator.checkVdoms(device, names, report, cidr, ip);
|
|
11256
|
+
}
|
|
11257
|
+
static checkVdoms(device, names, report, cidr, ip) {
|
|
11258
|
+
const management = device.vdoms?.management;
|
|
11259
|
+
if (!management)
|
|
11260
|
+
return;
|
|
11261
|
+
if (device.platform !== "fortios")
|
|
11262
|
+
report("vdoms are a FortiOS setting");
|
|
11263
|
+
if (management.name === "root")
|
|
11264
|
+
report("vdoms.management.name is root, the VDOM everything else is in; give it a name of its own");
|
|
11265
|
+
for (const name of management.interfaces) {
|
|
11266
|
+
names.expectInterface("vdoms.management interface", name);
|
|
11267
|
+
const port = device.ports?.[name];
|
|
11268
|
+
if (port?.lag)
|
|
11269
|
+
report(`vdoms.management interface ${name} is in LAG ${port.lag}; list the LAG`);
|
|
11270
|
+
}
|
|
11271
|
+
for (const route of management.static ?? []) {
|
|
11272
|
+
const where = `vdoms.management static route ${route.prefix}`;
|
|
11273
|
+
cidr(where, route.prefix);
|
|
11274
|
+
if (route.via)
|
|
11275
|
+
ip(where, route.via);
|
|
11276
|
+
if (!route.via && !route.interface && !route.blackhole)
|
|
11277
|
+
report(`${where} has no next hop`);
|
|
11278
|
+
if (route.interface && !management.interfaces.includes(route.interface))
|
|
11279
|
+
report(`${where} leaves through ${route.interface}, which is not in the VDOM`);
|
|
11280
|
+
}
|
|
11009
11281
|
}
|
|
11010
11282
|
static checkLimits(device) {
|
|
11011
11283
|
const limits = Catalog.limits(device.platform, device.model);
|
|
@@ -2,6 +2,8 @@ import type { Finding, Network } from "./define.js";
|
|
|
2
2
|
export declare class Validator {
|
|
3
3
|
static run(network: Network): Finding[];
|
|
4
4
|
private static checkDevice;
|
|
5
|
+
/** A management VDOM: FortiOS only, not `root`, with declared interfaces, and routes that leave through its own. */
|
|
6
|
+
private static checkVdoms;
|
|
5
7
|
/** What the model's hardware holds, from the catalog. A LAG with too many members is an error; too many switch rules is a warning, since the limit is only roughly known. */
|
|
6
8
|
private static checkLimits;
|
|
7
9
|
private static checkVlans;
|
|
@@ -195,9 +195,9 @@ type Names<C, N, F extends Field, Ks extends Key<C>> = {
|
|
|
195
195
|
[K in Ks]: Named<C, N, F, K>;
|
|
196
196
|
}[Ks];
|
|
197
197
|
/** The catalog as written, with any key the shape does not have refused, at every level. */
|
|
198
|
-
type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret | ((...args: never[]) => unknown) ? T : {
|
|
198
|
+
type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret | ((...args: never[]) => unknown) ? T : S extends unknown ? {
|
|
199
199
|
[K in keyof T]: K extends keyof S ? Exact<NonNullable<S[K]>, T[K]> : never;
|
|
200
|
-
};
|
|
200
|
+
} : never;
|
|
201
201
|
/** The names as written, with a name or a role Upstreams does not make refused. */
|
|
202
202
|
type ExactNames<N> = {
|
|
203
203
|
[F in keyof N]: F extends keyof UpstreamNames ? N[F] extends string | ((...args: never[]) => unknown) ? N[F] : {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Device } from "../schema/index.js";
|
|
2
2
|
/** A feature a platform renders once proved on one of its devices. */
|
|
3
|
-
export type ProvedFeature = "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter";
|
|
3
|
+
export type ProvedFeature = "bfd-timers" | "static-route-bfd" | "multihop-bfd" | "alarm-severity" | "mac-limit" | "ethertype-filter" | "management-vdom";
|
|
4
4
|
export declare class Derive {
|
|
5
5
|
/** Each LAG's number: its own `id`, or its position among the device's LAGs. */
|
|
6
6
|
static lagIds(device: Device): Map<string, number>;
|
|
@@ -20,6 +20,8 @@ export declare class FortiOsFirewall {
|
|
|
20
20
|
};
|
|
21
21
|
/** A rule the platform keeps without being told: replies to a session it let through, and packets that belong to one, pass on their own. */
|
|
22
22
|
static isImplied(rule: FilterRule<string, string>): boolean;
|
|
23
|
+
/** Who reaches the device's own services, alone: for a management VDOM, which routes no traffic of its own. */
|
|
24
|
+
static managementOnly(root: FortiOsBlock, device: Device): void;
|
|
23
25
|
/** Packet filtering, who reaches the device, NAT and stateless ACLs. */
|
|
24
26
|
static render(root: FortiOsBlock, device: Device): void;
|
|
25
27
|
/**
|
|
@@ -92,12 +92,18 @@ export declare class FortiOs extends Platform {
|
|
|
92
92
|
private static consoleSession;
|
|
93
93
|
/** Log in, open the CLI, and run `work` in it: through the console when asked, else over SSH. */
|
|
94
94
|
private static withCli;
|
|
95
|
-
/** Refuse a device on a release this adapter was not proved on, before anything is read or sent. */
|
|
95
|
+
/** Refuse a device on a release this adapter was not proved on, before anything is read or sent. Gives what `get system status` printed. */
|
|
96
96
|
private static checkVersion;
|
|
97
|
+
/** Run `work` among the device's own settings: inside `config global` on a device with VDOMs, as it is on one without. */
|
|
98
|
+
private static inGlobal;
|
|
97
99
|
/** Secrets the device prints: a password or an SNMPv3 key as a cipher, a certificate's key encrypted, and the SNMP community in clear. None leave the read. */
|
|
98
100
|
private static scrub;
|
|
99
101
|
/** Every section Circuit manages, as the device holds it, credentials scrubbed. */
|
|
100
102
|
private static readConfig;
|
|
103
|
+
/**
|
|
104
|
+
* A device in multi-VDOM mode, read where each section lives: the device's own under `config global`, the rest in `root`, and a management VDOM's sections named as the render names them. A VDOM that does not exist yet is not entered, since `edit` would make it.
|
|
105
|
+
*/
|
|
106
|
+
private static readVdoms;
|
|
101
107
|
/** Leave every `config` and `edit` level, back to the top prompt. */
|
|
102
108
|
private static leave;
|
|
103
109
|
/** Send the lines that set how the device saves changes. */
|
|
@@ -42,6 +42,8 @@ export declare class FortiOsRender {
|
|
|
42
42
|
private static allowAccess;
|
|
43
43
|
/** The whole configuration the device should run, in the form `show` prints. */
|
|
44
44
|
static of(device: Device): string;
|
|
45
|
+
/** A management VDOM's routes, and who reaches the device's own services through it, as sections of their own after everything else. */
|
|
46
|
+
private static managementVdom;
|
|
45
47
|
/** A certificate with its key is the device's own; one without is only trusted. */
|
|
46
48
|
private static certificates;
|
|
47
49
|
private static global;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Device } from "../../schema/index.js";
|
|
2
|
+
import type { Plan } from "../platform.js";
|
|
3
|
+
import { FortiOsBlock } from "./tree.js";
|
|
4
|
+
/** Where a section lives in multi-VDOM mode: the device's own, or one VDOM's. */
|
|
5
|
+
type Scope = {
|
|
6
|
+
kind: "global";
|
|
7
|
+
} | {
|
|
8
|
+
kind: "vdom";
|
|
9
|
+
vdom: string;
|
|
10
|
+
};
|
|
11
|
+
export declare class FortiOsVdoms {
|
|
12
|
+
/** The VDOM everything is in unless named otherwise. */
|
|
13
|
+
static readonly ROOT = "root";
|
|
14
|
+
/**
|
|
15
|
+
* Sections under `config global` in multi-VDOM mode, from FortiOS 6.2's CLI reference: the device's identity, logins, interfaces, name resolution, time, SNMP, logging targets, flow export and session helpers. Every other section belongs to a VDOM. Not proved on a device yet.
|
|
16
|
+
*/
|
|
17
|
+
static readonly GLOBAL: Set<string>;
|
|
18
|
+
/** What a management VDOM holds: its routes, and who reaches the device's own services through it, with the objects those name. */
|
|
19
|
+
static readonly MANAGEMENT_SECTIONS: readonly ["firewall address", "firewall address6", "firewall addrgrp", "firewall addrgrp6", "firewall service custom", "router static", "router static6", "firewall local-in-policy", "firewall local-in-policy6"];
|
|
20
|
+
/** The management VDOM's name, when the device has one. */
|
|
21
|
+
static managementOf(device: Device): string | undefined;
|
|
22
|
+
/** The VDOM a port or interface is in, by Circuit's name. A port in a LAG is where its LAG is. */
|
|
23
|
+
static vdomOf(device: Device, name: string): string;
|
|
24
|
+
/** A management VDOM's section as the render and the read write it. */
|
|
25
|
+
static key(vdom: string, section: string): string;
|
|
26
|
+
/** A section's scope, and its name as the device writes it. */
|
|
27
|
+
static scopeOf(key: string): {
|
|
28
|
+
scope: Scope;
|
|
29
|
+
section: string;
|
|
30
|
+
};
|
|
31
|
+
/** Whether a configuration runs in multi-VDOM mode. */
|
|
32
|
+
static isMulti(tree: FortiOsBlock): boolean;
|
|
33
|
+
/** Lines sent in a scope: inside `config global`, or inside one VDOM. */
|
|
34
|
+
static wrap(scope: Scope, lines: readonly string[]): string[];
|
|
35
|
+
/** What `show` printed in a management VDOM, each top-level section named as the render names it. */
|
|
36
|
+
static named(vdom: string, text: string): string;
|
|
37
|
+
/** The VDOMs `diagnose sys vd list` names, such as `name=mgmt/mgmt index=1`. */
|
|
38
|
+
static listed(text: string): string[];
|
|
39
|
+
/**
|
|
40
|
+
* A plan for a device that may run in multi-VDOM mode. In one VDOM it is `planOne` as it is. In several, each scope is planned on its own and its steps sent in that scope: the global steps first, then each VDOM's, then the global removals, so an interface is gone only once no VDOM names it.
|
|
41
|
+
*
|
|
42
|
+
* Turning multi-VDOM mode on or off, and moving an interface into another VDOM, are left to an operator: the first moves everything into `root` and ends every session, and the second can cut the way in. A plan that needs either holds only the problem.
|
|
43
|
+
*/
|
|
44
|
+
static plan(desired: string, current: string, planOne: (desired: string, current: string) => Plan): Plan;
|
|
45
|
+
/** Interfaces the config puts in another VDOM than the device has them in. */
|
|
46
|
+
private static moved;
|
|
47
|
+
/** What a management VDOM can't hold yet, or a config with one can't have. */
|
|
48
|
+
static gaps(device: Device): string[];
|
|
49
|
+
}
|
|
50
|
+
export {};
|
|
@@ -164,8 +164,14 @@ export declare class RouterOsRender {
|
|
|
164
164
|
*/
|
|
165
165
|
private addressLists;
|
|
166
166
|
private firewall;
|
|
167
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* Untracked prefixes skip connection tracking in the raw table, both ways, before a connection is ever made.
|
|
169
|
+
*
|
|
170
|
+
* An address NAT uses inside one stays tracked, by an `accept` in each direction before every `notrack`: a reply to it that skipped tracking would never be translated back.
|
|
171
|
+
*/
|
|
168
172
|
private untracked;
|
|
173
|
+
/** Every address NAT translates to or from: each translation's target, and the address a destination translation matches. Once each, in rule order. */
|
|
174
|
+
static natAddresses(device: Device): string[];
|
|
169
175
|
/**
|
|
170
176
|
* The families a rule applies to. An address in the rule decides it; otherwise it applies to both.
|
|
171
177
|
*
|