@takodotid/circuit 0.34.3 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11345 -9024
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +17 -11
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
- package/src/adapters/devices/raisecom-ros/index.ts +0 -700
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
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
|
+
|
|
3
|
+
import { Address } from "../../core/address";
|
|
4
|
+
import type { AclRule, Device, Family, FilterMatch, FilterRule, NatRule, Ports, Prefix, Protocol } from "../../schema";
|
|
5
|
+
import { FortiOsNames } from "./names";
|
|
6
|
+
import { FortiOsBlock, FortiOsTree } from "./tree";
|
|
7
|
+
|
|
8
|
+
/** The address objects, groups and services rules name, each created once. */
|
|
9
|
+
class Objects {
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly root: FortiOsBlock,
|
|
12
|
+
private readonly device: Device
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
/** The address a rule names on one side: one prefix, a set, or everything. */
|
|
16
|
+
address(prefix: string | undefined, set: string | undefined, family: Family): string {
|
|
17
|
+
if (set) return this.group(set, this.device.firewall?.address_sets?.[set] ?? [], family);
|
|
18
|
+
if (!prefix) return "all";
|
|
19
|
+
|
|
20
|
+
return this.prefix(prefix, family);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private prefix(prefix: string, family: Family): string {
|
|
24
|
+
const name = FortiOsNames.network(prefix);
|
|
25
|
+
|
|
26
|
+
if (family === "ipv4") this.root.block("firewall address").entry(name).set("subnet", FortiOsNames.subnet(name));
|
|
27
|
+
else this.root.block("firewall address6").entry(name).set("ip6", name);
|
|
28
|
+
|
|
29
|
+
return name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A group of the members of one family, named once for both families. */
|
|
33
|
+
group(name: string, members: readonly string[], family: Family): string {
|
|
34
|
+
const names = members
|
|
35
|
+
.filter((member) => FortiOsNames.familyOfPrefix(member) === family)
|
|
36
|
+
.map((member) => this.prefix(member, family));
|
|
37
|
+
|
|
38
|
+
const table = family === "ipv4" ? "firewall addrgrp" : "firewall addrgrp6";
|
|
39
|
+
this.root
|
|
40
|
+
.block(table)
|
|
41
|
+
.entry(name)
|
|
42
|
+
.set("member", FortiOsTree.quoteAll([...new Set(names)]));
|
|
43
|
+
return name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A pool of one address, for source NAT to it. */
|
|
47
|
+
pool(address: string): void {
|
|
48
|
+
this.root.block("firewall ippool").entry(address).set("startip", address).set("endip", address);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
service(match: FilterMatch<string, string> | undefined, family: Family): string {
|
|
52
|
+
const { name, block } = FortiOsFirewall.serviceOf(match, family);
|
|
53
|
+
|
|
54
|
+
// Transparent mode deletes the factory's services, `ALL` with them, so every protocol is a service of Circuit's own there.
|
|
55
|
+
if (name === "ALL" && FortiOsNames.isTransparent(this.device)) {
|
|
56
|
+
this.root.block("firewall service custom").entry(FortiOsFirewall.ALL_IN_TRANSPARENT).set("protocol", "IP");
|
|
57
|
+
return FortiOsFirewall.ALL_IN_TRANSPARENT;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!block) return name;
|
|
61
|
+
|
|
62
|
+
const entry = this.root.block("firewall service custom").entry(name);
|
|
63
|
+
for (const [key, value] of block.settings) entry.set(key, value);
|
|
64
|
+
return name;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class FortiOsFirewall {
|
|
69
|
+
/** IP protocol numbers, for a protocol the platform names only by number. */
|
|
70
|
+
static readonly PROTOCOL_NUMBER: Record<Protocol, number> = { tcp: 6, udp: 17, icmp: 1, gre: 47, esp: 50, ah: 51, ospf: 89, vrrp: 112 };
|
|
71
|
+
|
|
72
|
+
/** The device's own services, by the config's name, with the protocol and port each listens on unless the config moves it. */
|
|
73
|
+
private static readonly SERVICES = {
|
|
74
|
+
ssh: { protocol: "tcp", port: 22 },
|
|
75
|
+
https: { protocol: "tcp", port: 443 },
|
|
76
|
+
http: { protocol: "tcp", port: 80 },
|
|
77
|
+
telnet: { protocol: "tcp", port: 23 },
|
|
78
|
+
snmp: { protocol: "udp", port: 161 },
|
|
79
|
+
native: { protocol: "tcp", port: 541 },
|
|
80
|
+
} as const;
|
|
81
|
+
|
|
82
|
+
/** Every protocol, as a service of Circuit's own, for a device in transparent mode. */
|
|
83
|
+
static readonly ALL_IN_TRANSPARENT = "circuit-all";
|
|
84
|
+
|
|
85
|
+
/** NAT rules of both directions, which central NAT needs turned on. */
|
|
86
|
+
static natRules(device: Device): NatRule<string, string>[] {
|
|
87
|
+
const nat = device.firewall?.nat;
|
|
88
|
+
return [...(nat?.source ?? []), ...(nat?.destination ?? [])];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Which families a rule applies to: its own, its addresses', or both. */
|
|
92
|
+
static familiesOf(device: Device, match: FilterMatch<string, string> | undefined): Family[] {
|
|
93
|
+
if (match?.family) return [match.family];
|
|
94
|
+
|
|
95
|
+
const prefixes = [match?.src, match?.dst].filter((prefix): prefix is Prefix => prefix !== undefined);
|
|
96
|
+
if (prefixes.length) return [...new Set(prefixes.map(FortiOsNames.familyOfPrefix))];
|
|
97
|
+
|
|
98
|
+
let families: Family[] = ["ipv4", "ipv6"];
|
|
99
|
+
|
|
100
|
+
for (const set of [match?.src_set, match?.dst_set]) {
|
|
101
|
+
if (!set) continue;
|
|
102
|
+
|
|
103
|
+
const present = new Set((device.firewall?.address_sets?.[set] ?? []).map(FortiOsNames.familyOfPrefix));
|
|
104
|
+
families = families.filter((family) => present.has(family));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return families;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A port or ports as the platform writes them in a service: `22`, `1000-2000`. */
|
|
111
|
+
static portList(ports: Ports | undefined): string[] {
|
|
112
|
+
if (ports === undefined) return [];
|
|
113
|
+
|
|
114
|
+
const list = Array.isArray(ports) ? ports : [ports as number | { min: number; max: number }];
|
|
115
|
+
return list.map((port) => (typeof port === "number" ? String(port) : `${port.min}-${port.max}`));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 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<string, string> | undefined, family: Family): { name: string; block?: FortiOsBlock } {
|
|
120
|
+
const protocol = match?.protocol;
|
|
121
|
+
const destinations = FortiOsFirewall.portList(match?.dst_port);
|
|
122
|
+
const sources = FortiOsFirewall.portList(match?.src_port);
|
|
123
|
+
|
|
124
|
+
if (!protocol && !destinations.length && !sources.length) return { name: "ALL" };
|
|
125
|
+
|
|
126
|
+
if (!protocol || protocol === "tcp" || protocol === "udp") {
|
|
127
|
+
const ranges = (destinations.length ? destinations : ["1-65535"]).flatMap((destination) =>
|
|
128
|
+
sources.length ? sources.map((source) => `${destination}:${source}`) : [destination]
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const protocols = protocol ? [protocol] : (["tcp", "udp"] as const);
|
|
132
|
+
const name = `${protocols.join("+")}/${destinations.join(",") || "any"}${sources.length ? ` from ${sources.join(",")}` : ""}`;
|
|
133
|
+
const block = new FortiOsBlock();
|
|
134
|
+
for (const each of protocols) block.set(`${each}-portrange`, ranges.join(" "));
|
|
135
|
+
return { name, block };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (protocol === "icmp") {
|
|
139
|
+
const kind = family === "ipv4" ? "ICMP" : "ICMP6";
|
|
140
|
+
return { name: kind.toLowerCase(), block: new FortiOsBlock().set("protocol", kind).set("icmptype", FortiOsTree.UNSET) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const number = FortiOsFirewall.PROTOCOL_NUMBER[protocol];
|
|
144
|
+
return { name: `protocol/${number}`, block: new FortiOsBlock().set("protocol", "IP").set("protocol-number", number) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 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<string, string>): boolean {
|
|
149
|
+
const state = rule.match?.state;
|
|
150
|
+
return (
|
|
151
|
+
rule.action === "accept" &&
|
|
152
|
+
state !== undefined &&
|
|
153
|
+
state.length > 0 &&
|
|
154
|
+
state.every((one) => one === "established" || one === "related")
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Packet filtering, who reaches the device, NAT and stateless ACLs. */
|
|
159
|
+
static render(root: FortiOsBlock, device: Device): void {
|
|
160
|
+
const fw = device.firewall;
|
|
161
|
+
const objects = new Objects(root, device);
|
|
162
|
+
|
|
163
|
+
const forward = fw?.filter?.forward;
|
|
164
|
+
const policies = { ipv4: 0, ipv6: 0 };
|
|
165
|
+
|
|
166
|
+
for (const rule of forward?.rules ?? []) {
|
|
167
|
+
if (FortiOsFirewall.isImplied(rule)) continue;
|
|
168
|
+
|
|
169
|
+
for (const family of FortiOsFirewall.familiesOf(device, rule.match)) {
|
|
170
|
+
const table = root.block(family === "ipv4" ? "firewall policy" : "firewall policy6");
|
|
171
|
+
FortiOsFirewall.policy(table.entry(++policies[family]), objects, device, rule, family);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// A packet no rule matched is accepted unless the chain drops it; the platform's own last word is deny.
|
|
176
|
+
if (forward?.default !== "drop") {
|
|
177
|
+
for (const family of ["ipv4", "ipv6"] as const) {
|
|
178
|
+
const table = root.block(family === "ipv4" ? "firewall policy" : "firewall policy6");
|
|
179
|
+
FortiOsFirewall.policy(table.entry(++policies[family]), objects, device, { action: "accept" }, family);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const localIns = { ipv4: 0, ipv6: 0 };
|
|
184
|
+
const localInEntry = (family: Family) =>
|
|
185
|
+
root.block(family === "ipv4" ? "firewall local-in-policy" : "firewall local-in-policy6").entry(++localIns[family]);
|
|
186
|
+
|
|
187
|
+
FortiOsFirewall.management(localInEntry, objects, device);
|
|
188
|
+
|
|
189
|
+
const input = fw?.filter?.input;
|
|
190
|
+
|
|
191
|
+
for (const rule of input?.rules ?? []) {
|
|
192
|
+
if (FortiOsFirewall.isImplied(rule)) continue;
|
|
193
|
+
|
|
194
|
+
for (const family of FortiOsFirewall.familiesOf(device, rule.match)) {
|
|
195
|
+
FortiOsFirewall.localIn(localInEntry(family), objects, device, rule, family);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (input?.default === "drop") {
|
|
200
|
+
for (const family of ["ipv4", "ipv6"] as const) {
|
|
201
|
+
FortiOsFirewall.localIn(localInEntry(family), objects, device, { action: "drop" }, family);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
(fw?.nat?.source ?? []).forEach((rule, index) =>
|
|
206
|
+
FortiOsFirewall.sourceNat(root.block("firewall central-snat-map").entry(index + 1), objects, device, rule)
|
|
207
|
+
);
|
|
208
|
+
(fw?.nat?.destination ?? []).forEach((rule, index) =>
|
|
209
|
+
FortiOsFirewall.destinationNat(root.block("firewall vip").entry(`dnat-${index + 1}`), device, rule)
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
FortiOsFirewall.acls(root, objects, device);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Who reaches the device's own services: each service with sources to allow accepts them and refuses everyone else, ahead of the input chain. Services sharing their sources share the policies, named after `management.allow` or the service.
|
|
217
|
+
*/
|
|
218
|
+
private static management(entry: (family: Family) => FortiOsBlock, objects: Objects, device: Device): void {
|
|
219
|
+
type Services = typeof FortiOsFirewall.SERVICES;
|
|
220
|
+
|
|
221
|
+
const config = device.management ?? {};
|
|
222
|
+
const guards = new Map<string, { sources: readonly string[]; services: string[] }>();
|
|
223
|
+
|
|
224
|
+
for (const [service, listener] of Object.entries(FortiOsFirewall.SERVICES) as [keyof Services, Services[keyof Services]][]) {
|
|
225
|
+
const settings = config[service];
|
|
226
|
+
const sources = settings?.allow ?? config.allow;
|
|
227
|
+
if (!settings || !sources?.length) continue;
|
|
228
|
+
|
|
229
|
+
const name = settings.allow ? `circuit-${service}` : "circuit-management";
|
|
230
|
+
const guard = guards.get(name) ?? { sources, services: [] };
|
|
231
|
+
guard.services.push(objects.service({ protocol: listener.protocol, dst_port: settings.port ?? listener.port }, "ipv4"));
|
|
232
|
+
guards.set(name, guard);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const [name, guard] of guards) {
|
|
236
|
+
for (const family of ["ipv4", "ipv6"] as const) {
|
|
237
|
+
const services = FortiOsTree.quoteAll(guard.services);
|
|
238
|
+
|
|
239
|
+
if (guard.sources.some((source) => FortiOsNames.familyOfPrefix(source) === family)) {
|
|
240
|
+
const accept = entry(family);
|
|
241
|
+
accept.set("intf", FortiOsTree.quote("any"));
|
|
242
|
+
accept.set("srcaddr", FortiOsTree.quote(objects.group(name, guard.sources, family)));
|
|
243
|
+
accept.set("dstaddr", FortiOsTree.quote("all"));
|
|
244
|
+
accept.set("action", "accept");
|
|
245
|
+
accept.set("service", services);
|
|
246
|
+
accept.set("schedule", FortiOsTree.quote("always"));
|
|
247
|
+
accept.set("comments", FortiOsTree.quote("Management from allowed sources"));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const deny = entry(family);
|
|
251
|
+
deny.set("intf", FortiOsTree.quote("any"));
|
|
252
|
+
deny.set("srcaddr", FortiOsTree.quote("all"));
|
|
253
|
+
deny.set("dstaddr", FortiOsTree.quote("all"));
|
|
254
|
+
deny.set("action", "deny");
|
|
255
|
+
deny.set("service", services);
|
|
256
|
+
deny.set("schedule", FortiOsTree.quote("always"));
|
|
257
|
+
deny.set("comments", FortiOsTree.quote("Management from anywhere else"));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The interface a rule names on one side, or `any`, quoted. */
|
|
263
|
+
private static interfaceOf(device: Device, name: string | undefined): string {
|
|
264
|
+
return FortiOsTree.quote(name ? FortiOsNames.nameOf(device, name) : "any");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** A policy for traffic through the device. A rejection answers the sender; the platform's deny is silent otherwise. */
|
|
268
|
+
private static policy(entry: FortiOsBlock, objects: Objects, device: Device, rule: FilterRule<string, string>, family: Family): void {
|
|
269
|
+
const match = rule.match;
|
|
270
|
+
|
|
271
|
+
entry.set("srcintf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
|
|
272
|
+
entry.set("dstintf", FortiOsFirewall.interfaceOf(device, match?.out_interface));
|
|
273
|
+
entry.set("srcaddr", FortiOsTree.quote(objects.address(match?.src, match?.src_set, family)));
|
|
274
|
+
entry.set("dstaddr", FortiOsTree.quote(objects.address(match?.dst, match?.dst_set, family)));
|
|
275
|
+
entry.set("action", rule.action === "accept" ? "accept" : "deny");
|
|
276
|
+
entry.set("schedule", FortiOsTree.quote("always"));
|
|
277
|
+
entry.set("service", FortiOsTree.quote(objects.service(match, family)));
|
|
278
|
+
|
|
279
|
+
// The device gives a deny policy `logtraffic disable` of its own accord.
|
|
280
|
+
if (rule.action !== "accept") entry.set("logtraffic", "disable");
|
|
281
|
+
if (rule.action === "accept" && !device.hardware_offload && !rule.offload) entry.set("auto-asic-offload", "disable");
|
|
282
|
+
if (rule.description) entry.set("comments", FortiOsTree.quote(rule.description));
|
|
283
|
+
if (rule.action === "reject") entry.set("send-deny-packet", "enable");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** A local-in policy, for traffic to the device itself. */
|
|
287
|
+
private static localIn(entry: FortiOsBlock, objects: Objects, device: Device, rule: FilterRule<string, string>, family: Family): void {
|
|
288
|
+
const match = rule.match;
|
|
289
|
+
|
|
290
|
+
entry.set("intf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
|
|
291
|
+
entry.set("srcaddr", FortiOsTree.quote(objects.address(match?.src, match?.src_set, family)));
|
|
292
|
+
entry.set("dstaddr", FortiOsTree.quote(objects.address(match?.dst, match?.dst_set, family)));
|
|
293
|
+
entry.set("action", rule.action === "accept" ? "accept" : "deny");
|
|
294
|
+
entry.set("service", FortiOsTree.quote(objects.service(match, family)));
|
|
295
|
+
entry.set("schedule", FortiOsTree.quote("always"));
|
|
296
|
+
if (rule.description) entry.set("comments", FortiOsTree.quote(rule.description));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Source NAT: to the outgoing interface's address, or to a pool of one address. */
|
|
300
|
+
private static sourceNat(entry: FortiOsBlock, objects: Objects, device: Device, rule: NatRule<string, string>): void {
|
|
301
|
+
const match = rule.match;
|
|
302
|
+
|
|
303
|
+
entry.set("orig-addr", FortiOsTree.quote(objects.address(match?.src, match?.src_set, "ipv4")));
|
|
304
|
+
entry.set("srcintf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
|
|
305
|
+
entry.set("dst-addr", FortiOsTree.quote(objects.address(match?.dst, match?.dst_set, "ipv4")));
|
|
306
|
+
entry.set("dstintf", FortiOsFirewall.interfaceOf(device, match?.out_interface));
|
|
307
|
+
if (match?.protocol) entry.set("protocol", FortiOsFirewall.PROTOCOL_NUMBER[match.protocol]);
|
|
308
|
+
|
|
309
|
+
const ports = FortiOsFirewall.portList(match?.src_port);
|
|
310
|
+
|
|
311
|
+
if (rule.action === "snat") {
|
|
312
|
+
objects.pool(rule.to);
|
|
313
|
+
entry.set("nat-ippool", FortiOsTree.quote(rule.to));
|
|
314
|
+
// A pool keeps the source port only when told which.
|
|
315
|
+
if (ports.length) entry.set("orig-port", ports[0]!).set("nat-port", rule.to_port ?? ports[0]!);
|
|
316
|
+
} else if (ports.length) {
|
|
317
|
+
entry.set("orig-port", ports[0]!);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (rule.description) entry.set("comments", FortiOsTree.quote(rule.description));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Destination NAT, as a virtual IP: the address it answers on, and the port it forwards when the rule names one. */
|
|
324
|
+
private static destinationNat(entry: FortiOsBlock, device: Device, rule: NatRule<string, string>): void {
|
|
325
|
+
if (rule.action !== "dnat") return;
|
|
326
|
+
|
|
327
|
+
const match = rule.match;
|
|
328
|
+
if (rule.description) entry.set("comment", FortiOsTree.quote(rule.description));
|
|
329
|
+
if (match?.dst) entry.set("extip", Address.host(FortiOsNames.withLength(match.dst)));
|
|
330
|
+
entry.set("extintf", FortiOsFirewall.interfaceOf(device, match?.in_interface));
|
|
331
|
+
|
|
332
|
+
const sources = match?.src ? [match.src] : match?.src_set ? (device.firewall?.address_sets?.[match.src_set] ?? []) : [];
|
|
333
|
+
if (sources.length) entry.set("src-filter", FortiOsTree.quoteAll(sources.map(FortiOsNames.network)));
|
|
334
|
+
|
|
335
|
+
const ports = FortiOsFirewall.portList(match?.dst_port);
|
|
336
|
+
if (ports.length) entry.set("portforward", "enable");
|
|
337
|
+
entry.set("mappedip", FortiOsTree.quote(rule.to));
|
|
338
|
+
|
|
339
|
+
if (ports.length) {
|
|
340
|
+
if (match?.protocol === "udp") entry.set("protocol", "udp");
|
|
341
|
+
entry.set("extport", ports[0]!);
|
|
342
|
+
entry.set("mappedport", rule.to_port ?? ports[0]!);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* A port's ACL as the platform's stateless ACLs, which only drop: each rule that drops becomes one, of each family it applies to. What no rule drops passes, so the rules that accept after the last drop are already there; `unsupported()` refuses one before a drop.
|
|
348
|
+
*/
|
|
349
|
+
private static acls(root: FortiOsBlock, objects: Objects, device: Device): void {
|
|
350
|
+
const counters = { ipv4: 0, ipv6: 0 };
|
|
351
|
+
|
|
352
|
+
for (const [port, settings] of Object.entries(device.ports ?? {})) {
|
|
353
|
+
if (!settings?.acl) continue;
|
|
354
|
+
|
|
355
|
+
for (const rule of device.acls?.[settings.acl] ?? []) {
|
|
356
|
+
if (rule.action !== "drop") continue;
|
|
357
|
+
|
|
358
|
+
const match = rule.match as FilterMatch<string, string> | undefined;
|
|
359
|
+
|
|
360
|
+
for (const family of FortiOsFirewall.familiesOf(device, match)) {
|
|
361
|
+
const entry = root.block(family === "ipv4" ? "firewall acl" : "firewall acl6").entry(++counters[family]);
|
|
362
|
+
if (rule.description) entry.set("comments", FortiOsTree.quote(rule.description));
|
|
363
|
+
entry.set("interface", FortiOsTree.quote(FortiOsNames.nameOf(device, port)));
|
|
364
|
+
entry.set("srcaddr", FortiOsTree.quote(objects.address(match?.src, undefined, family)));
|
|
365
|
+
entry.set("dstaddr", FortiOsTree.quote(objects.address(match?.dst, undefined, family)));
|
|
366
|
+
entry.set("service", FortiOsTree.quote(objects.service(match, family)));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Whether a port's ACL drops before it accepts: an accept before a drop is an exception the platform's ACLs cannot hold. */
|
|
373
|
+
static acceptsBeforeDrop(rules: readonly AclRule<string>[]): boolean {
|
|
374
|
+
const lastDrop = rules.map((rule) => rule.action).lastIndexOf("drop");
|
|
375
|
+
return rules.slice(0, Math.max(0, lastDrop)).some((rule) => rule.action === "accept");
|
|
376
|
+
}
|
|
377
|
+
}
|