@takodotid/circuit 0.38.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/chunks/{index-yzw86rc8.js → index-k38bby43.js} +62 -4
- package/dist/chunks/{server-cxr9ynxc.js → server-ggertnce.js} +1 -1
- package/dist/cli/index.js +332 -36
- package/dist/helpers/index.js +71 -29
- package/dist/types/core/cabling.d.ts +6 -0
- package/dist/types/core/validator.d.ts +2 -0
- package/dist/types/helpers/cables.d.ts +16 -20
- package/dist/types/helpers/index.d.ts +1 -1
- package/dist/types/helpers/upstreams.d.ts +102 -19
- 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/dist/types/schema/interfaces.d.ts +1 -1
- package/package.json +1 -1
- package/src/cli/index.ts +12 -4
- package/src/core/cabling.ts +30 -0
- package/src/core/validator.ts +25 -0
- package/src/helpers/cables.ts +21 -43
- package/src/helpers/index.ts +1 -1
- package/src/helpers/upstreams.ts +175 -39
- 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/src/schema/interfaces.ts +1 -1
- package/dist/chunks/index-6wb41q67.js +0 -62
package/dist/helpers/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Catalog
|
|
3
|
-
} from "../chunks/index-6wb41q67.js";
|
|
4
1
|
import {
|
|
5
2
|
Address2
|
|
6
3
|
} from "../chunks/index-n7w3tg5y.js";
|
|
@@ -235,8 +232,8 @@ class Bgp {
|
|
|
235
232
|
class Cables {
|
|
236
233
|
static link(spec) {
|
|
237
234
|
return {
|
|
238
|
-
a: { [spec.a.port]: Cables.end(spec, spec.b, spec.b.port) },
|
|
239
|
-
b: { [spec.b.port]: Cables.end(spec, spec.a, spec.a.port) }
|
|
235
|
+
a: { [spec.a.port]: Cables.end(spec, spec.b.device, spec.b.port) },
|
|
236
|
+
b: { [spec.b.port]: Cables.end(spec, spec.a.device, spec.a.port) }
|
|
240
237
|
};
|
|
241
238
|
}
|
|
242
239
|
static lag(spec) {
|
|
@@ -248,18 +245,17 @@ class Cables {
|
|
|
248
245
|
throw new Error(`Cables.lag: ${spec.b.device} ${far} is cabled to both ${other} and ${near} on ${spec.a.device}`);
|
|
249
246
|
taken.set(far, near);
|
|
250
247
|
}
|
|
251
|
-
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b, far), lag: spec.a.lag }]));
|
|
252
|
-
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a, near), lag: spec.b.lag }]));
|
|
248
|
+
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b.device, far), lag: spec.a.lag }]));
|
|
249
|
+
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a.device, near), lag: spec.b.lag }]));
|
|
253
250
|
return {
|
|
254
251
|
a,
|
|
255
252
|
b
|
|
256
253
|
};
|
|
257
254
|
}
|
|
258
|
-
static end(shared,
|
|
259
|
-
const farEnd = { device: far.device, port, label: Catalog.spell(far.platform, far.model, port) };
|
|
255
|
+
static end(shared, device, port) {
|
|
260
256
|
return {
|
|
261
|
-
|
|
262
|
-
|
|
257
|
+
link: { device, port, ...shared.through ? { through: shared.through } : {} },
|
|
258
|
+
...shared.describe ? { description: shared.describe({ device, port }) } : {}
|
|
263
259
|
};
|
|
264
260
|
}
|
|
265
261
|
}
|
|
@@ -668,21 +664,61 @@ class Filters {
|
|
|
668
664
|
}
|
|
669
665
|
}
|
|
670
666
|
// src/helpers/upstreams.ts
|
|
667
|
+
var DEFAULTS = {
|
|
668
|
+
vlan: "{key}",
|
|
669
|
+
native: "{key}-native",
|
|
670
|
+
interface: "{key}",
|
|
671
|
+
import: "{KEY}-IN",
|
|
672
|
+
export: "{KEY}-OUT",
|
|
673
|
+
group: "{key}"
|
|
674
|
+
};
|
|
675
|
+
|
|
671
676
|
class Upstreams {
|
|
672
|
-
static define(catalog) {
|
|
677
|
+
static define(catalog, options = {}) {
|
|
678
|
+
const names = options.names ?? {};
|
|
673
679
|
const all = Object.entries(catalog);
|
|
674
680
|
const nameOf = (key, upstream) => upstream.name ?? key;
|
|
675
|
-
const policy = (key) => key.toUpperCase();
|
|
676
681
|
const grouped = (upstream) => upstream.role === "rs" || Boolean(upstream.multihop);
|
|
682
|
+
const named = (field, key, upstream) => {
|
|
683
|
+
const given = names[field];
|
|
684
|
+
const pattern = given === undefined || typeof given === "string" ? given ?? DEFAULTS[field] : (upstream.tunnel && "tunnel" in given ? given.tunnel : given[upstream.role]) ?? DEFAULTS[field];
|
|
685
|
+
return pattern.replaceAll("{key}", key).replaceAll("{KEY}", key.toUpperCase()).replaceAll("{number}", () => {
|
|
686
|
+
const number = upstream.handoff?.number ?? upstream.tunnel?.number;
|
|
687
|
+
if (number === undefined)
|
|
688
|
+
throw new Error(`Upstreams: ${key} needs a number for ${field} name ${pattern}`);
|
|
689
|
+
return String(number);
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
for (const field of Object.keys(DEFAULTS)) {
|
|
693
|
+
const taken = new Map;
|
|
694
|
+
for (const [key, upstream] of all) {
|
|
695
|
+
if (field === "native" && upstream.handoff?.native === undefined)
|
|
696
|
+
continue;
|
|
697
|
+
if (field === "import" && !upstream.import)
|
|
698
|
+
continue;
|
|
699
|
+
if (field === "group" && !grouped(upstream))
|
|
700
|
+
continue;
|
|
701
|
+
if (field === "vlan" && !upstream.handoff || field === "interface" && !upstream.handoff && !upstream.tunnel)
|
|
702
|
+
continue;
|
|
703
|
+
const name = named(field, key, upstream);
|
|
704
|
+
const other = taken.get(name);
|
|
705
|
+
if (other)
|
|
706
|
+
throw new Error(`Upstreams: ${other} and ${key} both have the ${field} name ${name}`);
|
|
707
|
+
taken.set(name, key);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
const vlan = (key, upstream) => named("vlan", key, upstream);
|
|
711
|
+
const native = (key, upstream) => named("native", key, upstream);
|
|
712
|
+
const policy = (direction, key, upstream) => named(direction, key, upstream);
|
|
677
713
|
const vlans = (device) => {
|
|
678
714
|
const result = {};
|
|
679
715
|
for (const [key, upstream] of all) {
|
|
680
716
|
const handoff = upstream.handoff;
|
|
681
717
|
if (!handoff || device !== undefined && handoff.port?.device !== device)
|
|
682
718
|
continue;
|
|
683
|
-
result[key] = { id: handoff.vlan, description: nameOf(key, upstream) };
|
|
719
|
+
result[vlan(key, upstream)] = { id: handoff.vlan, description: nameOf(key, upstream) };
|
|
684
720
|
if (device !== undefined && handoff.native !== undefined) {
|
|
685
|
-
result[
|
|
721
|
+
result[native(key, upstream)] = {
|
|
686
722
|
id: handoff.native,
|
|
687
723
|
description: `Native toward ${nameOf(key, upstream)}, nothing else in it`
|
|
688
724
|
};
|
|
@@ -690,7 +726,7 @@ class Upstreams {
|
|
|
690
726
|
}
|
|
691
727
|
return result;
|
|
692
728
|
};
|
|
693
|
-
const vlansOn = (device) => all.filter(([, upstream]) => upstream.handoff?.port?.device === device).map(([key]) => key);
|
|
729
|
+
const vlansOn = (device) => all.filter(([, upstream]) => upstream.handoff?.port?.device === device).map(([key, upstream]) => vlan(key, upstream));
|
|
694
730
|
const ports = (device) => {
|
|
695
731
|
const result = {};
|
|
696
732
|
for (const [key, upstream] of all) {
|
|
@@ -698,8 +734,8 @@ class Upstreams {
|
|
|
698
734
|
if (!handoff?.port || handoff.port.device !== device)
|
|
699
735
|
continue;
|
|
700
736
|
result[handoff.port.port] = {
|
|
701
|
-
description: nameOf(key, upstream),
|
|
702
|
-
...handoff.native !== undefined ? { trunk_vlans: [key], native_vlan:
|
|
737
|
+
description: names.port ? names.port(key, upstream) : nameOf(key, upstream),
|
|
738
|
+
...handoff.native !== undefined ? { trunk_vlans: [vlan(key, upstream)], native_vlan: native(key, upstream) } : { access_vlan: vlan(key, upstream) },
|
|
703
739
|
lldp: false,
|
|
704
740
|
stp: false,
|
|
705
741
|
...handoff.port.storm_control ? { storm_control: handoff.port.storm_control } : {}
|
|
@@ -711,27 +747,29 @@ class Upstreams {
|
|
|
711
747
|
const result = {};
|
|
712
748
|
for (const [key, upstream] of all) {
|
|
713
749
|
const description = nameOf(key, upstream);
|
|
750
|
+
const name = named("interface", key, upstream);
|
|
714
751
|
if (upstream.tunnel) {
|
|
715
752
|
const { local, remote, addresses, mtu } = upstream.tunnel;
|
|
716
|
-
result[
|
|
753
|
+
result[name] = { type: "gre", description, local, remote, addresses, ...mtu ? { mtu } : {} };
|
|
717
754
|
} else if (upstream.handoff) {
|
|
718
755
|
const { addresses, exchange } = upstream.handoff;
|
|
719
|
-
result[
|
|
756
|
+
result[name] = { type: "vlan", vlan: vlan(key, upstream), description, addresses, ...exchange ? { exchange } : {} };
|
|
720
757
|
}
|
|
721
758
|
}
|
|
722
759
|
return result;
|
|
723
760
|
};
|
|
761
|
+
const uplinks = () => all.filter(([, upstream]) => upstream.handoff).map(([key, upstream]) => named("interface", key, upstream));
|
|
724
762
|
const settings = (key, upstream) => ({
|
|
725
763
|
remote_as: upstream.asn,
|
|
726
764
|
description: nameOf(key, upstream),
|
|
727
765
|
role: upstream.role,
|
|
728
|
-
...upstream.import ? { import:
|
|
729
|
-
export:
|
|
766
|
+
...upstream.import ? { import: policy("import", key, upstream) } : {},
|
|
767
|
+
export: policy("export", key, upstream)
|
|
730
768
|
});
|
|
731
769
|
const groups = () => Object.fromEntries(all.filter(([, upstream]) => grouped(upstream)).map(([key, upstream]) => {
|
|
732
770
|
const multihop = upstream.multihop;
|
|
733
771
|
return [
|
|
734
|
-
key,
|
|
772
|
+
named("group", key, upstream),
|
|
735
773
|
{
|
|
736
774
|
...settings(key, upstream),
|
|
737
775
|
...multihop ? {
|
|
@@ -749,7 +787,7 @@ class Upstreams {
|
|
|
749
787
|
name,
|
|
750
788
|
{
|
|
751
789
|
...session,
|
|
752
|
-
...grouped(upstream) ? { group: key } : settings(key, upstream),
|
|
790
|
+
...grouped(upstream) ? { group: named("group", key, upstream) } : settings(key, upstream),
|
|
753
791
|
...limit ? { max_prefixes: limit } : {}
|
|
754
792
|
}
|
|
755
793
|
];
|
|
@@ -757,7 +795,7 @@ class Upstreams {
|
|
|
757
795
|
const policies = (options = {}) => {
|
|
758
796
|
const result = {};
|
|
759
797
|
for (const [key, upstream] of all) {
|
|
760
|
-
result[
|
|
798
|
+
result[policy("export", key, upstream)] = [
|
|
761
799
|
...options.actions?.(key, upstream) ?? [],
|
|
762
800
|
...upstream.export ?? [],
|
|
763
801
|
...Bgp.announce(upstream.announce)
|
|
@@ -766,7 +804,7 @@ class Upstreams {
|
|
|
766
804
|
if (!taken)
|
|
767
805
|
continue;
|
|
768
806
|
const tag = options.tag?.(key, upstream);
|
|
769
|
-
result[
|
|
807
|
+
result[policy("import", key, upstream)] = [
|
|
770
808
|
...taken.default ? Bgp.defaultOnly({ ...taken.default, ...options.sources ? { sources: options.sources } : {} }) : [],
|
|
771
809
|
...taken.learn ? Bgp.learn({
|
|
772
810
|
...taken.learn.before ? { before: taken.learn.before } : {},
|
|
@@ -779,9 +817,12 @@ class Upstreams {
|
|
|
779
817
|
}
|
|
780
818
|
return result;
|
|
781
819
|
};
|
|
782
|
-
const untrusted = () => [
|
|
783
|
-
|
|
784
|
-
|
|
820
|
+
const untrusted = () => [
|
|
821
|
+
...all.filter(([, upstream]) => upstream.handoff).map(([key, upstream]) => vlan(key, upstream)),
|
|
822
|
+
...natives()
|
|
823
|
+
];
|
|
824
|
+
const natives = () => all.filter(([, upstream]) => upstream.handoff?.native !== undefined).map(([key, upstream]) => native(key, upstream));
|
|
825
|
+
const privateDrops = () => all.filter(([, upstream]) => upstream.handoff && upstream.drop_private).map(([key, upstream]) => vlan(key, upstream));
|
|
785
826
|
const tunnelRules = () => all.flatMap(([key, upstream]) => upstream.tunnel ? [
|
|
786
827
|
{
|
|
787
828
|
description: `GRE from ${nameOf(key, upstream)}`,
|
|
@@ -798,6 +839,7 @@ class Upstreams {
|
|
|
798
839
|
vlansOn,
|
|
799
840
|
ports,
|
|
800
841
|
interfaces,
|
|
842
|
+
uplinks,
|
|
801
843
|
groups,
|
|
802
844
|
neighbors,
|
|
803
845
|
policies,
|
|
@@ -31,6 +31,12 @@ export declare class Cabling {
|
|
|
31
31
|
* Only ports where both ends run LLDP, by the config, are compared, and only on devices whose neighbors were read. A link `through` a carrier is not compared. LLDP can be off at the far end, so each is a warning.
|
|
32
32
|
*/
|
|
33
33
|
static compare(devices: readonly Device[], neighbors: ReadonlyMap<string, readonly Neighbor[]>): Finding[];
|
|
34
|
+
/**
|
|
35
|
+
* Each linked port without a description gets one from its far end: the far device and the far port as printed on it, such as `firewall-1 port6`. The far device's own file has its platform and model, so the label is never written twice. A port whose far device is not in the network keeps none.
|
|
36
|
+
*/
|
|
37
|
+
static describeLinks(devices: readonly Device[]): Device[];
|
|
38
|
+
/** A port as printed on the device, or Circuit's name for it when the model has no spelling for it. */
|
|
39
|
+
private static labelOf;
|
|
34
40
|
/** A system name is the device's name, alone or with a domain after it. */
|
|
35
41
|
private static isDevice;
|
|
36
42
|
private static deviceOf;
|
|
@@ -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;
|
|
@@ -1,32 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
/** One end of a cable: the device, and its platform and model, which name its ports. */
|
|
3
|
-
export type CableEnd<P extends PlatformName, M extends ModelName<P>> = {
|
|
4
|
-
device: string;
|
|
5
|
-
platform: P;
|
|
6
|
-
model: M;
|
|
7
|
-
};
|
|
8
|
-
/** The far end of a port, for its description: the device, the port by Circuit's name, and by the name printed on the device. */
|
|
1
|
+
/** The far end of a port, for a description of your own: the device and its port by Circuit's name. */
|
|
9
2
|
export type FarEnd = {
|
|
10
3
|
device: string;
|
|
11
4
|
port: string;
|
|
12
|
-
label: string;
|
|
13
5
|
};
|
|
14
6
|
/** What every cable of one entry shares. */
|
|
15
7
|
type Shared = {
|
|
16
8
|
/** What is between the two ends, such as a carrier's local loop. Both ends get it on their `link`. */
|
|
17
9
|
through?: string;
|
|
18
|
-
/** A port's description from its far end.
|
|
10
|
+
/** A port's description from its far end. When absent, Circuit writes the far device and the far port as printed on it, such as `firewall-1 port6`, from the far device's own file. */
|
|
19
11
|
describe?: (far: FarEnd) => string;
|
|
20
12
|
};
|
|
21
|
-
/** A port a cable builds:
|
|
13
|
+
/** A port a cable builds: the other end, the LAG it is in, and a description when `describe` gives one. */
|
|
22
14
|
type CablePort<L extends string> = {
|
|
23
|
-
description: string;
|
|
24
15
|
link: {
|
|
25
16
|
device: string;
|
|
26
17
|
port: string;
|
|
27
18
|
through?: string;
|
|
28
19
|
};
|
|
29
20
|
lag?: L;
|
|
21
|
+
description?: string;
|
|
30
22
|
};
|
|
31
23
|
/** Ports for both ends of the cables between two of our devices, each cable written once. */
|
|
32
24
|
export declare class Cables {
|
|
@@ -34,15 +26,17 @@ export declare class Cables {
|
|
|
34
26
|
* One cable, both ends. Spread each end into its device's `ports`:
|
|
35
27
|
*
|
|
36
28
|
* ```ts
|
|
37
|
-
* const uplink = Cables.link({ a: { device: "router-1",
|
|
29
|
+
* const uplink = Cables.link({ a: { device: "router-1", port: "25g-1" }, b: { device: "switch-1", port: "10g-48" } });
|
|
38
30
|
* // router-1: ports: { ...uplink.a }
|
|
39
31
|
* ```
|
|
40
32
|
*/
|
|
41
|
-
static link<const
|
|
42
|
-
a:
|
|
33
|
+
static link<const NA extends string, const NB extends string>(spec: Shared & {
|
|
34
|
+
a: {
|
|
35
|
+
device: string;
|
|
43
36
|
port: NA;
|
|
44
37
|
};
|
|
45
|
-
b:
|
|
38
|
+
b: {
|
|
39
|
+
device: string;
|
|
46
40
|
port: NB;
|
|
47
41
|
};
|
|
48
42
|
}): {
|
|
@@ -52,17 +46,19 @@ export declare class Cables {
|
|
|
52
46
|
/**
|
|
53
47
|
* The cables of a LAG, both ends, from one list read from end A: `{ "25g-1": "10g-2" }` is a cable from A's 25g-1 to B's 10g-2. Each port is a member of its end's LAG. Spread each end into its device's `ports`; the LAG itself stays on each device.
|
|
54
48
|
*/
|
|
55
|
-
static lag<const
|
|
56
|
-
a:
|
|
49
|
+
static lag<const LA extends string, const LB extends string, const C extends Readonly<Record<string, string>>>(spec: Shared & {
|
|
50
|
+
a: {
|
|
51
|
+
device: string;
|
|
57
52
|
lag: LA;
|
|
58
53
|
};
|
|
59
|
-
b:
|
|
54
|
+
b: {
|
|
55
|
+
device: string;
|
|
60
56
|
lag: LB;
|
|
61
57
|
};
|
|
62
58
|
cables: C;
|
|
63
59
|
}): {
|
|
64
60
|
a: Record<keyof C & string, CablePort<LA>>;
|
|
65
|
-
b: Record<
|
|
61
|
+
b: Record<C[keyof C], CablePort<LB>>;
|
|
66
62
|
};
|
|
67
63
|
/** A port facing `port` on the far device. */
|
|
68
64
|
private static end;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { Bgp } from "./bgp.js";
|
|
2
2
|
export type { Announced, CommunityScheme, DefaultOnlyOptions, LearnOptions, SanityLengths } from "./bgp.js";
|
|
3
3
|
export { Cables } from "./cables.js";
|
|
4
|
-
export type {
|
|
4
|
+
export type { FarEnd } from "./cables.js";
|
|
5
5
|
export { Checks } from "./checks.js";
|
|
6
6
|
export type { TrustBoundary } from "./checks.js";
|
|
7
7
|
export { Exchange } from "./exchange.js";
|
|
@@ -13,6 +13,8 @@ export type Upstream = {
|
|
|
13
13
|
vlan: number;
|
|
14
14
|
/** It sends its VLAN tagged: its port gets a native VLAN of its own with this ID, which nothing else is in. Untagged when absent. */
|
|
15
15
|
native?: number;
|
|
16
|
+
/** Its number in names with `{number}`, such as `core-ipt01-transita`. Written here, so removing another upstream renumbers nothing. */
|
|
17
|
+
number?: number;
|
|
16
18
|
/** Our addresses on it. `{ address, peer }` for a point-to-point address whose far end is outside its own prefix. */
|
|
17
19
|
addresses: readonly (InterfaceAddress | {
|
|
18
20
|
address: InterfaceAddress;
|
|
@@ -27,12 +29,13 @@ export type Upstream = {
|
|
|
27
29
|
/** An exchange's LAN, for `circuit peeringdb`. */
|
|
28
30
|
exchange?: ExchangeLan;
|
|
29
31
|
};
|
|
30
|
-
/** A GRE tunnel to it. */
|
|
32
|
+
/** A GRE tunnel to it. `number` is its number in names with `{number}`. */
|
|
31
33
|
tunnel?: {
|
|
32
34
|
local: IP;
|
|
33
35
|
remote: IP;
|
|
34
36
|
addresses: readonly InterfaceAddress[];
|
|
35
37
|
mtu?: number;
|
|
38
|
+
number?: number;
|
|
36
39
|
};
|
|
37
40
|
/** Our BGP sessions with it, by name: its address, and ours when it is not the interface's. */
|
|
38
41
|
sessions: Readonly<Record<string, {
|
|
@@ -72,6 +75,36 @@ export type Upstream = {
|
|
|
72
75
|
};
|
|
73
76
|
/** Upstreams by key. The key names everything built from it. */
|
|
74
77
|
export type UpstreamCatalog = Readonly<Record<string, Upstream>>;
|
|
78
|
+
/** A name made from an upstream: `{key}` is its key, `{KEY}` the key in capitals, `{number}` its `handoff.number` or `tunnel.number`. */
|
|
79
|
+
type Pattern = string;
|
|
80
|
+
/** A pattern for every upstream, or one per role. A role left out takes the default. */
|
|
81
|
+
type ByRole = Pattern | {
|
|
82
|
+
readonly provider?: Pattern;
|
|
83
|
+
readonly rs?: Pattern;
|
|
84
|
+
readonly peer?: Pattern;
|
|
85
|
+
};
|
|
86
|
+
/** How the names built from each upstream are made. Each one left out takes its default. */
|
|
87
|
+
export type UpstreamNames = {
|
|
88
|
+
/** The VLAN a handoff is on. `{key}` when absent. */
|
|
89
|
+
vlan?: ByRole;
|
|
90
|
+
/** The native VLAN of a handoff sent tagged. `{key}-native` when absent. */
|
|
91
|
+
native?: Pattern;
|
|
92
|
+
/** The router's interface: per role, and `tunnel` for one reached over a tunnel. `{key}` when absent. */
|
|
93
|
+
interface?: Pattern | {
|
|
94
|
+
readonly provider?: Pattern;
|
|
95
|
+
readonly rs?: Pattern;
|
|
96
|
+
readonly peer?: Pattern;
|
|
97
|
+
readonly tunnel?: Pattern;
|
|
98
|
+
};
|
|
99
|
+
/** The import policy. `{KEY}-IN` when absent. */
|
|
100
|
+
import?: Pattern;
|
|
101
|
+
/** The export policy. `{KEY}-OUT` when absent. */
|
|
102
|
+
export?: Pattern;
|
|
103
|
+
/** The BGP group of an exchange's route servers or of multihop sessions. `{key}` when absent. */
|
|
104
|
+
group?: Pattern;
|
|
105
|
+
/** The description of a handoff's switch port. Its `name` when absent. No device names it, so it may be any function. */
|
|
106
|
+
port?: (key: string, upstream: Upstream) => string;
|
|
107
|
+
};
|
|
75
108
|
/** How each upstream's policies are written: shared by every upstream of a catalog. */
|
|
76
109
|
export type UpstreamPolicies<P extends string> = {
|
|
77
110
|
/** A policy holding `Bgp.sanity()` that each import calls. The sanity rules are written in each import when absent. */
|
|
@@ -83,6 +116,17 @@ export type UpstreamPolicies<P extends string> = {
|
|
|
83
116
|
/** The source address for routes of its family, one per family. */
|
|
84
117
|
sources?: readonly IP[];
|
|
85
118
|
};
|
|
119
|
+
/** The names a device can refer to, and their patterns when the project gives none. */
|
|
120
|
+
declare const DEFAULTS: {
|
|
121
|
+
readonly vlan: "{key}";
|
|
122
|
+
readonly native: "{key}-native";
|
|
123
|
+
readonly interface: "{key}";
|
|
124
|
+
readonly import: "{KEY}-IN";
|
|
125
|
+
readonly export: "{KEY}-OUT";
|
|
126
|
+
readonly group: "{key}";
|
|
127
|
+
};
|
|
128
|
+
type Defaults = typeof DEFAULTS;
|
|
129
|
+
type Field = keyof Defaults;
|
|
86
130
|
type Key<C> = keyof C & string;
|
|
87
131
|
/** The upstreams that land on a VLAN of ours. */
|
|
88
132
|
type Landed<C> = {
|
|
@@ -118,37 +162,76 @@ type Importing<C> = {
|
|
|
118
162
|
import: object;
|
|
119
163
|
} ? K : never;
|
|
120
164
|
}[Key<C>];
|
|
121
|
-
/**
|
|
122
|
-
type
|
|
123
|
-
|
|
124
|
-
|
|
165
|
+
/** The project's pattern for a name, or the default. */
|
|
166
|
+
type PatternOf<N, F extends Field> = N extends {
|
|
167
|
+
[X in F]: infer P;
|
|
168
|
+
} ? P : Defaults[F];
|
|
169
|
+
/** The pattern an upstream takes: the only one, its tunnel's, its role's, or the default. */
|
|
170
|
+
type Chosen<P, U, F extends Field> = P extends string ? P : U extends {
|
|
171
|
+
tunnel: object;
|
|
172
|
+
} ? P extends {
|
|
173
|
+
tunnel: infer T extends string;
|
|
174
|
+
} ? T : Defaults[F] : U extends {
|
|
175
|
+
role: infer R extends string;
|
|
176
|
+
} ? P extends {
|
|
177
|
+
[X in R]: infer T extends string;
|
|
178
|
+
} ? T : Defaults[F] : Defaults[F];
|
|
179
|
+
/** Every `From` in `S` replaced. */
|
|
180
|
+
type Replace<S extends string, From extends string, To extends string> = S extends `${infer A}${From}${infer B}` ? `${A}${To}${Replace<B, From, To>}` : S;
|
|
181
|
+
/** An upstream's `handoff.number` or `tunnel.number`. */
|
|
182
|
+
type NumberOf<U> = U extends {
|
|
183
|
+
handoff: {
|
|
184
|
+
number: infer N extends number;
|
|
185
|
+
};
|
|
186
|
+
} ? N : U extends {
|
|
187
|
+
tunnel: {
|
|
188
|
+
number: infer N extends number;
|
|
189
|
+
};
|
|
190
|
+
} ? N : number;
|
|
191
|
+
/** The name built for upstream `K`: its pattern with the key, the key in capitals and its number filled in. */
|
|
192
|
+
type Named<C, N, F extends Field, K extends Key<C>> = Replace<Replace<Replace<Chosen<PatternOf<N, F>, C[K], F>, "{key}", K>, "{KEY}", Uppercase<K>>, "{number}", `${NumberOf<C[K]>}`>;
|
|
193
|
+
/** Each of these upstreams' names. */
|
|
194
|
+
type Names<C, N, F extends Field, Ks extends Key<C>> = {
|
|
195
|
+
[K in Ks]: Named<C, N, F, K>;
|
|
196
|
+
}[Ks];
|
|
125
197
|
/** The catalog as written, with any key the shape does not have refused, at every level. */
|
|
126
|
-
type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret ? T : {
|
|
198
|
+
type Exact<S, T> = T extends readonly unknown[] | string | number | boolean | Secret | ((...args: never[]) => unknown) ? T : S extends unknown ? {
|
|
127
199
|
[K in keyof T]: K extends keyof S ? Exact<NonNullable<S[K]>, T[K]> : never;
|
|
200
|
+
} : never;
|
|
201
|
+
/** The names as written, with a name or a role Upstreams does not make refused. */
|
|
202
|
+
type ExactNames<N> = {
|
|
203
|
+
[F in keyof N]: F extends keyof UpstreamNames ? N[F] extends string | ((...args: never[]) => unknown) ? N[F] : {
|
|
204
|
+
[R in keyof N[F]]: R extends "provider" | "rs" | "peer" | "tunnel" ? N[F][R] : never;
|
|
205
|
+
} : never;
|
|
128
206
|
};
|
|
129
207
|
/** A network's upstreams written once, and each device's part built from them. */
|
|
130
208
|
export declare class Upstreams {
|
|
131
209
|
/**
|
|
132
|
-
* Each upstream written once: its AS, where it lands, its sessions, limits and policy. Everything a device needs is built from it, under names made from each key, so a device that names one is checked:
|
|
210
|
+
* Each upstream written once: its AS, where it lands, its sessions, limits and policy. Everything a device needs is built from it, under names made from each key, so a device that names one is checked. By default:
|
|
133
211
|
*
|
|
134
212
|
* - VLAN `<key>`, and `<key>-native` for one that sends its VLAN tagged. The router's interface is `<key>` too, a VLAN interface or a GRE tunnel.
|
|
135
213
|
* - Policies `<KEY>-IN` and `<KEY>-OUT`; a BGP group `<key>` for an exchange's route servers or multihop sessions.
|
|
136
214
|
*
|
|
137
|
-
*
|
|
215
|
+
* `names` gives patterns of your own, such as `{ interface: { provider: "core-ipt0{number}-{key}" } }`; see `UpstreamNames`.
|
|
216
|
+
*
|
|
217
|
+
* On the router: `vlans()`, `interfaces()`, `uplinks()`, `groups()`, `neighbors()`, `policies({ ... })`, `tunnelRules()`, `addresses()`. On a switch: `vlans(device)`, `ports(device)`, `vlansOn(device)`. For checks and ACLs: `untrusted()`, `natives()`, `privateDrops()`. Lists follow the catalog's order, where order counts on a device.
|
|
138
218
|
*/
|
|
139
|
-
static define<const C extends UpstreamCatalog>(catalog: C & {
|
|
219
|
+
static define<const C extends UpstreamCatalog, const N extends UpstreamNames = {}>(catalog: C & {
|
|
140
220
|
[K in keyof C]: Exact<Upstream, C[K]>;
|
|
221
|
+
}, options?: {
|
|
222
|
+
names?: N & ExactNames<N>;
|
|
141
223
|
}): {
|
|
142
|
-
vlans: <const D extends string | undefined = undefined>(device?: D) => D extends string ? { [K in Landed<C
|
|
143
|
-
vlansOn: (device: string) => Landed<C
|
|
144
|
-
ports: (device: string) => Record<string, Port
|
|
145
|
-
interfaces: () => { [K in Landed<C
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
224
|
+
vlans: <const D extends string | undefined = undefined>(device?: D) => D extends string ? { [K in Names<C, N, "vlan", Landed<C>>]: Vlan; } & { [K in Names<C, N, "native", Tagged<C>>]: Vlan; } : { [K in Names<C, N, "vlan", Landed<C>>]: Vlan; };
|
|
225
|
+
vlansOn: (device: string) => Names<C, N, "vlan", Landed<C>>[];
|
|
226
|
+
ports: (device: string) => Record<string, Port<Names<C, N, "native", Tagged<C>> | Names<C, N, "vlan", Landed<C>>, never, never, never>>;
|
|
227
|
+
interfaces: () => { [K in Names<C, N, "interface", Landed<C>>]: VlanInterface<Names<C, N, "vlan", Landed<C>>, never>; } & { [K in Names<C, N, "interface", Tunneled<C>>]: Gre<never>; };
|
|
228
|
+
uplinks: () => Names<C, N, "interface", Landed<C>>[];
|
|
229
|
+
groups: () => Record<Names<C, N, "group", Grouped<C>>, NeighborSettings<Names<C, N, "export", Key<C>> | Names<C, N, "import", Importing<C>>>>;
|
|
230
|
+
neighbors: () => Record<string, Neighbor<Names<C, N, "export", Key<C>> | Names<C, N, "import", Importing<C>>, Names<C, N, "group", Grouped<C>>>>;
|
|
231
|
+
policies: <const P extends string = never>(options?: UpstreamPolicies<P>) => Record<Names<C, N, "export", Key<C>> | Names<C, N, "import", Importing<C>>, PolicyRule<P, never>[]>;
|
|
232
|
+
untrusted: () => (Names<C, N, "native", Tagged<C>> | Names<C, N, "vlan", Landed<C>>)[];
|
|
233
|
+
natives: () => Names<C, N, "native", Tagged<C>>[];
|
|
234
|
+
privateDrops: () => Names<C, N, "vlan", Landed<C>>[];
|
|
152
235
|
tunnelRules: () => FilterRule<never, never>[];
|
|
153
236
|
addresses: () => IP[];
|
|
154
237
|
};
|
|
@@ -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
|
*
|