@takodotid/circuit 0.34.2 → 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.
Files changed (240) hide show
  1. package/README.md +13 -14
  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 +11347 -9018
  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 +17 -11
  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 -25
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -693
  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
@@ -1,872 +0,0 @@
1
- // The neutral model as Junos `set` statements, in the form `show configuration | display set` prints, so the two compare statement by statement.
2
-
3
- import { contains, familyOf, host, network, split } from "../../../core/addr";
4
- import { marker } from "../../../core/secrets";
5
- import type { AclRule, Device, Family, Interface, Port, Ports, Rate, Severity, Vrrp } from "../../../schema";
6
- import { portsOf, spell } from "../catalog";
7
- import { lagIds } from "../derive";
8
- import { renderBgp } from "./bgp";
9
-
10
- type AnyPort = Port<string, string, string, string>;
11
- type AnyInterface = Interface<string, string>;
12
- type LagInterface = Extract<AnyInterface, { type: "lag" }>;
13
- type Switched = AnyPort | LagInterface;
14
-
15
- /** The class each role logs in with. */
16
- const USER_CLASS = { admin: "super-user", operator: "operator", "read-only": "read-only" } as const;
17
-
18
- /** The statement each kind of public key is written under, by the key's own prefix. */
19
- export const KEY_STATEMENT: Record<string, string> = {
20
- "ssh-ed25519": "ssh-ed25519",
21
- "ssh-rsa": "ssh-rsa",
22
- "ecdsa-sha2-nistp256": "ssh-ecdsa",
23
- "ecdsa-sha2-nistp384": "ssh-ecdsa",
24
- "ecdsa-sha2-nistp521": "ssh-ecdsa",
25
- };
26
-
27
- const SNMP_AUTH = { sha1: "authentication-sha", sha256: "authentication-sha256" } as const;
28
- const SNMP_PRIVACY = { aes128: "privacy-aes128", des: "privacy-des" } as const;
29
-
30
- /** The SNMPv3 group every user joins, with read access to everything. */
31
- const SNMP_GROUP = "circuit";
32
-
33
- /** A severity as the platform names it. It has no debug; `any` takes everything. */
34
- const LEVEL: Record<Severity, string> = {
35
- emergency: "emergency",
36
- alert: "alert",
37
- critical: "critical",
38
- error: "error",
39
- warning: "warning",
40
- notice: "notice",
41
- info: "info",
42
- debug: "any",
43
- };
44
-
45
- /** The filters that decide who reaches the device's own services, one per family. A filter name is unique across families. */
46
- const SERVICES_FILTER = { inet: "circuit-services", inet6: "circuit-services-v6" } as const;
47
-
48
- /** The policy OSPF announces other routes with. */
49
- const OSPF_EXPORT = "circuit-ospf-redistribute";
50
-
51
- /** Strong ciphers, MACs and key exchanges, offered unless `weak_crypto`. */
52
- const SSH_CIPHERS = ["aes256-gcm@openssh.com", "aes128-gcm@openssh.com", "aes256-ctr", "aes192-ctr", "aes128-ctr"];
53
- const SSH_MACS = ["hmac-sha2-512", "hmac-sha2-256"];
54
- const SSH_KEY_EXCHANGES = ["curve25519-sha256", "ecdh-sha2-nistp521", "ecdh-sha2-nistp384", "ecdh-sha2-nistp256", "group-exchange-sha2"];
55
-
56
- /** The transport and port each management service listens on. */
57
- const SERVICE_PORTS = {
58
- ssh: ["tcp", 22],
59
- telnet: ["tcp", 23],
60
- ftp: ["tcp", 21],
61
- snmp: ["udp", 161],
62
- http: ["tcp", 80],
63
- api: ["tcp", 3000],
64
- native: ["tcp", 830],
65
- } as const;
66
-
67
- /** Detection under a second: three missed packets 300 ms apart. */
68
- export const BFD_INTERVAL = 300;
69
-
70
- /** Quote a value unless it is a plain word. Quoting more than the platform would is harmless: the comparison ignores quotes. */
71
- export function quote(value: string): string {
72
- if (/^[\w.:/@-]+$/.test(value)) return value;
73
-
74
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
75
- return `"${escaped}"`;
76
- }
77
-
78
- /** Collects `set` statements, and comments the plan reads. */
79
- export class Statements {
80
- private readonly lines: string[] = [];
81
- private readonly notes: string[] = [];
82
-
83
- set(...words: (string | number)[]): void {
84
- this.lines.push(`set ${words.join(" ")}`);
85
- }
86
-
87
- note(comment: string): void {
88
- this.notes.push(comment);
89
- }
90
-
91
- /** The statements as the device prints them: each once, and none that another one extends, because the platform prints only the longest. */
92
- text(): string {
93
- const unique = [...new Set(this.lines)];
94
- const extended = new Set<string>();
95
-
96
- for (const line of unique) {
97
- const words = line.split(" ");
98
- for (let length = 2; length < words.length; length++) extended.add(words.slice(0, length).join(" "));
99
- }
100
-
101
- return [...unique.filter((line) => !extended.has(line)), ...this.notes].join("\n") + "\n";
102
- }
103
- }
104
-
105
- /** What the platform calls a port, a LAG, a VLAN interface or a loopback. */
106
- export class Names {
107
- readonly lags: Map<string, number>;
108
- readonly loopbacks = new Map<string, number>();
109
- readonly tunnels = new Map<string, number>();
110
-
111
- constructor(private readonly device: Device) {
112
- this.lags = lagIds(device);
113
-
114
- // The default table's loopback is unit 0; one in a VRF takes the next unit.
115
- const loopbacks = Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "loopback");
116
- let unit = 0;
117
-
118
- for (const [name, iface] of loopbacks) if (!iface.vrf) this.loopbacks.set(name, 0);
119
-
120
- for (const [name, iface] of loopbacks) if (iface.vrf) this.loopbacks.set(name, ++unit);
121
-
122
- // GRE tunnels are units of the one tunnel interface, in order.
123
- const tunnels = Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "gre");
124
- tunnels.forEach(([name], index) => this.tunnels.set(name, index));
125
- }
126
-
127
- /** A physical port. A 10G cage holding a 1G optic is `ge-`, not `xe-`. */
128
- port(port: string): string {
129
- const name = spell(this.device.platform, this.device.model, port);
130
- const settings = this.device.ports?.[port as keyof typeof this.device.ports];
131
- return settings?.speed === "1g" ? name.replace(/^xe-/, "ge-") : name;
132
- }
133
-
134
- /** The physical interface a port or a LAG is: `xe-0/0/1`, `ae1`. */
135
- physical(name: string): string {
136
- if (this.device.interfaces?.[name]?.type === "lag") return `ae${this.lags.get(name)}`;
137
-
138
- return this.port(name);
139
- }
140
-
141
- /** The logical interface that holds addresses: `xe-0/0/1.0`, `ae1.0`, `irb.10`, `lo0.0`. */
142
- unit(name: string): string {
143
- const iface = this.device.interfaces?.[name];
144
- if (iface?.type === "vlan") return `irb.${this.device.vlans![iface.vlan]!.id}`;
145
-
146
- if (iface?.type === "loopback") return `lo0.${this.loopbacks.get(name)}`;
147
-
148
- if (iface?.type === "gre") return `gr-0/0/0.${this.tunnels.get(name)}`;
149
-
150
- return `${this.physical(name)}.0`;
151
- }
152
-
153
- /** The physical ports behind a port or a LAG. */
154
- members(name: string): string[] {
155
- if (this.device.interfaces?.[name]?.type !== "lag") return [this.port(name)];
156
-
157
- const ports = Object.entries(this.device.ports ?? {}).filter(([, settings]) => settings?.lag === name);
158
- return ports.map(([port]) => this.port(port));
159
- }
160
- }
161
-
162
- /** The address a VRRP group sits on: the one whose network holds the virtual address. */
163
- export function vrrpAddress(addresses: readonly string[], group: Vrrp): string | undefined {
164
- return addresses.find((address) => familyOf(address) === "ipv4" && contains(address, group.address));
165
- }
166
-
167
- /** Kinds of flooded traffic, with the statement that leaves each out of a storm-control profile. */
168
- const STORM_KINDS = [
169
- ["broadcast", "no-broadcast"],
170
- ["multicast", "no-multicast"],
171
- ["unknown_unicast", "no-unknown-unicast"],
172
- ] as const;
173
-
174
- /**
175
- * The storm-control profile a port or LAG names. A profile holds one threshold for every kind it limits, so a port's kinds must share one in percent; `unsupported()` refuses any other.
176
- */
177
- export function stormProfile(settings: Switched): { name: string; percent: number; left: string[] } | undefined {
178
- const storm = settings.storm_control;
179
- if (!storm) return undefined;
180
-
181
- const limited = STORM_KINDS.filter(([kind]) => storm[kind]);
182
- const first = limited[0] && storm[limited[0][0]];
183
- if (!first || !("percent" in first)) return undefined;
184
-
185
- const kinds = limited.map(([kind]) => kind.replace("_", "-"));
186
- const left = STORM_KINDS.filter(([kind]) => !storm[kind]).map(([, statement]) => statement);
187
- return { name: `${kinds.join("-")}-${first.percent}-percent`, percent: first.percent, left };
188
- }
189
-
190
- export function render(device: Device): string {
191
- const out = new Statements();
192
- const names = new Names(device);
193
-
194
- system(out, device);
195
- users(out, device);
196
- services(out, device);
197
- snmp(out, device);
198
- vlans(out, device, names);
199
- ports(out, device, names);
200
- lags(out, device, names);
201
- routedInterfaces(out, device, names);
202
- stormProfiles(out, device);
203
- servicesFilter(out, device, names);
204
- vrfs(out, device, names);
205
- staticRoutes(out, device, names);
206
- spanningTree(out, device, names);
207
- lldp(out, device, names);
208
- ospf(out, device, names);
209
- dhcpRelay(out, device, names);
210
- dhcpServers(out, device, names);
211
- routerAdvertisements(out, device, names);
212
- acls(out, device, names);
213
- renderBgp(out, device);
214
- sflow(out, device, names);
215
-
216
- return out.text();
217
- }
218
-
219
- function system(out: Statements, device: Device): void {
220
- out.set("system host-name", quote(device.name));
221
-
222
- const system = device.system ?? {};
223
- if (system.timezone) out.set("system time-zone", system.timezone);
224
- if (system.banner) out.set("system login message", quote(system.banner));
225
-
226
- for (const server of system.dns?.servers ?? []) out.set("system name-server", server);
227
-
228
- for (const server of system.ntp?.servers ?? []) out.set("system ntp server", server);
229
-
230
- // Redirects are sent unless turned off, for each family.
231
- if (!system.ip?.icmp_redirects) {
232
- out.set("system no-redirects");
233
- out.set("system no-redirects-ipv6");
234
- }
235
-
236
- const local = system.logging?.local;
237
- if (local) {
238
- out.set("system syslog file messages any", LEVEL[local.level]);
239
- if (local.files !== undefined) out.set("system syslog file messages archive files", local.files);
240
- }
241
-
242
- for (const remote of system.logging?.remote ?? []) {
243
- out.set("system syslog host", remote.address, "any", LEVEL[remote.level]);
244
- if (remote.port !== undefined && remote.port !== 514) out.set("system syslog host", remote.address, "port", remote.port);
245
- }
246
- }
247
-
248
- /** Every user but root logs in with a class. Root is the platform's own account and takes only its password and keys. */
249
- function users(out: Statements, device: Device): void {
250
- for (const [name, user] of Object.entries(device.users ?? {})) {
251
- const login = name === "root" ? "system root-authentication" : `system login user ${name} authentication`;
252
-
253
- if (name !== "root") {
254
- out.set(`system login user ${name} class`, USER_CLASS[user.role]);
255
- if (user.description) out.set(`system login user ${name} full-name`, quote(user.description));
256
- }
257
-
258
- out.set(login, "plain-text-password-value", marker(user.password));
259
-
260
- for (const key of user.ssh_keys ?? []) out.set(login, KEY_STATEMENT[key.split(" ")[0]!]!, quote(key));
261
- }
262
- }
263
-
264
- function services(out: Statements, device: Device): void {
265
- const management = device.management ?? {};
266
-
267
- const ssh = management.ssh;
268
- if (ssh) {
269
- out.set("system services ssh");
270
- if (ssh.port !== undefined && ssh.port !== 22) out.set("system services ssh port", ssh.port);
271
-
272
- if (!ssh.weak_crypto) {
273
- for (const cipher of SSH_CIPHERS) out.set("system services ssh ciphers", cipher);
274
- for (const mac of SSH_MACS) out.set("system services ssh macs", mac);
275
- for (const exchange of SSH_KEY_EXCHANGES) out.set("system services ssh key-exchange", exchange);
276
- }
277
- }
278
-
279
- if (management.telnet) out.set("system services telnet");
280
- if (management.ftp) out.set("system services ftp");
281
-
282
- // The web interface, the REST API and NETCONF, each on its own port when given.
283
- const others = [
284
- ["http", "system services web-management http"],
285
- ["api", "system services rest http"],
286
- ["native", "system services netconf ssh"],
287
- ] as const;
288
-
289
- for (const [service, statement] of others) {
290
- const settings = management[service];
291
- if (!settings) continue;
292
-
293
- out.set(statement);
294
- if (settings.port !== undefined && settings.port !== SERVICE_PORTS[service][1]) out.set(statement, "port", settings.port);
295
- }
296
- }
297
-
298
- /** SNMP version 2c with one read community, version 3 with users in one read-only group, or both. */
299
- function snmp(out: Statements, device: Device): void {
300
- const snmp = device.management?.snmp;
301
- if (!snmp) return;
302
-
303
- if (snmp.contact) out.set("snmp contact", quote(snmp.contact));
304
- if (snmp.location) out.set("snmp location", quote(snmp.location));
305
- if (snmp.community) out.set("snmp community", marker(snmp.community), "authorization read-only");
306
-
307
- if (!snmp.users) return;
308
-
309
- for (const [name, user] of Object.entries(snmp.users)) {
310
- const usm = `snmp v3 usm local-engine user ${name}`;
311
- out.set(usm, SNMP_AUTH[user.auth], "authentication-password", marker(user.auth_password));
312
- out.set(usm, SNMP_PRIVACY[user.privacy], "privacy-password", marker(user.privacy_password));
313
- out.set("snmp v3 vacm security-to-group security-model usm security-name", name, "group", SNMP_GROUP);
314
- }
315
-
316
- out.set("snmp view all oid .1 include");
317
- out.set(`snmp v3 vacm access group ${SNMP_GROUP} default-context-prefix security-model usm security-level privacy read-view all`);
318
- }
319
-
320
- function vlans(out: Statements, device: Device, names: Names): void {
321
- for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
322
- out.set(`vlans ${name} vlan-id`, vlan.id);
323
- if (vlan.description) out.set(`vlans ${name} description`, quote(vlan.description));
324
- }
325
-
326
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
327
- if (iface.type === "vlan") out.set(`vlans ${iface.vlan} l3-interface`, names.unit(name));
328
- }
329
- }
330
-
331
- /** Layer 2 membership of a port or LAG. The native VLAN is a member too, or its untagged frames are dropped. */
332
- function switching(out: Statements, device: Device, physical: string, settings: Switched): void {
333
- const family = `interfaces ${physical} unit 0 family ethernet-switching`;
334
-
335
- if (settings.access_vlan) {
336
- out.set(family, "interface-mode access");
337
- out.set(family, "vlan members", settings.access_vlan);
338
- }
339
-
340
- if (settings.trunk_vlans) {
341
- out.set(family, "interface-mode trunk");
342
-
343
- const members = new Set([...settings.trunk_vlans, ...(settings.native_vlan ? [settings.native_vlan] : [])]);
344
- for (const vlan of members) out.set(family, "vlan members", vlan);
345
-
346
- if (settings.native_vlan) out.set(`interfaces ${physical} native-vlan-id`, device.vlans![settings.native_vlan]!.id);
347
- }
348
-
349
- const storm = stormProfile(settings);
350
- if (storm) out.set(family, "storm-control", storm.name);
351
- }
352
-
353
- /** Addresses and VRRP groups of a logical interface such as `irb.10`. */
354
- function addressing(out: Statements, unit: string, settings: AnyPort | AnyInterface): void {
355
- const [physical, number] = unit.split(".");
356
- const prefix = `interfaces ${physical} unit ${number}`;
357
- const addresses = (settings.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
358
-
359
- for (const address of addresses) out.set(`${prefix} family ${familyOf(address) === "ipv4" ? "inet" : "inet6"} address`, address);
360
-
361
- for (const group of settings.vrrp ?? []) {
362
- const address = vrrpAddress(addresses, group);
363
- if (!address) continue;
364
-
365
- // The platform preempts unless told not to.
366
- const statement = `${prefix} family inet address ${address} vrrp-group ${group.id}`;
367
- out.set(statement, "virtual-address", group.address);
368
- if (group.priority !== undefined) out.set(statement, "priority", group.priority);
369
- if (!group.preempt) out.set(statement, "no-preempt");
370
- if (group.interval !== undefined && group.interval !== 1) out.set(statement, "advertise-interval", group.interval);
371
- }
372
- }
373
-
374
- /** Every port the model has. One the config does not declare is turned off, and so is a 10G cage's name for the optic it does not hold. */
375
- function ports(out: Statements, device: Device, names: Names): void {
376
- for (const port of portsOf(device.platform, device.model)) {
377
- const settings = device.ports?.[port as keyof typeof device.ports];
378
- const name = names.port(port);
379
-
380
- // A 10G cage is `xe-` with a 10G optic and `ge-` with a 1G one. The name not in use stays off, so a swapped optic does not bring up a port nobody configured.
381
- if (name.startsWith("xe-")) out.set(`interfaces ${name.replace("xe-", "ge-")} disable`);
382
- if (name.startsWith("ge-")) out.set(`interfaces ${name.replace("ge-", "xe-")} disable`);
383
-
384
- if (!settings) {
385
- out.set(`interfaces ${name} disable`);
386
- continue;
387
- }
388
-
389
- if (settings.description) out.set(`interfaces ${name} description`, quote(settings.description));
390
-
391
- if (settings.lag) {
392
- out.set(`interfaces ${name} ether-options 802.3ad`, `ae${names.lags.get(settings.lag)}`);
393
- continue;
394
- }
395
-
396
- const switched = !!(settings.access_vlan || settings.trunk_vlans);
397
- if (settings.mtu !== undefined) out.set(`interfaces ${name} mtu`, frameSize(settings.mtu, switched));
398
-
399
- switching(out, device, name, settings);
400
- addressing(out, `${name}.0`, settings);
401
- }
402
- }
403
-
404
- function lags(out: Statements, device: Device, names: Names): void {
405
- let highest = -1;
406
-
407
- for (const [name, lag] of Object.entries(device.interfaces ?? {})) {
408
- if (lag.type !== "lag") continue;
409
-
410
- const physical = names.physical(name);
411
- highest = Math.max(highest, names.lags.get(name)!);
412
-
413
- if (lag.description) out.set(`interfaces ${physical} description`, quote(lag.description));
414
- if (lag.mode !== "static") out.set(`interfaces ${physical} aggregated-ether-options lacp active`);
415
- if (lag.mtu !== undefined) out.set(`interfaces ${physical} mtu`, frameSize(lag.mtu, !!(lag.access_vlan || lag.trunk_vlans)));
416
-
417
- switching(out, device, physical, lag);
418
- addressing(out, `${physical}.0`, lag);
419
- }
420
-
421
- // The platform makes this many LAG interfaces, numbered from 0.
422
- if (highest >= 0) out.set("chassis aggregated-devices ethernet device-count", highest + 1);
423
- }
424
-
425
- /** VLAN interfaces, loopbacks and GRE tunnels: a description, a tunnel's ends, addresses and VRRP, and an MTU per family. */
426
- function routedInterfaces(out: Statements, device: Device, names: Names): void {
427
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
428
- if (iface.type !== "vlan" && iface.type !== "loopback" && iface.type !== "gre") continue;
429
-
430
- const unit = names.unit(name);
431
- const [physical, number] = unit.split(".");
432
- const prefix = `interfaces ${physical} unit ${number}`;
433
-
434
- if (iface.description) out.set(`${prefix} description`, quote(iface.description));
435
-
436
- if (iface.type === "gre") {
437
- out.set(`${prefix} tunnel source`, iface.local);
438
- out.set(`${prefix} tunnel destination`, iface.remote);
439
- }
440
-
441
- addressing(out, unit, iface);
442
-
443
- if (iface.mtu === undefined) continue;
444
-
445
- const addresses = (iface.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
446
- const families = new Set(addresses.map((address) => (familyOf(address) === "ipv4" ? "inet" : "inet6")));
447
- for (const family of families) out.set(`${prefix} family ${family} mtu`, iface.mtu);
448
- }
449
- }
450
-
451
- /** One profile per threshold the ports and LAGs name. */
452
- function stormProfiles(out: Statements, device: Device): void {
453
- const switched: Switched[] = [
454
- ...Object.values(device.ports ?? {}).filter((settings): settings is AnyPort => !!settings && !settings.lag),
455
- ...Object.values(device.interfaces ?? {}).filter((iface): iface is LagInterface => iface.type === "lag"),
456
- ];
457
-
458
- for (const settings of switched) {
459
- const storm = stormProfile(settings);
460
- if (!storm) continue;
461
-
462
- const profile = `forwarding-options storm-control-profiles ${storm.name} all`;
463
- out.set(profile, "bandwidth-percentage", storm.percent);
464
- for (const statement of storm.left) out.set(profile, statement);
465
- }
466
- }
467
-
468
- /**
469
- * Who reaches the device's own services: each guarded service from its sources only, time answered only to the servers the device keeps time from, and everything else as it was.
470
- *
471
- * The filter sits on every loopback unit and on each management port with an address, so it applies whichever routing table or port a packet arrives on.
472
- */
473
- function servicesFilter(out: Statements, device: Device, names: Names): void {
474
- const management = device.management ?? {};
475
- const guarded: { service: keyof typeof SERVICE_PORTS; sources: readonly string[] }[] = [];
476
-
477
- for (const service of Object.keys(SERVICE_PORTS) as (keyof typeof SERVICE_PORTS)[]) {
478
- const settings = management[service];
479
- const sources = settings?.allow ?? management.allow;
480
- if (settings && sources?.length) guarded.push({ service, sources });
481
- }
482
-
483
- const ntp = device.system?.ntp;
484
- const keepsTimeQuietly = !!ntp?.servers.length && !ntp.serve;
485
- if (!guarded.length && !keepsTimeQuietly) return;
486
-
487
- const families = [
488
- ["inet", "ipv4", "protocol"],
489
- ["inet6", "ipv6", "next-header"],
490
- ] as const;
491
-
492
- for (const [family, addressFamily, protocolWord] of families) {
493
- const filter = `firewall family ${family} filter ${SERVICES_FILTER[family]}`;
494
- const ofFamily = (prefix: string) => familyOf(host(prefix)) === addressFamily;
495
-
496
- for (const { service, sources } of guarded) {
497
- const [protocol, standardPort] = SERVICE_PORTS[service];
498
- const port = management[service]?.port ?? standardPort;
499
- const term = `${filter} term ${service}`;
500
- const allowed = sources.filter(ofFamily);
501
-
502
- // A term without a source would accept every source, so a family no source is in gets only the discard.
503
- if (allowed.length) {
504
- for (const source of allowed) out.set(term, "from source-address", source);
505
- out.set(term, "from", protocolWord, protocol);
506
- out.set(term, "from destination-port", port);
507
- out.set(term, "then accept");
508
- }
509
-
510
- const others = `${filter} term ${service}-others`;
511
- out.set(others, "from", protocolWord, protocol);
512
- out.set(others, "from destination-port", port);
513
- out.set(others, "then discard");
514
- }
515
-
516
- if (keepsTimeQuietly) {
517
- const servers = ntp!.servers.filter(ofFamily);
518
-
519
- if (servers.length) {
520
- const length = addressFamily === "ipv4" ? 32 : 128;
521
- for (const server of servers) out.set(`${filter} term ntp from source-address`, `${server}/${length}`);
522
- out.set(`${filter} term ntp from`, protocolWord, "udp");
523
- out.set(`${filter} term ntp from destination-port 123`);
524
- out.set(`${filter} term ntp then accept`);
525
- }
526
-
527
- out.set(`${filter} term ntp-others from`, protocolWord, "udp");
528
- out.set(`${filter} term ntp-others from destination-port 123`);
529
- out.set(`${filter} term ntp-others then discard`);
530
- }
531
-
532
- out.set(`${filter} term everything-else then accept`);
533
- }
534
-
535
- const anchors = new Set(["lo0.0", ...[...names.loopbacks.values()].map((unit) => `lo0.${unit}`)]);
536
-
537
- for (const [port, settings] of Object.entries(device.ports ?? {})) {
538
- const name = names.port(port);
539
- if (settings?.addresses?.length && name.startsWith("em")) anchors.add(`${name}.0`);
540
- }
541
-
542
- for (const anchor of anchors) {
543
- const [physical, unit] = anchor.split(".");
544
- out.set(`interfaces ${physical} unit ${unit} family inet filter input ${SERVICES_FILTER.inet}`);
545
- out.set(`interfaces ${physical} unit ${unit} family inet6 filter input ${SERVICES_FILTER.inet6}`);
546
- }
547
- }
548
-
549
- /** A VRF is a virtual router: its own table, holding its interfaces. */
550
- function vrfs(out: Statements, device: Device, names: Names): void {
551
- for (const [name, vrf] of Object.entries(device.vrfs ?? {})) {
552
- out.set(`routing-instances ${name} instance-type virtual-router`);
553
- if (vrf.description) out.set(`routing-instances ${name} description`, quote(vrf.description));
554
- }
555
-
556
- const routed = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})];
557
-
558
- for (const [name, settings] of routed) {
559
- if (settings?.vrf) out.set(`routing-instances ${settings.vrf} interface`, names.unit(name));
560
- }
561
- }
562
-
563
- /** A route with a distance names its next hop as qualified, so two routes to one prefix can each have their own. */
564
- function staticRoutes(out: Statements, device: Device, names: Names): void {
565
- if (device.routing?.router_id) out.set("routing-options router-id", device.routing.router_id);
566
-
567
- for (const route of device.routing?.static ?? []) {
568
- const ipv6 = familyOf(split(route.prefix).addr) === "ipv6";
569
- const table = ipv6 ? ` rib ${route.vrf ? `${route.vrf}.` : ""}inet6.0` : "";
570
- const options = route.vrf ? `routing-instances ${route.vrf} routing-options${table}` : `routing-options${table}`;
571
- const statement = `${options} static route ${route.prefix}`;
572
-
573
- if (route.blackhole) {
574
- out.set(statement, "discard");
575
- if (route.distance !== undefined) out.set(statement, "preference", route.distance);
576
- continue;
577
- }
578
-
579
- const nextHop = route.via ?? names.unit(route.interface!);
580
- if (route.distance === undefined) out.set(statement, "next-hop", nextHop);
581
- else out.set(statement, "qualified-next-hop", nextHop, "preference", route.distance);
582
- }
583
- }
584
-
585
- /** Spanning tree on every switched port and LAG, each with its own settings. RSTP when no mode is given. */
586
- function spanningTree(out: Statements, device: Device, names: Names): void {
587
- const stp = device.stp;
588
- if (!stp) return;
589
-
590
- // The platform has no plain STP; RSTP is held to it.
591
- const protocol = `protocols ${stp.mode === "mstp" ? "mstp" : "rstp"}`;
592
- if (stp.mode === "stp") out.set(protocol, "force-version stp");
593
- if (stp.priority !== undefined) out.set(protocol, "bridge-priority", stp.priority === 0 ? "0" : `${stp.priority / 1024}k`);
594
-
595
- const switched: [string, Switched][] = [];
596
-
597
- for (const [port, settings] of Object.entries(device.ports ?? {})) {
598
- if (settings && !settings.lag && (settings.access_vlan || settings.trunk_vlans)) switched.push([names.port(port), settings]);
599
- }
600
-
601
- for (const [name, lag] of Object.entries(device.interfaces ?? {})) {
602
- if (lag.type === "lag" && (lag.access_vlan || lag.trunk_vlans)) switched.push([names.physical(name), lag]);
603
- }
604
-
605
- for (const [name, settings] of switched) {
606
- const statement = `${protocol} interface ${name}`;
607
- out.set(statement);
608
-
609
- if (settings.stp === false) out.set(statement, "disable");
610
- else if (typeof settings.stp === "object") {
611
- if (settings.stp.edge) out.set(statement, "edge");
612
- if (settings.stp.cost !== undefined) out.set(statement, "cost", settings.stp.cost);
613
- if (settings.stp.priority !== undefined) out.set(statement, "priority", settings.stp.priority);
614
- }
615
- }
616
- }
617
-
618
- /** LLDP on every port or on those listed, a port's own setting over the device's. LLDP runs on a LAG's member ports. */
619
- function lldp(out: Statements, device: Device, names: Names): void {
620
- const lldp = device.lldp;
621
-
622
- if (lldp === true) out.set("protocols lldp interface all");
623
- else if (lldp) for (const name of lldp.interfaces) for (const port of names.members(name)) out.set("protocols lldp interface", port);
624
-
625
- for (const [port, settings] of Object.entries(device.ports ?? {})) {
626
- if (settings?.lldp === false) out.set("protocols lldp interface", names.port(port), "disable");
627
- else if (settings?.lldp === true) out.set("protocols lldp interface", names.port(port));
628
- }
629
- }
630
-
631
- /** OSPF for IPv4 and OSPFv3 for IPv6, with the same areas and interfaces. */
632
- function ospf(out: Statements, device: Device, names: Names): void {
633
- const ospf = device.routing?.ospf;
634
- if (!ospf) return;
635
-
636
- const protocols = (ospf.families ?? ["ipv4"]).map((family) => (family === "ipv4" ? "ospf" : "ospf3"));
637
-
638
- for (const protocol of protocols) {
639
- for (const [areaId, area] of Object.entries(ospf.areas)) {
640
- for (const [iface, settings] of Object.entries(area.interfaces)) {
641
- const statement = `protocols ${protocol} area ${areaId} interface ${names.unit(iface)}`;
642
- out.set(statement);
643
-
644
- if (settings?.cost !== undefined) out.set(statement, "metric", settings.cost);
645
- if (settings?.passive) out.set(statement, "passive");
646
- if (settings?.network === "point-to-point") out.set(statement, "interface-type p2p");
647
- if (settings?.bfd) out.set(statement, "bfd-liveness-detection minimum-interval", BFD_INTERVAL);
648
- }
649
- }
650
-
651
- if (ospf.redistribute?.length) out.set(`protocols ${protocol} export`, OSPF_EXPORT);
652
- }
653
-
654
- for (const source of ospf.redistribute ?? []) {
655
- const term = `policy-options policy-statement ${OSPF_EXPORT} term ${source}`;
656
- out.set(term, "from protocol", source === "connected" ? "direct" : "static");
657
- out.set(term, "then accept");
658
- }
659
- }
660
-
661
- /** Each relay is a group of servers and the interface it listens on, in the table that interface routes in. */
662
- function dhcpRelay(out: Statements, device: Device, names: Names): void {
663
- for (const [name, relay] of Object.entries(device.dhcp_relay ?? {})) {
664
- const vrf = device.interfaces?.[relay.interface]?.vrf ?? device.ports?.[relay.interface as keyof typeof device.ports]?.vrf;
665
- const relays = vrf ? `routing-instances ${vrf} forwarding-options dhcp-relay` : "forwarding-options dhcp-relay";
666
-
667
- for (const server of relay.servers) out.set(`${relays} server-group ${name}`, server);
668
- out.set(`${relays} group ${name} active-server-group`, name);
669
- out.set(`${relays} group ${name} interface`, names.unit(relay.interface));
670
- }
671
- }
672
-
673
- /** sFlow from the management address, to each collector, on every declared port or those listed. A LAG is observed on its member ports. */
674
- function sflow(out: Statements, device: Device, names: Names): void {
675
- const flow = device.flow_export;
676
- if (flow?.protocol !== "sflow") return;
677
-
678
- if (device.connection.host) out.set("protocols sflow agent-id", device.connection.host);
679
- if (flow.sampling !== undefined) out.set("protocols sflow sample-rate ingress", flow.sampling);
680
-
681
- for (const collector of flow.collectors) {
682
- out.set("protocols sflow collector", collector.address);
683
- if (collector.port !== undefined && collector.port !== 6343) {
684
- out.set("protocols sflow collector", collector.address, "udp-port", collector.port);
685
- }
686
- }
687
-
688
- const declared = Object.entries(device.ports ?? {}).filter(([, settings]) => settings && !settings.lag);
689
- const observed = flow.interfaces ?? declared.map(([port]) => port);
690
-
691
- for (const name of observed) {
692
- for (const port of names.members(name)) if (!port.startsWith("em")) out.set("protocols sflow interfaces", `${port}.0`);
693
- }
694
- }
695
-
696
- /** The size a port's frames may reach for an IP MTU: the Ethernet header, and a VLAN tag when the port switches. */
697
- export const frameSize = (mtu: number, switched: boolean) => mtu + (switched ? 18 : 14);
698
-
699
- /** `2g`, a rate as a policer takes it. */
700
- const bandwidth = (rate: Rate) => rate.toLowerCase();
701
-
702
- /** Bytes a policer lets through at once: 10 ms at its rate, and never less than one full frame. */
703
- function burstOf(rate: Rate): number {
704
- const scale = { k: 1e3, m: 1e6, g: 1e9 }[rate.slice(-1).toLowerCase() as "k" | "m" | "g"];
705
- return Math.max(1500, Math.round((Number(rate.slice(0, -1)) * scale) / 800));
706
- }
707
-
708
- /** Each port or range as the platform writes it: `53`, `1000-2000`. */
709
- const eachPort = (ports: Ports) => [ports].flat().map((port) => (typeof port === "number" ? String(port) : `${port.min}-${port.max}`));
710
-
711
- type AnyAclRule = AclRule<string>;
712
-
713
- /** The family a hardware ACL rule is limited to, by its family or its addresses. */
714
- function aclFamily(rule: AnyAclRule): Family | undefined {
715
- const match = rule.match;
716
- if (match?.family) return match.family;
717
-
718
- const address = match?.src ?? match?.dst;
719
- return address ? familyOf(host(address)) : undefined;
720
- }
721
-
722
- /** The protocol as an IPv6 header names it. */
723
- const nextHeader = (protocol: string) => (protocol === "icmp" ? "icmp6" : protocol);
724
-
725
- /** A rule's term name: its place, then its description so a reader sees it on the device. */
726
- const ruleName = (label: string, description: string | undefined) => quote(description ? `${label} ${description}` : label);
727
-
728
- /**
729
- * Hardware ACLs. A switched port takes a filter of the switching family, which matches IPv4 and IPv6 under `ip-version`; a routed port takes one filter per family. Each ends by accepting what no rule matched.
730
- */
731
- function acls(out: Statements, device: Device, names: Names): void {
732
- const switched = new Set<string>();
733
- const routed = new Set<string>();
734
-
735
- for (const [port, settings] of Object.entries(device.ports ?? {})) {
736
- if (!settings?.acl) continue;
737
-
738
- const physical = names.port(port);
739
-
740
- if (settings.access_vlan || settings.trunk_vlans) {
741
- switched.add(settings.acl);
742
- out.set(`interfaces ${physical} unit 0 family ethernet-switching filter input`, settings.acl);
743
- } else {
744
- routed.add(settings.acl);
745
- out.set(`interfaces ${physical} unit 0 family inet filter input`, `${settings.acl}-v4`);
746
- out.set(`interfaces ${physical} unit 0 family inet6 filter input`, `${settings.acl}-v6`);
747
- }
748
- }
749
-
750
- for (const acl of switched) switchingFilter(out, device, acl);
751
-
752
- for (const acl of routed) {
753
- routedFilter(out, device, acl, "ipv4");
754
- routedFilter(out, device, acl, "ipv6");
755
- }
756
- }
757
-
758
- /** A rule's decision, through a policer when it has a rate. */
759
- function aclThen(out: Statements, term: string, policer: string, rule: AnyAclRule): void {
760
- if (rule.action === "drop") {
761
- out.set(term, "then discard");
762
- return;
763
- }
764
-
765
- if (rule.rate) {
766
- out.set(`firewall policer ${policer} if-exceeding bandwidth-limit`, bandwidth(rule.rate));
767
- out.set(`firewall policer ${policer} if-exceeding burst-size-limit`, burstOf(rule.rate));
768
- out.set(`firewall policer ${policer} then discard`);
769
- out.set(term, "then policer", policer);
770
- }
771
-
772
- out.set(term, "then accept");
773
- }
774
-
775
- function switchingFilter(out: Statements, device: Device, acl: string): void {
776
- const filter = `firewall family ethernet-switching filter ${acl}`;
777
-
778
- (device.acls?.[acl] ?? []).forEach((rule, index) => {
779
- const match = rule.match ?? {};
780
- const addressed = match.src || match.dst || match.protocol || match.src_port !== undefined || match.dst_port !== undefined;
781
- const family = aclFamily(rule);
782
- const families = addressed || family ? (family ? [family] : (["ipv4", "ipv6"] as const)) : [undefined];
783
-
784
- for (const each of families) {
785
- const label = families.length > 1 ? `${index + 1}-${each === "ipv4" ? "v4" : "v6"}` : `${index + 1}`;
786
- const term = `${filter} term ${ruleName(label, rule.description)}`;
787
-
788
- if (each) {
789
- const version = `from ip-version ${each}`;
790
- const v4 = each === "ipv4";
791
-
792
- if (match.src) out.set(term, version, v4 ? "ip-source-address" : "ip6-source-address", match.src);
793
- if (match.dst) out.set(term, version, v4 ? "ip-destination-address" : "ip6-destination-address", match.dst);
794
- if (match.protocol)
795
- out.set(term, version, v4 ? "ip-protocol" : "next-header", v4 ? match.protocol : nextHeader(match.protocol));
796
- for (const port of match.src_port !== undefined ? eachPort(match.src_port) : [])
797
- out.set(term, version, "source-port", port);
798
- for (const port of match.dst_port !== undefined ? eachPort(match.dst_port) : [])
799
- out.set(term, version, "destination-port", port);
800
- if (!addressed) out.set(term, version);
801
- }
802
-
803
- aclThen(out, term, `${acl}-${label}`, rule);
804
- }
805
- });
806
-
807
- out.set(filter, "term circuit-default then accept");
808
- }
809
-
810
- function routedFilter(out: Statements, device: Device, acl: string, family: Family): void {
811
- const name = `${acl}-${family === "ipv4" ? "v4" : "v6"}`;
812
- const filter = `firewall family ${family === "ipv4" ? "inet" : "inet6"} filter ${name}`;
813
-
814
- (device.acls?.[acl] ?? []).forEach((rule, index) => {
815
- const ruleFamily = aclFamily(rule);
816
- if (ruleFamily && ruleFamily !== family) return;
817
-
818
- const match = rule.match ?? {};
819
- const label = `${index + 1}`;
820
- const term = `${filter} term ${ruleName(label, rule.description)}`;
821
- const protocolWord = family === "ipv4" ? "protocol" : "next-header";
822
-
823
- if (match.src) out.set(term, "from source-address", match.src);
824
- if (match.dst) out.set(term, "from destination-address", match.dst);
825
- if (match.protocol) out.set(term, "from", protocolWord, family === "ipv4" ? match.protocol : nextHeader(match.protocol));
826
- for (const port of match.src_port !== undefined ? eachPort(match.src_port) : []) out.set(term, "from source-port", port);
827
- for (const port of match.dst_port !== undefined ? eachPort(match.dst_port) : []) out.set(term, "from destination-port", port);
828
-
829
- aclThen(out, term, `${name}-${label}`, rule);
830
- });
831
-
832
- out.set(filter, "term circuit-default then accept");
833
- }
834
-
835
- /** A DHCP server is an address pool with its network, range, options and reservations, served on its interface. */
836
- function dhcpServers(out: Statements, device: Device, names: Names): void {
837
- for (const [name, server] of Object.entries(device.dhcp ?? {})) {
838
- out.set(`system services dhcp-local-server group ${name} interface`, names.unit(server.interface));
839
-
840
- const pool = `access address-assignment pool ${name} family inet`;
841
- const [low, high] = server.pool;
842
- out.set(pool, "network", server.network);
843
- out.set(pool, `range ${name} low`, low);
844
- out.set(pool, `range ${name} high`, high);
845
- out.set(pool, "dhcp-attributes router", server.gateway);
846
-
847
- for (const resolver of server.dns ?? []) out.set(pool, "dhcp-attributes name-server", resolver);
848
- if (server.lease_time !== undefined) out.set(pool, "dhcp-attributes maximum-lease-time", server.lease_time);
849
-
850
- // A reservation is named by its address.
851
- for (const reservation of server.reservations ?? []) {
852
- const host = `${pool} host ${reservation.address.replace(/\./g, "-")}`;
853
- out.set(host, "hardware-address", reservation.mac.toLowerCase());
854
- out.set(host, "ip-address", reservation.address);
855
- }
856
- }
857
- }
858
-
859
- /** Router advertisements announce each IPv6 network of the interface. */
860
- function routerAdvertisements(out: Statements, device: Device, names: Names): void {
861
- const routed = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})];
862
-
863
- for (const [name, settings] of routed) {
864
- if (!settings?.ipv6_ra) continue;
865
-
866
- const addresses = (settings.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
867
-
868
- for (const address of addresses.filter((address) => familyOf(address) === "ipv6")) {
869
- out.set(`protocols router-advertisement interface ${names.unit(name)} prefix`, network(address));
870
- }
871
- }
872
- }