@takodotid/circuit 0.34.3 → 0.36.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.
Files changed (240) hide show
  1. package/README.md +14 -15
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11345 -9024
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +18 -12
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -700
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /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
+ }