@takodotid/circuit 0.39.0 → 0.41.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 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("root"));
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("root"));
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("root"));
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("root"));
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("root"));
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
- return FortiOs.neighborsOf(await cli.run("diagnose lldprx neighbor details"), device);
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 FortiOsPlan.of(desired, current, options);
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 = FortiOsPlan.of(FortiOsRender.of(device), await this.read(device, network, options), options);
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
- for (const line of FortiOs.fillLines(set, family, prefixes)) {
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
- for (const section of FortiOsRender.SECTIONS) {
3172
- const show = FortiOsRender.FULL.includes(section) ? "show full-configuration" : "show";
3173
- parts.push(await cli.run(`${show} ${section}`, 120000));
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
- if (!/no config to be reloaded/.test(await cli.run("execute cfg reload")))
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
- for (const prefix of this.device.firewall?.untracked ?? []) {
7905
- if (Address2.family(prefix) !== family)
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);
@@ -11195,13 +11467,17 @@ class Validator {
11195
11467
  const policies = device.policies ?? {};
11196
11468
  const sets = device.prefix_sets ?? {};
11197
11469
  for (const [name, rules] of Object.entries(policies)) {
11198
- for (const rule of rules) {
11470
+ rules.forEach((rule, index) => {
11471
+ const beside = ["match", "set", "action"].filter((key) => rule.call !== undefined && rule[key] !== undefined);
11472
+ if (beside.length) {
11473
+ report(`policy ${name} rule ${index + 1} has call and ${beside.join(", ")}: a call is a rule of its own, run for every route. Put ${beside.join(", ")} in the next rule, or the match inside ${rule.call}`);
11474
+ }
11199
11475
  names.expect(`policy ${name} call`, rule.call, rule.call !== undefined && rule.call in policies);
11200
11476
  const set = rule.match?.prefix_set;
11201
11477
  names.expect(`policy ${name} prefix set`, set, set !== undefined && set in sets);
11202
11478
  if (rule.match?.prefix)
11203
11479
  cidr(`policy ${name}`, rule.match.prefix);
11204
- }
11480
+ });
11205
11481
  }
11206
11482
  const visit = (policy, path) => {
11207
11483
  if (path.includes(policy))