@takodotid/circuit 0.43.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -37
- package/package.json +21 -58
- package/src/checks/across.ts +402 -0
- package/src/checks/built-in.ts +256 -0
- package/src/checks/device.ts +436 -0
- package/src/checks/index.ts +53 -0
- package/src/cli/advisories.ts +108 -0
- package/src/cli/app.ts +71 -0
- package/src/cli/editor-types.generated.ts +30 -0
- package/src/cli/editor-types.ts +69 -0
- package/src/cli/format.ts +124 -0
- package/src/cli/git-hooks.ts +79 -0
- package/src/cli/index.ts +350 -1170
- package/src/cli/migrate.ts +554 -0
- package/src/cli/project-commands.ts +656 -0
- package/src/cli/releases.ts +374 -0
- package/src/cli/remember.ts +339 -0
- package/src/cli/rename.ts +27 -33
- package/src/cli/secrets-password.ts +60 -0
- package/src/cli/secrets.ts +258 -0
- package/src/cli/setup.ts +300 -0
- package/src/cli/signing.ts +23 -0
- package/src/cli/text-editor.ts +268 -0
- package/src/cli/ui.ts +298 -0
- package/src/cli/version.ts +2 -0
- package/src/cli/versions.ts +158 -0
- package/src/config/address.ts +102 -0
- package/src/config/circuit.d.ts +30 -0
- package/src/config/common.ts +65 -0
- package/src/config/communities.ts +479 -0
- package/src/config/define.ts +684 -0
- package/src/config/device.ts +164 -0
- package/src/config/firewall.ts +150 -0
- package/src/config/index.ts +22 -0
- package/src/{schema → config}/interfaces.ts +121 -85
- package/src/config/keys.ts +434 -0
- package/src/config/management.ts +61 -0
- package/src/config/merge.ts +335 -0
- package/src/config/models.ts +70 -0
- package/src/config/monitoring.ts +64 -0
- package/src/config/network.ts +180 -0
- package/src/config/presets.ts +149 -0
- package/src/config/routing.ts +401 -0
- package/src/config/services.ts +51 -0
- package/src/config/switching.ts +84 -0
- package/src/config/system.ts +103 -0
- package/src/{schema → config}/time-zones.ts +1 -1
- package/src/core/address.ts +1 -1
- package/src/core/cabling.ts +8 -43
- package/src/core/graph.ts +40 -0
- package/src/{integrations → core}/registries.ts +5 -3
- package/src/core/secrets-file.ts +539 -0
- package/src/core/secrets.ts +83 -64
- package/src/core/topology.ts +103 -146
- package/src/core/utils.ts +12 -1
- package/src/model.ts +83 -0
- package/src/platforms/derive.ts +64 -37
- package/src/platforms/finding.ts +9 -0
- package/src/platforms/fortios/firewall.ts +76 -64
- package/src/platforms/fortios/index.ts +189 -125
- package/src/platforms/fortios/names.ts +51 -18
- package/src/platforms/fortios/render.ts +69 -62
- package/src/platforms/fortios/routing.ts +89 -58
- package/src/platforms/fortios/vdoms.ts +47 -16
- package/src/platforms/junos/TRAPS.md +2 -2
- package/src/platforms/junos/bgp.ts +63 -42
- package/src/platforms/junos/index.ts +122 -89
- package/src/platforms/junos/render.ts +139 -104
- package/src/platforms/platform.ts +10 -8
- package/src/platforms/raisecom-ros/TRAPS.md +1 -1
- package/src/platforms/raisecom-ros/index.ts +89 -70
- package/src/platforms/raisecom-ros/render.ts +49 -45
- package/src/platforms/routeros/TRAPS.md +3 -3
- package/src/platforms/routeros/index.ts +121 -60
- package/src/platforms/routeros/render.ts +214 -195
- package/src/platforms/vrp/TRAPS.md +1 -1
- package/src/platforms/vrp/index.ts +74 -50
- package/src/platforms/vrp/render.ts +67 -56
- package/src/project/project.ts +725 -0
- package/src/studio/app/env.d.ts +9 -0
- package/src/studio/app/index.html +18 -0
- package/src/studio/app/src/App.vue +95 -0
- package/src/studio/app/src/components/AddressesView.vue +38 -0
- package/src/studio/app/src/components/CablesView.vue +29 -0
- package/src/studio/app/src/components/CompareView.vue +160 -0
- package/src/studio/app/src/components/DataTable.vue +49 -0
- package/src/studio/app/src/components/DeviceView.vue +237 -0
- package/src/studio/app/src/components/DevicesView.vue +32 -0
- package/src/studio/app/src/components/Faceplate.vue +75 -0
- package/src/studio/app/src/components/GraphView.vue +282 -0
- package/src/studio/app/src/components/LineDiff.vue +24 -0
- package/src/studio/app/src/components/ProblemsView.vue +42 -0
- package/src/studio/app/src/components/SearchView.vue +72 -0
- package/src/studio/app/src/components/SecretsView.vue +112 -0
- package/src/studio/app/src/components/SessionsView.vue +35 -0
- package/src/studio/app/src/components/Settings.vue +95 -0
- package/src/studio/app/src/components/ToolsView.vue +118 -0
- package/src/studio/app/src/components/VlansView.vue +50 -0
- package/src/studio/app/src/lib/csv.ts +19 -0
- package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
- package/src/studio/app/src/lib/format.ts +38 -0
- package/src/studio/app/src/lib/net.ts +68 -0
- package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
- package/src/studio/app/src/lib/store.ts +71 -0
- package/src/studio/app/src/lib/tools.ts +217 -0
- package/src/studio/app/src/main.ts +9 -0
- package/src/studio/{public → app/src}/style.css +14 -4
- package/src/studio/app/tsconfig.json +17 -0
- package/src/studio/app/vite.config.ts +16 -0
- package/src/studio/assets.generated.ts +3 -0
- package/src/studio/data.ts +96 -188
- package/src/studio/git.ts +27 -43
- package/src/studio/server.ts +196 -107
- package/src/studio/types.ts +141 -0
- package/src/transport/console.ts +3 -3
- package/src/transport/ssh.ts +1 -1
- package/dist/chunks/index-3b343xva.js +0 -177
- package/dist/chunks/index-6wb41q67.js +0 -62
- package/dist/chunks/index-n7w3tg5y.js +0 -149
- package/dist/chunks/index-yzw86rc8.js +0 -921
- package/dist/chunks/new-w3zys5y4.js +0 -172
- package/dist/chunks/server-cxr9ynxc.js +0 -259
- package/dist/cli/index.js +0 -12922
- package/dist/helpers/index.js +0 -881
- package/dist/index.js +0 -28
- package/dist/types/core/address-space.d.ts +0 -62
- package/dist/types/core/address.d.ts +0 -44
- package/dist/types/core/cabling.d.ts +0 -41
- package/dist/types/core/cross-checks.d.ts +0 -28
- package/dist/types/core/define.d.ts +0 -79
- package/dist/types/core/project-files.d.ts +0 -12
- package/dist/types/core/schema-docs.d.ts +0 -25
- package/dist/types/core/secrets.d.ts +0 -62
- package/dist/types/core/topology.d.ts +0 -96
- package/dist/types/core/utils.d.ts +0 -10
- package/dist/types/core/validator.d.ts +0 -23
- package/dist/types/helpers/bgp.d.ts +0 -195
- package/dist/types/helpers/cables.d.ts +0 -69
- package/dist/types/helpers/checks.d.ts +0 -36
- package/dist/types/helpers/exchange.d.ts +0 -55
- package/dist/types/helpers/filters.d.ts +0 -51
- package/dist/types/helpers/index.d.ts +0 -14
- package/dist/types/helpers/route-server.d.ts +0 -52
- package/dist/types/helpers/upstreams.d.ts +0 -192
- package/dist/types/index.d.ts +0 -10
- package/dist/types/integrations/librenms.d.ts +0 -60
- package/dist/types/integrations/netbox.d.ts +0 -89
- package/dist/types/integrations/peeringdb.d.ts +0 -24
- package/dist/types/integrations/prometheus.d.ts +0 -24
- package/dist/types/integrations/registries.d.ts +0 -8
- package/dist/types/platforms/bgp-wait.d.ts +0 -24
- package/dist/types/platforms/catalog.d.ts +0 -109
- package/dist/types/platforms/derive.d.ts +0 -17
- package/dist/types/platforms/fortios/firewall.d.ts +0 -47
- package/dist/types/platforms/fortios/index.d.ts +0 -140
- package/dist/types/platforms/fortios/models.d.ts +0 -14
- package/dist/types/platforms/fortios/names.d.ts +0 -52
- package/dist/types/platforms/fortios/plan.d.ts +0 -114
- package/dist/types/platforms/fortios/render.d.ts +0 -77
- package/dist/types/platforms/fortios/routing.d.ts +0 -56
- package/dist/types/platforms/fortios/tree.d.ts +0 -37
- package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
- package/dist/types/platforms/index.d.ts +0 -13
- package/dist/types/platforms/junos/bgp.d.ts +0 -44
- package/dist/types/platforms/junos/index.d.ts +0 -114
- package/dist/types/platforms/junos/models.d.ts +0 -14
- package/dist/types/platforms/junos/render.d.ts +0 -154
- package/dist/types/platforms/line-diff.d.ts +0 -67
- package/dist/types/platforms/platform.d.ts +0 -135
- package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
- package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
- package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
- package/dist/types/platforms/routeros/export.d.ts +0 -59
- package/dist/types/platforms/routeros/index.d.ts +0 -116
- package/dist/types/platforms/routeros/models.d.ts +0 -18
- package/dist/types/platforms/routeros/plan.d.ts +0 -58
- package/dist/types/platforms/routeros/render.d.ts +0 -250
- package/dist/types/platforms/vrp/index.d.ts +0 -83
- package/dist/types/platforms/vrp/models.d.ts +0 -12
- package/dist/types/platforms/vrp/render.d.ts +0 -96
- package/dist/types/schema/common.d.ts +0 -30
- package/dist/types/schema/device.d.ts +0 -126
- package/dist/types/schema/firewall.d.ts +0 -131
- package/dist/types/schema/index.d.ts +0 -8
- package/dist/types/schema/interfaces.d.ts +0 -205
- package/dist/types/schema/policy.d.ts +0 -107
- package/dist/types/schema/routing.d.ts +0 -161
- package/dist/types/schema/system.d.ts +0 -198
- package/dist/types/schema/time-zones.d.ts +0 -2
- package/dist/types/transport/console.d.ts +0 -115
- package/dist/types/transport/path-check.d.ts +0 -32
- package/dist/types/transport/ssh.d.ts +0 -27
- package/src/cli/graph-format.ts +0 -91
- package/src/cli/new.ts +0 -234
- package/src/core/address-space.ts +0 -320
- package/src/core/cross-checks.ts +0 -369
- package/src/core/define.ts +0 -124
- package/src/core/project-files.ts +0 -35
- package/src/core/schema-docs.ts +0 -174
- package/src/core/validator.ts +0 -508
- package/src/helpers/bgp.ts +0 -473
- package/src/helpers/cables.ts +0 -74
- package/src/helpers/checks.ts +0 -201
- package/src/helpers/exchange.ts +0 -123
- package/src/helpers/filters.ts +0 -107
- package/src/helpers/index.ts +0 -25
- package/src/helpers/route-server.ts +0 -212
- package/src/helpers/upstreams.ts +0 -369
- package/src/index.ts +0 -13
- package/src/integrations/librenms.ts +0 -291
- package/src/integrations/netbox.ts +0 -620
- package/src/integrations/peeringdb.ts +0 -173
- package/src/integrations/prometheus.ts +0 -90
- package/src/schema/common.ts +0 -41
- package/src/schema/device.ts +0 -145
- package/src/schema/firewall.ts +0 -140
- package/src/schema/index.ts +0 -8
- package/src/schema/policy.ts +0 -115
- package/src/schema/routing.ts +0 -173
- package/src/schema/system.ts +0 -203
- package/src/studio/public/app.js +0 -186
- package/src/studio/public/compare.js +0 -250
- package/src/studio/public/dom.js +0 -138
- package/src/studio/public/evaluate.js +0 -687
- package/src/studio/public/export.js +0 -130
- package/src/studio/public/graph.js +0 -1541
- package/src/studio/public/index.html +0 -83
- package/src/studio/public/net.js +0 -134
- package/src/studio/public/tables.js +0 -1289
- package/src/studio/public/tools.js +0 -610
- package/templates/colocation/checks.ts +0 -53
- package/templates/colocation/circuit.config.ts +0 -9
- package/templates/colocation/edge-01.ts +0 -133
- package/templates/colocation/routing.ts +0 -13
- package/templates/colocation/tenancy.ts +0 -76
- package/templates/colocation/tenants/acme.ts +0 -19
- package/templates/colocation/tenants/index.ts +0 -6
- package/templates/colocation/tor-01.ts +0 -33
- package/templates/edge-router/checks.ts +0 -20
- package/templates/edge-router/circuit.config.ts +0 -8
- package/templates/edge-router/edge-01.ts +0 -110
- package/templates/edge-router/routing.ts +0 -13
- package/templates/project/AGENTS.md +0 -51
- package/templates/project/CLAUDE.md +0 -1
- package/templates/project/README.md +0 -16
- package/templates/project/gitignore +0 -5
- package/templates/project/tsconfig.json +0 -13
- package/templates/single-site/checks.ts +0 -26
- package/templates/single-site/circuit.config.ts +0 -8
- package/templates/single-site/router.ts +0 -86
- package/templates/single-site/site.ts +0 -18
- package/templates/single-site/switch.ts +0 -34
- /package/src/studio/{public → app/public}/favicon.svg +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
package/src/platforms/derive.ts
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { Address } from "../core/address";
|
|
4
4
|
import { Utils } from "../core/utils";
|
|
5
|
-
import type { Device } from "../
|
|
5
|
+
import type { Device, Interface, InterfaceKind, Port } from "../model";
|
|
6
|
+
|
|
7
|
+
/** One interface of a device, with the group it is in. */
|
|
8
|
+
export type NamedInterface = { name: string; kind: InterfaceKind; settings: Interface };
|
|
9
|
+
|
|
10
|
+
/** The groups of `interfaces` besides `ethernet`, in the order Circuit reads them. */
|
|
11
|
+
const KINDS: readonly InterfaceKind[] = ["lag", "vlan_interface", "bridge", "loopback", "gre", "vxlan", "wireguard"];
|
|
6
12
|
|
|
7
13
|
/** A feature a platform renders once proved on one of its devices. */
|
|
8
14
|
export type ProvedFeature =
|
|
@@ -16,16 +22,35 @@ export type ProvedFeature =
|
|
|
16
22
|
| "routing-tables";
|
|
17
23
|
|
|
18
24
|
export class Derive {
|
|
25
|
+
/** Every interface that is not a port, group by group (`KINDS`), each in the order it is written. */
|
|
26
|
+
static interfaces(device: Device): NamedInterface[] {
|
|
27
|
+
return KINDS.flatMap((kind) =>
|
|
28
|
+
Object.entries<Interface | undefined>(device.interfaces?.[kind] ?? {}).flatMap(([name, settings]) =>
|
|
29
|
+
settings ? [{ name, kind, settings }] : []
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Every port the device writes, in the order it is written. */
|
|
35
|
+
static ports(device: Device): [string, Port][] {
|
|
36
|
+
return Object.entries<Port | undefined>(device.interfaces?.ethernet ?? {}).flatMap(([name, port]) =>
|
|
37
|
+
port ? [[name, port] as [string, Port]] : []
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A port or an interface by its name, whatever group it is in. */
|
|
42
|
+
static settings(device: Device, name: string): Port | Interface | undefined {
|
|
43
|
+
return device.interfaces?.ethernet?.[name] ?? Derive.interfaces(device).find((entry) => entry.name === name)?.settings;
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
/** Each LAG's number: its own `id`, or its position among the device's LAGs. */
|
|
20
47
|
static lagIds(device: Device): Map<string, number> {
|
|
21
48
|
const numbers = new Map<string, number>();
|
|
22
49
|
let position = 0;
|
|
23
50
|
|
|
24
|
-
for (const [name,
|
|
25
|
-
if (iface.type !== "lag") continue;
|
|
26
|
-
|
|
51
|
+
for (const [name, lag] of Object.entries(device.interfaces?.lag ?? {})) {
|
|
27
52
|
position++;
|
|
28
|
-
numbers.set(name,
|
|
53
|
+
numbers.set(name, lag?.id ?? position);
|
|
29
54
|
}
|
|
30
55
|
|
|
31
56
|
return numbers;
|
|
@@ -33,8 +58,9 @@ export class Derive {
|
|
|
33
58
|
|
|
34
59
|
/** The IPv4 networks of a port's or an interface's addresses. */
|
|
35
60
|
static ipv4Networks(device: Device, iface: string): string[] {
|
|
36
|
-
const
|
|
37
|
-
|
|
61
|
+
const addresses = (Derive.settings(device, iface)?.addresses ?? []).map((entry) =>
|
|
62
|
+
typeof entry === "string" ? entry : entry.address
|
|
63
|
+
);
|
|
38
64
|
|
|
39
65
|
return addresses.filter((address) => Address.family(address) === "ipv4").map((address) => Address.prefixOf(address));
|
|
40
66
|
}
|
|
@@ -50,32 +76,33 @@ export class Derive {
|
|
|
50
76
|
const has = (feature: ProvedFeature) => proved.includes(feature);
|
|
51
77
|
|
|
52
78
|
if (routing?.bfd && !has("bfd-timers")) found.push(`routing.bfd, BFD timers are ${unproved}`);
|
|
53
|
-
if (device.vdoms && !has("management-vdom")) found.push(`vdoms, a management VDOM is ${unproved}`);
|
|
79
|
+
if (device.system?.vdoms && !has("management-vdom")) found.push(`system.vdoms, a management VDOM is ${unproved}`);
|
|
54
80
|
|
|
55
|
-
for (const route of routing?.
|
|
81
|
+
for (const route of routing?.static_routes ?? []) {
|
|
56
82
|
if (route.bfd && !has("static-route-bfd"))
|
|
57
|
-
found.push(`routing.
|
|
58
|
-
if (Utils.otherTable(route.
|
|
59
|
-
found.push(`routing.
|
|
83
|
+
found.push(`routing.static_routes ${route.prefix} bfd, BFD on a static route is ${unproved}`);
|
|
84
|
+
if (Utils.otherTable(route.routing_table) && !has("routing-tables"))
|
|
85
|
+
found.push(`routing.static_routes ${route.prefix} routing_table, a routing table beside the main one is ${unproved}`);
|
|
60
86
|
}
|
|
61
87
|
|
|
62
88
|
if (!has("routing-tables")) {
|
|
63
89
|
if (routing?.tables) found.push(`routing.tables, a routing table beside the main one is ${unproved}`);
|
|
64
|
-
if (routing?.
|
|
90
|
+
if (routing?.policy_routing) found.push(`routing.policy_routing, policy routing is ${unproved}`);
|
|
65
91
|
}
|
|
66
92
|
|
|
67
93
|
const bgp = routing?.bgp;
|
|
68
94
|
|
|
69
|
-
for (const [name, group] of Object.entries(bgp?.
|
|
70
|
-
if (Utils.otherTable(group
|
|
71
|
-
found.push(`routing.bgp.
|
|
95
|
+
for (const [name, group] of Object.entries(bgp?.neighbor_groups ?? {})) {
|
|
96
|
+
if (Utils.otherTable(group?.routing_table) && !has("routing-tables"))
|
|
97
|
+
found.push(`routing.bgp.neighbor_groups.${name}.routing_table, a routing table beside the main one is ${unproved}`);
|
|
72
98
|
}
|
|
73
99
|
|
|
74
100
|
for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
|
|
75
|
-
if (
|
|
76
|
-
|
|
101
|
+
if (!neighbor) continue;
|
|
102
|
+
if (Utils.otherTable(neighbor.routing_table) && !has("routing-tables"))
|
|
103
|
+
found.push(`routing.bgp.neighbors.${name}.routing_table, a routing table beside the main one is ${unproved}`);
|
|
77
104
|
|
|
78
|
-
const group = neighbor.
|
|
105
|
+
const group = neighbor.neighbor_group ? bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
|
|
79
106
|
const bfd = neighbor.bfd ?? group?.bfd;
|
|
80
107
|
const multihop = neighbor.multihop ?? group?.multihop;
|
|
81
108
|
|
|
@@ -89,12 +116,9 @@ export class Derive {
|
|
|
89
116
|
}
|
|
90
117
|
}
|
|
91
118
|
|
|
92
|
-
const holders = [
|
|
93
|
-
...
|
|
94
|
-
...Object.entries(device.interfaces ?? {}).map(([name, settings]) => ({
|
|
95
|
-
where: `interfaces.${name}`,
|
|
96
|
-
settings: settings.type === "lag" ? settings : undefined,
|
|
97
|
-
})),
|
|
119
|
+
const holders: { where: string; settings: { mac_limit?: number; ethertypes?: readonly string[] } | undefined }[] = [
|
|
120
|
+
...Derive.ports(device).map(([name, settings]) => ({ where: `interfaces.ethernet.${name}`, settings })),
|
|
121
|
+
...Object.entries(device.interfaces?.lag ?? {}).map(([name, settings]) => ({ where: `interfaces.lag.${name}`, settings })),
|
|
98
122
|
];
|
|
99
123
|
|
|
100
124
|
for (const { where, settings } of holders) {
|
|
@@ -115,26 +139,29 @@ export class Derive {
|
|
|
115
139
|
const found: string[] = [];
|
|
116
140
|
|
|
117
141
|
for (const service of Derive.SWITCH_SERVICES) {
|
|
118
|
-
|
|
142
|
+
// SNMP is in `monitoring`, the other services in `management`.
|
|
143
|
+
const group = service === "snmp" ? "monitoring" : "management";
|
|
144
|
+
const settings = service === "snmp" ? device.monitoring?.snmp : device.management?.[service];
|
|
119
145
|
if (settings?.allow && !ownSources.includes(service))
|
|
120
|
-
found.push(
|
|
121
|
-
if (settings?.port) found.push(
|
|
146
|
+
found.push(`${group}.${service}.allow, every service takes management.allow`);
|
|
147
|
+
if (settings?.port) found.push(`${group}.${service}.port`);
|
|
122
148
|
}
|
|
123
149
|
|
|
124
|
-
for (const [name, user] of Object.entries(device.users ?? {})) {
|
|
125
|
-
if (user
|
|
126
|
-
if (user
|
|
150
|
+
for (const [name, user] of Object.entries(device.system?.users ?? {})) {
|
|
151
|
+
if (user?.description) found.push(`system.users.${name}.description`);
|
|
152
|
+
if (user?.ssh_keys?.length) found.push(`system.users.${name}.ssh_keys`);
|
|
127
153
|
}
|
|
128
154
|
|
|
129
|
-
if (device.system?.
|
|
155
|
+
if (device.system?.ip_stack) found.push("system.ip_stack");
|
|
130
156
|
|
|
131
|
-
for (const [name, port] of
|
|
132
|
-
if (port
|
|
157
|
+
for (const [name, port] of Derive.ports(device)) {
|
|
158
|
+
if (port.hardware_offload !== undefined) found.push(`interfaces.ethernet.${name}.hardware_offload`);
|
|
133
159
|
}
|
|
134
160
|
|
|
135
|
-
for (const
|
|
136
|
-
if (
|
|
137
|
-
|
|
161
|
+
for (const { name, kind, settings } of Derive.interfaces(device)) {
|
|
162
|
+
if (settings.addresses?.some((entry) => typeof entry !== "string"))
|
|
163
|
+
found.push(`interfaces.${kind}.${name}.addresses with a peer`);
|
|
164
|
+
if (settings.hardware_offload !== undefined) found.push(`interfaces.${kind}.${name}.hardware_offload`);
|
|
138
165
|
}
|
|
139
166
|
|
|
140
167
|
return found;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Something wrong with a config, as `circuit validate` reports it. */
|
|
2
|
+
export type Finding = {
|
|
3
|
+
/** An error stops `diff` and `apply`; a warning is reported only. */
|
|
4
|
+
level: "error" | "warning";
|
|
5
|
+
/** The device it is about, when it is about one. */
|
|
6
|
+
device?: string;
|
|
7
|
+
/** What is wrong, in a sentence. */
|
|
8
|
+
message: string;
|
|
9
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Packet filtering as policies through the device and local-in policies to it, who reaches the device's own services, NAT as central source NAT and virtual IPs, and stateless ACLs on ports.
|
|
2
2
|
|
|
3
3
|
import { Address } from "../../core/address";
|
|
4
|
-
import type { AclRule, Device, Family, FilterMatch, FilterRule, NatRule, Ports, Prefix, Protocol } from "../../
|
|
4
|
+
import type { AclRule, Device, Family, FilterMatch, FilterRule, NatRule, Ports, Prefix, Protocol, Service } from "../../model";
|
|
5
|
+
import { Derive } from "../derive";
|
|
5
6
|
import { FortiOsNames } from "./names";
|
|
7
|
+
|
|
6
8
|
import { FortiOsBlock, FortiOsTree } from "./tree";
|
|
7
9
|
|
|
8
10
|
/** The address objects, groups and services rules name, each created once. */
|
|
@@ -48,7 +50,7 @@ class Objects {
|
|
|
48
50
|
this.root.block("firewall ippool").entry(address).set("startip", address).set("endip", address);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
service(match: FilterMatch
|
|
53
|
+
service(match: FilterMatch | undefined, family: Family): string {
|
|
52
54
|
const { name, block } = FortiOsFirewall.serviceOf(match, family);
|
|
53
55
|
|
|
54
56
|
// Transparent mode deletes the factory's services, `ALL` with them, so every protocol is a service of Circuit's own there.
|
|
@@ -69,7 +71,7 @@ export class FortiOsFirewall {
|
|
|
69
71
|
/** IP protocol numbers, for a protocol the platform names only by number. */
|
|
70
72
|
static readonly PROTOCOL_NUMBER: Record<Protocol, number> = { tcp: 6, udp: 17, icmp: 1, gre: 47, esp: 50, ah: 51, ospf: 89, vrrp: 112 };
|
|
71
73
|
|
|
72
|
-
/** The device's own services, by the
|
|
74
|
+
/** The device's own services, by the name their guards take, with the protocol and port each listens on unless the config moves it. */
|
|
73
75
|
private static readonly SERVICES = {
|
|
74
76
|
ssh: { protocol: "tcp", port: 22 },
|
|
75
77
|
https: { protocol: "tcp", port: 443 },
|
|
@@ -79,25 +81,34 @@ export class FortiOsFirewall {
|
|
|
79
81
|
native: { protocol: "tcp", port: 541 },
|
|
80
82
|
} as const;
|
|
81
83
|
|
|
84
|
+
/** A service's settings in the config: SNMP is in `monitoring`, the native protocol is `management.native_protocol`. */
|
|
85
|
+
private static serviceSettings(device: Device, service: keyof typeof FortiOsFirewall.SERVICES): Service | undefined {
|
|
86
|
+
if (service === "snmp") return device.monitoring?.snmp;
|
|
87
|
+
if (service === "native") return device.management?.native_protocol;
|
|
88
|
+
return device.management?.[service];
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
/** Every protocol, as a service of Circuit's own, for a device in transparent mode. */
|
|
83
92
|
static readonly ALL_IN_TRANSPARENT = "circuit-all";
|
|
84
93
|
|
|
85
94
|
/** NAT rules of both directions, which central NAT needs turned on. */
|
|
86
|
-
static natRules(device: Device): NatRule
|
|
95
|
+
static natRules(device: Device): NatRule[] {
|
|
87
96
|
const nat = device.firewall?.nat;
|
|
88
97
|
return [...(nat?.source ?? []), ...(nat?.destination ?? [])];
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
/** Which families a rule applies to: its own, its addresses', or both. */
|
|
92
|
-
static familiesOf(device: Device, match: FilterMatch
|
|
101
|
+
static familiesOf(device: Device, match: FilterMatch | undefined): Family[] {
|
|
93
102
|
if (match?.family) return [match.family];
|
|
94
103
|
|
|
95
|
-
const prefixes = [match?.src, match?.dst].filter(
|
|
104
|
+
const prefixes = [FortiOsNames.single(match?.src), FortiOsNames.single(match?.dst)].filter(
|
|
105
|
+
(prefix): prefix is Prefix => prefix !== undefined
|
|
106
|
+
);
|
|
96
107
|
if (prefixes.length) return [...new Set(prefixes.map(FortiOsNames.familyOfPrefix))];
|
|
97
108
|
|
|
98
109
|
let families: Family[] = ["ipv4", "ipv6"];
|
|
99
110
|
|
|
100
|
-
for (const set of [match?.
|
|
111
|
+
for (const set of [match?.src_address_set, match?.dst_address_set]) {
|
|
101
112
|
if (!set) continue;
|
|
102
113
|
|
|
103
114
|
const present = new Set((device.firewall?.address_sets?.[set] ?? []).map(FortiOsNames.familyOfPrefix));
|
|
@@ -116,7 +127,7 @@ export class FortiOsFirewall {
|
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
/** The service a rule matches, by name, with what it holds. `ALL` when the rule names no protocol and no port; ports without a protocol are TCP's and UDP's. */
|
|
119
|
-
static serviceOf(match: FilterMatch
|
|
130
|
+
static serviceOf(match: FilterMatch | undefined, family: Family): { name: string; block?: FortiOsBlock } {
|
|
120
131
|
const protocol = match?.protocol;
|
|
121
132
|
const destinations = FortiOsFirewall.portList(match?.dst_port);
|
|
122
133
|
const sources = FortiOsFirewall.portList(match?.src_port);
|
|
@@ -145,10 +156,10 @@ export class FortiOsFirewall {
|
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
/** 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. */
|
|
148
|
-
static isImplied(rule: FilterRule
|
|
149
|
-
const state = rule.
|
|
159
|
+
static isImplied(rule: FilterRule): boolean {
|
|
160
|
+
const state = rule.if?.connection_state;
|
|
150
161
|
return (
|
|
151
|
-
rule.
|
|
162
|
+
rule.then === "accept" &&
|
|
152
163
|
state !== undefined &&
|
|
153
164
|
state.length > 0 &&
|
|
154
165
|
state.every((one) => one === "established" || one === "related")
|
|
@@ -175,17 +186,17 @@ export class FortiOsFirewall {
|
|
|
175
186
|
for (const rule of forward?.rules ?? []) {
|
|
176
187
|
if (FortiOsFirewall.isImplied(rule)) continue;
|
|
177
188
|
|
|
178
|
-
for (const family of FortiOsFirewall.familiesOf(device, rule.
|
|
189
|
+
for (const family of FortiOsFirewall.familiesOf(device, rule.if)) {
|
|
179
190
|
const table = root.block(family === "ipv4" ? "firewall policy" : "firewall policy6");
|
|
180
191
|
FortiOsFirewall.policy(table.entry(++policies[family]), objects, device, rule, family);
|
|
181
192
|
}
|
|
182
193
|
}
|
|
183
194
|
|
|
184
195
|
// A packet no rule matched is accepted unless the chain drops it; the platform's own last word is deny.
|
|
185
|
-
if (forward?.
|
|
196
|
+
if (forward?.default_action !== "drop") {
|
|
186
197
|
for (const family of ["ipv4", "ipv6"] as const) {
|
|
187
198
|
const table = root.block(family === "ipv4" ? "firewall policy" : "firewall policy6");
|
|
188
|
-
FortiOsFirewall.policy(table.entry(++policies[family]), objects, device, {
|
|
199
|
+
FortiOsFirewall.policy(table.entry(++policies[family]), objects, device, { then: "accept" }, family);
|
|
189
200
|
}
|
|
190
201
|
}
|
|
191
202
|
|
|
@@ -200,14 +211,14 @@ export class FortiOsFirewall {
|
|
|
200
211
|
for (const rule of input?.rules ?? []) {
|
|
201
212
|
if (FortiOsFirewall.isImplied(rule)) continue;
|
|
202
213
|
|
|
203
|
-
for (const family of FortiOsFirewall.familiesOf(device, rule.
|
|
214
|
+
for (const family of FortiOsFirewall.familiesOf(device, rule.if)) {
|
|
204
215
|
FortiOsFirewall.localIn(localInEntry(family), objects, device, rule, family);
|
|
205
216
|
}
|
|
206
217
|
}
|
|
207
218
|
|
|
208
|
-
if (input?.
|
|
219
|
+
if (input?.default_action === "drop") {
|
|
209
220
|
for (const family of ["ipv4", "ipv6"] as const) {
|
|
210
|
-
FortiOsFirewall.localIn(localInEntry(family), objects, device, {
|
|
221
|
+
FortiOsFirewall.localIn(localInEntry(family), objects, device, { then: "drop" }, family);
|
|
211
222
|
}
|
|
212
223
|
}
|
|
213
224
|
|
|
@@ -227,13 +238,12 @@ export class FortiOsFirewall {
|
|
|
227
238
|
private static management(entry: (family: Family) => FortiOsBlock, objects: Objects, device: Device): void {
|
|
228
239
|
type Services = typeof FortiOsFirewall.SERVICES;
|
|
229
240
|
|
|
230
|
-
const config = device.management ?? {};
|
|
231
241
|
const guards = new Map<string, { sources: readonly string[]; services: string[] }>();
|
|
232
242
|
|
|
233
243
|
for (const [service, listener] of Object.entries(FortiOsFirewall.SERVICES) as [keyof Services, Services[keyof Services]][]) {
|
|
234
|
-
const settings =
|
|
235
|
-
const sources = settings?.allow ??
|
|
236
|
-
if (!settings || !sources
|
|
244
|
+
const settings = FortiOsFirewall.serviceSettings(device, service);
|
|
245
|
+
const sources = settings?.allow ?? FortiOsNames.allowed(device);
|
|
246
|
+
if (!settings || !sources.length) continue;
|
|
237
247
|
|
|
238
248
|
const name = settings.allow ? `circuit-${service}` : "circuit-management";
|
|
239
249
|
const guard = guards.get(name) ?? { sources, services: [] };
|
|
@@ -274,52 +284,52 @@ export class FortiOsFirewall {
|
|
|
274
284
|
}
|
|
275
285
|
|
|
276
286
|
/** A policy for traffic through the device. A rejection answers the sender; the platform's deny is silent otherwise. */
|
|
277
|
-
private static policy(entry: FortiOsBlock, objects: Objects, device: Device, rule: FilterRule
|
|
278
|
-
const match = rule.
|
|
279
|
-
|
|
280
|
-
entry.set("srcintf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
|
|
281
|
-
entry.set("dstintf", FortiOsFirewall.interfaceOf(device, match?.out_interface));
|
|
282
|
-
entry.set("srcaddr", FortiOsTree.quote(objects.address(match?.src, match?.
|
|
283
|
-
entry.set("dstaddr", FortiOsTree.quote(objects.address(match?.dst, match?.
|
|
284
|
-
entry.set("action", rule.
|
|
287
|
+
private static policy(entry: FortiOsBlock, objects: Objects, device: Device, rule: FilterRule, family: Family): void {
|
|
288
|
+
const match = rule.if;
|
|
289
|
+
|
|
290
|
+
entry.set("srcintf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.in_interface)));
|
|
291
|
+
entry.set("dstintf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.out_interface)));
|
|
292
|
+
entry.set("srcaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.src), match?.src_address_set, family)));
|
|
293
|
+
entry.set("dstaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.dst), match?.dst_address_set, family)));
|
|
294
|
+
entry.set("action", rule.then === "accept" ? "accept" : "deny");
|
|
285
295
|
entry.set("schedule", FortiOsTree.quote("always"));
|
|
286
296
|
entry.set("service", FortiOsTree.quote(objects.service(match, family)));
|
|
287
297
|
|
|
288
298
|
// The device gives a deny policy `logtraffic disable` of its own accord.
|
|
289
|
-
if (rule.
|
|
290
|
-
if (rule.
|
|
299
|
+
if (rule.then !== "accept") entry.set("logtraffic", "disable");
|
|
300
|
+
if (rule.then === "accept" && !device.system?.hardware_offload && !rule.fast_path) entry.set("auto-asic-offload", "disable");
|
|
291
301
|
if (rule.description) entry.set("comments", FortiOsTree.quote(rule.description));
|
|
292
|
-
if (rule.
|
|
302
|
+
if (rule.then === "reject") entry.set("send-deny-packet", "enable");
|
|
293
303
|
}
|
|
294
304
|
|
|
295
305
|
/** A local-in policy, for traffic to the device itself. */
|
|
296
|
-
private static localIn(entry: FortiOsBlock, objects: Objects, device: Device, rule: FilterRule
|
|
297
|
-
const match = rule.
|
|
306
|
+
private static localIn(entry: FortiOsBlock, objects: Objects, device: Device, rule: FilterRule, family: Family): void {
|
|
307
|
+
const match = rule.if;
|
|
298
308
|
|
|
299
|
-
entry.set("intf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
|
|
300
|
-
entry.set("srcaddr", FortiOsTree.quote(objects.address(match?.src, match?.
|
|
301
|
-
entry.set("dstaddr", FortiOsTree.quote(objects.address(match?.dst, match?.
|
|
302
|
-
entry.set("action", rule.
|
|
309
|
+
entry.set("intf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.in_interface)));
|
|
310
|
+
entry.set("srcaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.src), match?.src_address_set, family)));
|
|
311
|
+
entry.set("dstaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.dst), match?.dst_address_set, family)));
|
|
312
|
+
entry.set("action", rule.then === "accept" ? "accept" : "deny");
|
|
303
313
|
entry.set("service", FortiOsTree.quote(objects.service(match, family)));
|
|
304
314
|
entry.set("schedule", FortiOsTree.quote("always"));
|
|
305
315
|
if (rule.description) entry.set("comments", FortiOsTree.quote(rule.description));
|
|
306
316
|
}
|
|
307
317
|
|
|
308
318
|
/** Source NAT: to the outgoing interface's address, or to a pool of one address. */
|
|
309
|
-
private static sourceNat(entry: FortiOsBlock, objects: Objects, device: Device, rule: NatRule
|
|
310
|
-
const match = rule.
|
|
319
|
+
private static sourceNat(entry: FortiOsBlock, objects: Objects, device: Device, rule: NatRule): void {
|
|
320
|
+
const match = rule.if;
|
|
311
321
|
|
|
312
|
-
entry.set("orig-addr", FortiOsTree.quote(objects.address(match?.src, match?.
|
|
313
|
-
entry.set("srcintf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
|
|
314
|
-
entry.set("dst-addr", FortiOsTree.quote(objects.address(match?.dst, match?.
|
|
315
|
-
entry.set("dstintf", FortiOsFirewall.interfaceOf(device, match?.out_interface));
|
|
322
|
+
entry.set("orig-addr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.src), match?.src_address_set, "ipv4")));
|
|
323
|
+
entry.set("srcintf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.in_interface)));
|
|
324
|
+
entry.set("dst-addr", FortiOsTree.quote(objects.address(FortiOsNames.single(match?.dst), match?.dst_address_set, "ipv4")));
|
|
325
|
+
entry.set("dstintf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.out_interface)));
|
|
316
326
|
if (match?.protocol) entry.set("protocol", FortiOsFirewall.PROTOCOL_NUMBER[match.protocol]);
|
|
317
327
|
|
|
318
328
|
const ports = FortiOsFirewall.portList(match?.src_port);
|
|
319
329
|
|
|
320
|
-
if (rule.
|
|
321
|
-
objects.pool(rule.
|
|
322
|
-
entry.set("nat-ippool", FortiOsTree.quote(rule.
|
|
330
|
+
if (rule.then === "snat") {
|
|
331
|
+
objects.pool(rule.to_address);
|
|
332
|
+
entry.set("nat-ippool", FortiOsTree.quote(rule.to_address));
|
|
323
333
|
// A pool keeps the source port only when told which.
|
|
324
334
|
if (ports.length) entry.set("orig-port", ports[0]!).set("nat-port", rule.to_port ?? ports[0]!);
|
|
325
335
|
} else if (ports.length) {
|
|
@@ -330,20 +340,22 @@ export class FortiOsFirewall {
|
|
|
330
340
|
}
|
|
331
341
|
|
|
332
342
|
/** Destination NAT, as a virtual IP: the address it answers on, and the port it forwards when the rule names one. */
|
|
333
|
-
private static destinationNat(entry: FortiOsBlock, device: Device, rule: NatRule
|
|
334
|
-
if (rule.
|
|
343
|
+
private static destinationNat(entry: FortiOsBlock, device: Device, rule: NatRule): void {
|
|
344
|
+
if (rule.then !== "dnat") return;
|
|
335
345
|
|
|
336
|
-
const match = rule.
|
|
346
|
+
const match = rule.if;
|
|
347
|
+
const dst = FortiOsNames.single(match?.dst);
|
|
348
|
+
const src = FortiOsNames.single(match?.src);
|
|
337
349
|
if (rule.description) entry.set("comment", FortiOsTree.quote(rule.description));
|
|
338
|
-
if (
|
|
339
|
-
entry.set("extintf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
|
|
350
|
+
if (dst) entry.set("extip", Address.host(FortiOsNames.withLength(dst)));
|
|
351
|
+
entry.set("extintf", FortiOsFirewall.interfaceOf(device, FortiOsNames.single(match?.in_interface)));
|
|
340
352
|
|
|
341
|
-
const sources =
|
|
353
|
+
const sources = src ? [src] : match?.src_address_set ? (device.firewall?.address_sets?.[match.src_address_set] ?? []) : [];
|
|
342
354
|
if (sources.length) entry.set("src-filter", FortiOsTree.quoteAll(sources.map(FortiOsNames.network)));
|
|
343
355
|
|
|
344
356
|
const ports = FortiOsFirewall.portList(match?.dst_port);
|
|
345
357
|
if (ports.length) entry.set("portforward", "enable");
|
|
346
|
-
entry.set("mappedip", FortiOsTree.quote(rule.
|
|
358
|
+
entry.set("mappedip", FortiOsTree.quote(rule.to_address));
|
|
347
359
|
|
|
348
360
|
if (ports.length) {
|
|
349
361
|
if (match?.protocol === "udp") entry.set("protocol", "udp");
|
|
@@ -358,20 +370,20 @@ export class FortiOsFirewall {
|
|
|
358
370
|
private static acls(root: FortiOsBlock, objects: Objects, device: Device): void {
|
|
359
371
|
const counters = { ipv4: 0, ipv6: 0 };
|
|
360
372
|
|
|
361
|
-
for (const [port, settings] of
|
|
362
|
-
if (!settings
|
|
373
|
+
for (const [port, settings] of Derive.ports(device)) {
|
|
374
|
+
if (!settings.ingress_acl) continue;
|
|
363
375
|
|
|
364
|
-
for (const rule of device.acls?.[settings.
|
|
365
|
-
if (rule.
|
|
376
|
+
for (const rule of device.switching?.acls?.[settings.ingress_acl] ?? []) {
|
|
377
|
+
if (rule.then !== "drop") continue;
|
|
366
378
|
|
|
367
|
-
const match = rule.
|
|
379
|
+
const { vlan: _, ...match } = rule.if ?? {};
|
|
368
380
|
|
|
369
381
|
for (const family of FortiOsFirewall.familiesOf(device, match)) {
|
|
370
382
|
const entry = root.block(family === "ipv4" ? "firewall acl" : "firewall acl6").entry(++counters[family]);
|
|
371
383
|
if (rule.description) entry.set("comments", FortiOsTree.quote(rule.description));
|
|
372
384
|
entry.set("interface", FortiOsTree.quote(FortiOsNames.nameOf(device, port)));
|
|
373
|
-
entry.set("srcaddr", FortiOsTree.quote(objects.address(match
|
|
374
|
-
entry.set("dstaddr", FortiOsTree.quote(objects.address(match
|
|
385
|
+
entry.set("srcaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match.src), undefined, family)));
|
|
386
|
+
entry.set("dstaddr", FortiOsTree.quote(objects.address(FortiOsNames.single(match.dst), undefined, family)));
|
|
375
387
|
entry.set("service", FortiOsTree.quote(objects.service(match, family)));
|
|
376
388
|
}
|
|
377
389
|
}
|
|
@@ -379,8 +391,8 @@ export class FortiOsFirewall {
|
|
|
379
391
|
}
|
|
380
392
|
|
|
381
393
|
/** Whether a port's ACL drops before it accepts: an accept before a drop is an exception the platform's ACLs cannot hold. */
|
|
382
|
-
static acceptsBeforeDrop(rules: readonly AclRule
|
|
383
|
-
const lastDrop = rules.map((rule) => rule.
|
|
384
|
-
return rules.slice(0, Math.max(0, lastDrop)).some((rule) => rule.
|
|
394
|
+
static acceptsBeforeDrop(rules: readonly AclRule[]): boolean {
|
|
395
|
+
const lastDrop = rules.map((rule) => rule.then).lastIndexOf("drop");
|
|
396
|
+
return rules.slice(0, Math.max(0, lastDrop)).some((rule) => rule.then === "accept");
|
|
385
397
|
}
|
|
386
398
|
}
|