@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.
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 +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 +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 -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
@@ -1,1490 +0,0 @@
1
- // The neutral model as a RouterOS 7 export, written in the form `/export` prints so the two compare line for line.
2
-
3
- import { X509Certificate } from "node:crypto";
4
- import { familyOf, host, split } from "../../../core/addr";
5
- import { marker } from "../../../core/secrets";
6
- import type {
7
- Device,
8
- Family,
9
- FilterMatch,
10
- Interface,
11
- NatRule,
12
- Neighbor,
13
- NeighborSettings,
14
- PolicyRule,
15
- Port,
16
- Ports,
17
- Service,
18
- Severity,
19
- } from "../../../schema";
20
- import { portsOf, spell } from "../catalog";
21
-
22
- /** A property value as a command takes it. `undefined` and `false` leave the property out. */
23
- export type Value = string | number | undefined | false;
24
-
25
- /** Collects commands under their menu. */
26
- export class Output {
27
- private readonly menus = new Map<string, string[]>();
28
-
29
- /** Add a command to a menu. */
30
- add(menu: string, command: string): void {
31
- const commands = this.menus.get(menu) ?? [];
32
- commands.push(command);
33
- this.menus.set(menu, commands);
34
- }
35
-
36
- /** The whole configuration as text. */
37
- text(): string {
38
- const blocks = [...this.menus].map(([menu, commands]) => [menu, ...commands].join("\n"));
39
- return blocks.join("\n") + "\n";
40
- }
41
- }
42
-
43
- /** Quote a value when it holds anything but plain characters. */
44
- export function quote(value: string): string {
45
- const plain = /^[\w.:/@-]+$/.test(value);
46
- if (plain) return value;
47
- // Inside a quoted string `$` starts a variable, so it is escaped like a quote.
48
- const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$");
49
- return `"${escaped}"`;
50
- }
51
-
52
- /**
53
- * A command: a verb such as `add` or `set [ find name=x ]`, then `property=value` for every property that has a value.
54
- *
55
- * Properties are written in the order given.
56
- */
57
- export function command(verb: string, properties: Record<string, Value>): string {
58
- const words = [verb];
59
-
60
- for (const [name, value] of Object.entries(properties)) {
61
- if (value === undefined || value === false) continue;
62
-
63
- const written = typeof value === "number" ? String(value) : quote(value);
64
- words.push(`${name}=${written}`);
65
- }
66
-
67
- return words.join(" ");
68
- }
69
-
70
- /** `yes` or `no`. */
71
- export const yesNo = (on: boolean | undefined): string => (on ? "yes" : "no");
72
-
73
- /** `V4` or `V6`, the suffix of a chain rendered for one family. */
74
- export const familySuffix = (family: Family): string => (family === "ipv4" ? "V4" : "V6");
75
-
76
- /** Each port or range of ports as the platform writes it: `53`, `0-52`. */
77
- export const eachPort = (ports: Ports): string[] =>
78
- [ports].flat().map((port) => (typeof port === "number" ? String(port) : `${port.min}-${port.max}`));
79
-
80
- /** One port, a range, or a list of them, as `53,0-52`. */
81
- export function portList(ports: Ports | undefined): string | undefined {
82
- if (ports === undefined) return undefined;
83
-
84
- return eachPort(ports).join(",");
85
- }
86
-
87
- /** Seconds as the platform writes a duration: `3600` becomes `1h`, `90` becomes `1m30s`. */
88
- export function duration(seconds: number): string {
89
- const units = [
90
- ["w", 604800],
91
- ["d", 86400],
92
- ["h", 3600],
93
- ["m", 60],
94
- ["s", 1],
95
- ] as const;
96
- let remaining = seconds;
97
- let text = "";
98
-
99
- for (const [unit, size] of units) {
100
- const count = Math.floor(remaining / size);
101
- if (count > 0) text += `${count}${unit}`;
102
- remaining %= size;
103
- }
104
-
105
- return text || "0s";
106
- }
107
-
108
- /** A rate as the switch chip takes it: `2G` becomes `2.0Gbps`. */
109
- export function bitrate(rate: string): string {
110
- const match = /^([\d.]+)([kMG])$/.exec(rate);
111
- if (!match) return rate;
112
-
113
- const [, amount, unit] = match;
114
- const suffix = unit === "k" ? "kbps" : `${unit}bps`;
115
- return `${Number(amount).toFixed(1)}${suffix}`;
116
- }
117
-
118
- /** An IPv4 prefix with a dotted mask, as the switch chip takes it: `10.0.0.0/8` becomes `10.0.0.0/255.0.0.0`. */
119
- export function dottedMask(cidr: string): string {
120
- const { addr, len } = split(cidr);
121
- const mask = len === 0 ? 0 : (0xffffffff << (32 - len)) >>> 0;
122
- const octets = [24, 16, 8, 0].map((shift) => (mask >>> shift) & 255);
123
- return `${addr}/${octets.join(".")}`;
124
- }
125
-
126
- /** A community pattern with `*` as a regular expression the platform matches on: `64500:*:*` becomes `^64500:[0-9]+:[0-9]+$`. */
127
- export function communityPattern(pattern: string): string {
128
- const fields = pattern.split(":").map((field) => (field === "*" ? "[0-9]+" : field));
129
- return `^${fields.join(":")}$`;
130
- }
131
-
132
- /** The bridge every switched port joins. One per device. */
133
- export const BRIDGE = "bridge";
134
-
135
- export type AnyPort = Port<string, string, string, string>;
136
- export type AnyInterface = Interface<string, string>;
137
-
138
- export class Context {
139
- readonly ports: Record<string, AnyPort>;
140
- readonly interfaces: Record<string, AnyInterface>;
141
-
142
- constructor(readonly device: Device) {
143
- this.ports = Object.fromEntries(
144
- Object.entries(device.ports ?? {}).filter((entry): entry is [string, AnyPort] => entry[1] !== undefined)
145
- );
146
- this.interfaces = device.interfaces ?? {};
147
- }
148
-
149
- /** What the platform calls a port or an interface. A port is spelled by the model; an interface keeps its config name. */
150
- name(portOrInterface: string): string {
151
- if (portOrInterface in this.interfaces) return portOrInterface;
152
-
153
- return spell(this.device.platform, this.device.model, portOrInterface);
154
- }
155
-
156
- /** The number of a VLAN, by its config name. */
157
- vlanId(vlan: string): number {
158
- const declared = this.device.vlans?.[vlan];
159
- if (!declared) throw new Error(`${this.device.name}: VLAN ${vlan} is not declared`);
160
- return declared.id;
161
- }
162
-
163
- /** Ports and LAGs that are layer 2 members of a VLAN, with their settings. A port in a LAG is represented by the LAG. */
164
- switched(): [string, AnyPort | Extract<AnyInterface, { type: "lag" }>][] {
165
- const ports = Object.entries(this.ports).filter(([, port]) => !port.lag && (port.access_vlan || port.trunk_vlans));
166
- const lags = Object.entries(this.interfaces).filter((entry): entry is [string, Extract<AnyInterface, { type: "lag" }>] => {
167
- const lag = entry[1];
168
- return lag.type === "lag" && Boolean(lag.access_vlan || lag.trunk_vlans);
169
- });
170
- return [...ports, ...lags];
171
- }
172
-
173
- /** VLAN interfaces, with their settings. */
174
- vlanInterfaces(): [string, Extract<AnyInterface, { type: "vlan" }>][] {
175
- return Object.entries(this.interfaces).filter(
176
- (
177
- entry
178
- ): entry is [
179
- string,
180
- Extract<
181
- AnyInterface,
182
- {
183
- type: "vlan";
184
- }
185
- >,
186
- ] => entry[1].type === "vlan"
187
- );
188
- }
189
-
190
- /** Every port and interface that holds addresses or other layer 3 settings. Ports are named as the platform spells them. */
191
- routed(): { name: string; settings: AnyPort | AnyInterface }[] {
192
- const ports = Object.entries(this.ports).map(([port, settings]) => ({ name: this.name(port), settings }));
193
- const interfaces = Object.entries(this.interfaces).map(([name, settings]) => ({ name, settings }));
194
- return [...ports, ...interfaces];
195
- }
196
- }
197
-
198
- /** Destination of spanning tree BPDUs, so a filter can drop them on one port. */
199
- const BPDU_DESTINATION = "01:80:C2:00:00:00/FF:FF:FF:FF:FF:FF";
200
-
201
- export function renderInterfaces(out: Output, ctx: Context): void {
202
- hardwareOffload(out, ctx);
203
- bridge(out, ctx);
204
- ethernet(out, ctx);
205
- switchPorts(out, ctx);
206
- lags(out, ctx);
207
- vlanInterfaces(out, ctx);
208
- tunnels(out, ctx);
209
- wireguard(out, ctx);
210
- vrrp(out, ctx);
211
- bridgePorts(out, ctx);
212
- bridgeVlans(out, ctx);
213
- bridgeFilters(out, ctx);
214
- interfaceLists(out, ctx);
215
- vrfs(out, ctx);
216
- }
217
-
218
- /**
219
- * Routing in the switch chip. With it on, routed traffic bypasses the forward chain unless its interface opts out.
220
- *
221
- * Restarting the offload after a failure, and offloading IPv6 too, follow the same switch.
222
- */
223
- function hardwareOffload(out: Output, ctx: Context): void {
224
- const on = yesNo(ctx.device.hardware_offload);
225
- out.add("/interface ethernet switch", command("set switch1", { "l3-hw-offloading": on }));
226
- out.add("/interface ethernet switch l3hw-settings", command("set", { autorestart: on, "ipv6-hw": on }));
227
- }
228
-
229
- /** One VLAN-filtering bridge carries every switched port. A loopback is a bridge with no ports, the platform's idiom for one. */
230
- function bridge(out: Output, ctx: Context): void {
231
- const { device } = ctx;
232
- const needsBridge = ctx.switched().length > 0 || ctx.vlanInterfaces().length > 0;
233
-
234
- if (needsBridge) {
235
- const stp = device.stp;
236
- const protocolMode = stp ? (stp.mode ?? "rstp") : "none";
237
- const priority = stp?.priority === undefined ? undefined : `0x${stp.priority.toString(16)}`;
238
- out.add("/interface bridge", command("add", { name: BRIDGE, "protocol-mode": protocolMode, priority, "vlan-filtering": "yes" }));
239
- }
240
-
241
- for (const [name, iface] of Object.entries(ctx.interfaces)) {
242
- if (iface.type !== "loopback") continue;
243
-
244
- out.add("/interface bridge", command("add", { comment: iface.description, name }));
245
- }
246
- }
247
-
248
- type Speed = NonNullable<AnyPort["speed"]>;
249
-
250
- /** The platform names a fixed speed by its medium: an optic in a cage, or copper. */
251
- const MEDIUM: Record<"optical" | "copper", Partial<Record<Speed, string>>> = {
252
- optical: {
253
- "1g": "1G-baseX",
254
- "10g": "10G-baseSR-LR",
255
- "25g": "25G-baseSR-LR",
256
- "40g": "40G-baseSR4-LR4",
257
- "50g": "50G-baseSR2-LR2",
258
- "100g": "100G-baseSR4-LR4",
259
- },
260
- copper: {
261
- "100m": "100M-baseT-full",
262
- "1g": "1G-baseT-full",
263
- },
264
- };
265
-
266
- /** The platform's name for a fixed speed on a port, or undefined when it has none. */
267
- export function mediumOf(platformPortName: string, speed: Speed): string | undefined {
268
- const medium = platformPortName.startsWith("ether") ? "copper" : "optical";
269
- return MEDIUM[medium][speed];
270
- }
271
-
272
- /** Every port of the model is stated. One the config does not declare is disabled. */
273
- function ethernet(out: Output, ctx: Context): void {
274
- const { device } = ctx;
275
-
276
- for (const port of portsOf(device.platform, device.model)) {
277
- const selector = `set [ find default-name=${spell(device.platform, device.model, port)} ]`;
278
- const settings = ctx.ports[port];
279
- if (!settings) {
280
- out.add("/interface ethernet", command(selector, { disabled: "yes" }));
281
- continue;
282
- }
283
-
284
- const fixedSpeed = settings.speed && mediumOf(spell(device.platform, device.model, port), settings.speed);
285
-
286
- out.add(
287
- "/interface ethernet",
288
- command(selector, {
289
- "auto-negotiation": fixedSpeed ? "no" : undefined,
290
- comment: settings.description,
291
- mtu: settings.mtu,
292
- speed: fixedSpeed,
293
- })
294
- );
295
- }
296
- }
297
-
298
- /**
299
- * Storm control in the switch chip. The platform takes one rate, in percent, and which kinds of flooded traffic it limits.
300
- *
301
- * `unsupported()` refuses packets per second and different percentages per kind, so here they are always one shared percent.
302
- */
303
- function switchPorts(out: Output, ctx: Context): void {
304
- for (const [port, settings] of Object.entries(ctx.ports)) {
305
- const storm = settings.storm_control;
306
- if (!storm) continue;
307
-
308
- const thresholds = [storm.broadcast, storm.multicast, storm.unknown_unicast].filter((threshold) => threshold !== undefined);
309
- const percent = thresholds.find((threshold) => "percent" in threshold);
310
- if (!percent || !("percent" in percent)) continue;
311
-
312
- out.add(
313
- "/interface ethernet switch port",
314
- command(`set ${ctx.name(port)}`, {
315
- "limit-broadcasts": yesNo(storm.broadcast !== undefined),
316
- "limit-unknown-multicasts": yesNo(storm.multicast !== undefined),
317
- "limit-unknown-unicasts": yesNo(storm.unknown_unicast !== undefined),
318
- "storm-rate": percent.percent,
319
- })
320
- );
321
- }
322
- }
323
-
324
- /** A LAG is a bonding interface. Its members are named as the platform spells them. */
325
- function lags(out: Output, ctx: Context): void {
326
- for (const [name, lag] of Object.entries(ctx.interfaces)) {
327
- if (lag.type !== "lag") continue;
328
-
329
- const members = Object.entries(ctx.ports)
330
- .filter(([, port]) => port.lag === name)
331
- .map(([port]) => ctx.name(port));
332
- const mode = lag.mode === "static" ? "balance-xor" : "802.3ad";
333
- out.add(
334
- "/interface bonding",
335
- command("add", {
336
- comment: lag.description,
337
- mode,
338
- mtu: lag.mtu,
339
- name,
340
- slaves: members.join(","),
341
- "transmit-hash-policy": "layer-2-and-3",
342
- })
343
- );
344
- }
345
- }
346
-
347
- /** The name of the VLAN interface under one with a MAC of its own, which takes the configured name itself. */
348
- export const underMacvlan = (name: string) => `${name}-vlan`;
349
-
350
- /**
351
- * A VLAN interface sits on the bridge. When the device offloads, one that opts out is routed in software so the firewall applies.
352
- *
353
- * Every VLAN interface on the bridge has the bridge's MAC. One with a MAC of its own is a MACVLAN on top of the VLAN interface: the MACVLAN takes the configured name, so its addresses and every rule naming it land on it.
354
- */
355
- function vlanInterfaces(out: Output, ctx: Context): void {
356
- for (const [name, iface] of ctx.vlanInterfaces()) {
357
- const softwareOnly = ctx.device.hardware_offload && iface.hardware_offload === false;
358
- const vlanName = iface.mac ? underMacvlan(name) : name;
359
-
360
- out.add(
361
- "/interface vlan",
362
- command("add", {
363
- comment: iface.description,
364
- interface: BRIDGE,
365
- "l3-hw-offloading": softwareOnly ? "no" : undefined,
366
- mtu: iface.mtu,
367
- name: vlanName,
368
- "vlan-id": ctx.vlanId(iface.vlan),
369
- })
370
- );
371
-
372
- if (iface.mac) {
373
- out.add(
374
- "/interface macvlan",
375
- command("add", {
376
- comment: iface.description,
377
- interface: vlanName,
378
- "mac-address": iface.mac.toUpperCase(),
379
- mtu: iface.mtu,
380
- name,
381
- })
382
- );
383
- }
384
- }
385
- }
386
-
387
- function tunnels(out: Output, ctx: Context): void {
388
- for (const [name, iface] of Object.entries(ctx.interfaces)) {
389
- if (iface.type === "gre") {
390
- out.add(
391
- "/interface gre",
392
- command("add", {
393
- comment: iface.description,
394
- "local-address": iface.local,
395
- mtu: iface.mtu,
396
- name,
397
- "remote-address": iface.remote,
398
- })
399
- );
400
- }
401
-
402
- if (iface.type === "vxlan") {
403
- // 4789 is the platform's default and is not written.
404
- const port = iface.port && iface.port !== 4789 ? iface.port : undefined;
405
- out.add(
406
- "/interface vxlan",
407
- command("add", {
408
- comment: iface.description,
409
- "local-address": iface.local,
410
- "mac-address": iface.mac,
411
- mtu: iface.mtu,
412
- name,
413
- port,
414
- vni: iface.vni,
415
- })
416
- );
417
- out.add("/interface vxlan vteps", command("add", { interface: name, "remote-ip": iface.remote }));
418
- }
419
- }
420
- }
421
-
422
- function wireguard(out: Output, ctx: Context): void {
423
- for (const [name, iface] of Object.entries(ctx.interfaces)) {
424
- if (iface.type !== "wireguard") continue;
425
-
426
- out.add(
427
- "/interface wireguard",
428
- command("add", {
429
- comment: iface.description,
430
- "listen-port": iface.listen_port,
431
- mtu: iface.mtu,
432
- name,
433
- "private-key": marker(iface.private_key),
434
- })
435
- );
436
-
437
- for (const peer of iface.peers ?? []) {
438
- const endpoint = peer.endpoint ? splitEndpoint(peer.endpoint) : undefined;
439
- out.add(
440
- "/interface wireguard peers",
441
- command("add", {
442
- "allowed-address": peer.allowed_addresses.join(","),
443
- "client-allowed-address": peer.client_allowed_addresses?.join(","),
444
- "endpoint-address": endpoint?.host,
445
- "endpoint-port": endpoint?.port,
446
- interface: name,
447
- // Peer names are unique across the device, so they carry the interface.
448
- name: `${name}-${peer.name}`,
449
- "persistent-keepalive": peer.keepalive ? duration(peer.keepalive) : undefined,
450
- "public-key": peer.public_key,
451
- })
452
- );
453
- }
454
- }
455
- }
456
-
457
- /** `host:port` into its parts. The last colon separates them, so an IPv6 host keeps its own. */
458
- function splitEndpoint(endpoint: string): { host: string; port: string } {
459
- const colon = endpoint.lastIndexOf(":");
460
- return { host: endpoint.slice(0, colon), port: endpoint.slice(colon + 1) };
461
- }
462
-
463
- /** Each VRRP group is an interface of its own, holding the shared address. Version 3 carries IPv6. */
464
- function vrrp(out: Output, ctx: Context): void {
465
- for (const { name, settings } of ctx.routed()) {
466
- for (const group of settings.vrrp ?? []) {
467
- const ipv6 = familyOf(group.address) === "ipv6";
468
- out.add(
469
- "/interface vrrp",
470
- command("add", {
471
- interface: name,
472
- interval: group.interval ? duration(group.interval) : undefined,
473
- name: vrrpName(name, group.id),
474
- "preemption-mode": yesNo(group.preempt),
475
- priority: group.priority,
476
- "v3-protocol": ipv6 ? "ipv6" : undefined,
477
- version: ipv6 ? 3 : undefined,
478
- vrid: group.id,
479
- })
480
- );
481
- }
482
- }
483
- }
484
-
485
- /** The interface holding one VRRP group. */
486
- export const vrrpName = (iface: string, id: number) => `${iface}-vrrp${id}`;
487
-
488
- /** A switched port or LAG joins the bridge. Its untagged VLAN is its port VLAN ID. */
489
- function bridgePorts(out: Output, ctx: Context): void {
490
- for (const [port, settings] of ctx.switched()) {
491
- const isTrunk = Boolean(settings.trunk_vlans);
492
- const untagged = isTrunk ? settings.native_vlan : settings.access_vlan;
493
- const pvid = untagged ? ctx.vlanId(untagged) : undefined;
494
-
495
- // A trunk without a native VLAN takes tagged frames only; an access port untagged only.
496
- let frameTypes: string | undefined;
497
- if (!isTrunk) frameTypes = "admit-only-untagged-and-priority-tagged";
498
- else if (pvid === undefined) frameTypes = "admit-only-vlan-tagged";
499
-
500
- const stp = typeof settings.stp === "object" ? settings.stp : undefined;
501
- out.add(
502
- "/interface bridge port",
503
- command("add", {
504
- bridge: BRIDGE,
505
- edge: stp?.edge ? "yes" : undefined,
506
- "frame-types": frameTypes,
507
- interface: ctx.name(port),
508
- "path-cost": stp?.cost,
509
- priority: stp?.priority === undefined ? undefined : `0x${stp.priority.toString(16)}`,
510
- pvid,
511
- })
512
- );
513
- }
514
- }
515
-
516
- /** A VLAN is tagged on every trunk carrying it, and on the bridge itself when the router has an interface in it. */
517
- function bridgeVlans(out: Output, ctx: Context): void {
518
- for (const [vlanName, vlan] of Object.entries(ctx.device.vlans ?? {})) {
519
- const routedHere = ctx.vlanInterfaces().some(([, iface]) => iface.vlan === vlanName);
520
- const trunks = ctx
521
- .switched()
522
- .filter(([, settings]) => settings.trunk_vlans?.includes(vlanName))
523
- .map(([port]) => ctx.name(port));
524
- const tagged = [...(routedHere ? [BRIDGE] : []), ...trunks];
525
- if (!tagged.length) continue;
526
-
527
- out.add(
528
- "/interface bridge vlan",
529
- command("add", {
530
- bridge: BRIDGE,
531
- comment: vlan.description,
532
- tagged: tagged.join(","),
533
- "vlan-ids": vlan.id,
534
- })
535
- );
536
- }
537
- }
538
-
539
- /** The platform cannot take a port out of spanning tree, so `stp: false` drops the BPDUs it would send and receive. */
540
- function bridgeFilters(out: Output, ctx: Context): void {
541
- for (const [port, settings] of ctx.switched()) {
542
- if (settings.stp !== false) continue;
543
-
544
- const name = ctx.name(port);
545
- const comment = `${name} out of spanning tree`;
546
- out.add(
547
- "/interface bridge filter",
548
- command("add", {
549
- action: "drop",
550
- chain: "input",
551
- comment,
552
- "dst-mac-address": BPDU_DESTINATION,
553
- "in-interface": name,
554
- })
555
- );
556
- out.add(
557
- "/interface bridge filter",
558
- command("add", {
559
- action: "drop",
560
- chain: "output",
561
- comment,
562
- "dst-mac-address": BPDU_DESTINATION,
563
- "out-interface": name,
564
- })
565
- );
566
- }
567
- }
568
-
569
- /**
570
- * Interface lists scope what works at layer 2: neighbor discovery, and the platform's own client over MAC. The IP firewall never sees either.
571
- */
572
- function interfaceLists(out: Output, ctx: Context): void {
573
- const lists = new Map<string, string[]>();
574
-
575
- const lldp = lldpMembers(ctx);
576
- if (lldp) lists.set("lldp", lldp);
577
-
578
- const native = ctx.device.management?.native?.interfaces;
579
- if (native)
580
- lists.set(
581
- "native",
582
- native.map((member) => ctx.name(member))
583
- );
584
-
585
- for (const list of lists.keys()) out.add("/interface list", command("add", { name: list }));
586
- for (const [list, members] of lists)
587
- for (const member of members)
588
- out.add(
589
- "/interface list member",
590
- command("add", {
591
- interface: member,
592
- list,
593
- })
594
- );
595
- }
596
-
597
- /**
598
- * Where LLDP runs, when it is limited to some ports. Undefined when it runs everywhere or nowhere, which the discovery settings say directly.
599
- */
600
- export function lldpMembers(ctx: Context): string[] | undefined {
601
- const lldp = ctx.device.lldp;
602
- if (!lldp) return undefined;
603
-
604
- const overridden = Object.entries(ctx.ports).filter(([, port]) => port.lldp !== undefined);
605
- if (lldp === true && !overridden.length) return undefined;
606
-
607
- const base = lldp === true ? Object.keys(ctx.ports) : [...lldp.interfaces];
608
- const members = new Set(base.map((member) => ctx.name(member)));
609
-
610
- for (const [port, settings] of overridden) {
611
- if (settings.lldp) members.add(ctx.name(port));
612
- else members.delete(ctx.name(port));
613
- }
614
-
615
- return [...members];
616
- }
617
-
618
- /** A VRF lists its interfaces. */
619
- function vrfs(out: Output, ctx: Context): void {
620
- for (const [vrf, settings] of Object.entries(ctx.device.vrfs ?? {})) {
621
- const members = ctx
622
- .routed()
623
- .filter(({ settings: iface }) => iface.vrf === vrf)
624
- .map(({ name }) => name);
625
- out.add("/ip vrf", command("add", { comment: settings.description, interfaces: members.join(","), name: vrf }));
626
- }
627
- }
628
-
629
- /** Address lists holding the prefixes BGP originates. A session reads the one of its family. */
630
- export const NETWORKS: Record<Family, string> = { ipv4: "bgp-networks-v4", ipv6: "bgp-networks-v6" };
631
-
632
- export function renderAddressing(out: Output, ctx: Context): void {
633
- addresses(out, ctx);
634
- dhcp(out, ctx);
635
- dns(out, ctx);
636
- addressLists(out, ctx);
637
- }
638
-
639
- /** Every address goes on the menu of its family, with the interface's description as its comment. */
640
- function addresses(out: Output, ctx: Context): void {
641
- for (const { name, settings } of ctx.routed()) {
642
- for (const entry of settings.addresses ?? []) {
643
- const address = typeof entry === "string" ? entry : entry.address;
644
- // The far end of a point-to-point address, which the platform calls its network.
645
- const peer = typeof entry === "string" ? undefined : entry.peer;
646
-
647
- if (familyOf(address) === "ipv4") {
648
- out.add("/ip address", command("add", { address, comment: settings.description, interface: name, network: peer }));
649
- } else {
650
- out.add(
651
- "/ipv6 address",
652
- command("add", { address, advertise: yesNo(settings.ipv6_ra), comment: settings.description, interface: name })
653
- );
654
- }
655
- }
656
-
657
- // A VRRP address lives on the group's own interface, as a host address.
658
- for (const group of settings.vrrp ?? []) {
659
- const vrrpInterface = vrrpName(name, group.id);
660
- const comment = `VRRP ${group.id} on ${name}`;
661
- if (familyOf(group.address) === "ipv4") {
662
- out.add("/ip address", command("add", { address: `${group.address}/32`, comment, interface: vrrpInterface }));
663
- } else {
664
- out.add(
665
- "/ipv6 address",
666
- command("add", {
667
- address: `${group.address}/128`,
668
- advertise: "no",
669
- comment,
670
- interface: vrrpInterface,
671
- })
672
- );
673
- }
674
- }
675
- }
676
- }
677
-
678
- function dhcp(out: Output, ctx: Context): void {
679
- for (const [name, server] of Object.entries(ctx.device.dhcp ?? {})) {
680
- const [first, last] = server.pool;
681
- // The pool shares the server's name.
682
- out.add("/ip pool", command("add", { name, ranges: `${first}-${last}` }));
683
- out.add(
684
- "/ip dhcp-server",
685
- command("add", {
686
- "address-pool": name,
687
- interface: ctx.name(server.interface),
688
- "lease-time": server.lease_time ? duration(server.lease_time) : undefined,
689
- name,
690
- })
691
- );
692
- out.add(
693
- "/ip dhcp-server network",
694
- command("add", {
695
- address: server.network,
696
- "dns-server": server.dns?.join(","),
697
- gateway: server.gateway,
698
- })
699
- );
700
-
701
- for (const reservation of server.reservations ?? []) {
702
- out.add(
703
- "/ip dhcp-server lease",
704
- command("add", {
705
- address: reservation.address,
706
- comment: reservation.description,
707
- "mac-address": reservation.mac,
708
- server: name,
709
- })
710
- );
711
- }
712
- }
713
-
714
- for (const [name, relay] of Object.entries(ctx.device.dhcp_relay ?? {})) {
715
- out.add("/ip dhcp-relay", command("add", { "dhcp-server": relay.servers.join(","), interface: ctx.name(relay.interface), name }));
716
- }
717
- }
718
-
719
- function dns(out: Output, ctx: Context): void {
720
- const dns = ctx.device.system?.dns;
721
- out.add("/ip dns", command("set", { "allow-remote-requests": yesNo(dns?.serve), servers: dns?.servers.join(",") ?? "" }));
722
- }
723
-
724
- /**
725
- * Address lists come from firewall address sets, prefix sets typed in the config, and what BGP originates.
726
- *
727
- * A prefix set fetched from a registry is filled by `refresh`, so only its name is marked, and the plan leaves its entries alone.
728
- */
729
- function addressLists(out: Output, ctx: Context): void {
730
- const { device } = ctx;
731
- const entries: { list: string; address: string }[] = [];
732
-
733
- for (const [list, members] of Object.entries(device.firewall?.address_sets ?? {})) {
734
- for (const address of members) entries.push({ list, address });
735
- }
736
-
737
- for (const [list, set] of Object.entries(device.prefix_sets ?? {})) {
738
- if ("prefixes" in set) for (const address of set.prefixes) entries.push({ list, address });
739
- else out.add("/ip firewall address-list", `# generated ${list}`);
740
- }
741
-
742
- for (const prefix of device.routing?.bgp?.networks ?? []) {
743
- entries.push({ list: NETWORKS[familyOf(host(prefix))], address: prefix });
744
- }
745
-
746
- for (const family of ["ipv4", "ipv6"] as const) {
747
- const menu = family === "ipv4" ? "/ip firewall address-list" : "/ipv6 firewall address-list";
748
-
749
- for (const { list, address } of entries) {
750
- if (familyOf(host(address)) !== family) continue;
751
-
752
- out.add(menu, command("add", { address, list }));
753
- }
754
- }
755
- }
756
-
757
- /** Helpers the platform has, all stated so that one the config does not list is off. */
758
- const HELPERS = ["ftp", "tftp", "irc", "h323", "sip", "pptp", "rtsp"] as const;
759
-
760
- const CHAINS = ["input", "forward", "output"] as const;
761
-
762
- export function renderFirewall(out: Output, ctx: Context): void {
763
- for (const family of ["ipv4", "ipv6"] as const) {
764
- untracked(out, ctx, family);
765
- filter(out, ctx, family);
766
- nat(out, ctx, family);
767
- }
768
-
769
- helpers(out, ctx);
770
- switchAcls(out, ctx);
771
- }
772
-
773
- /** Untracked prefixes skip connection tracking in the raw table, both ways, before a connection is ever made. */
774
- function untracked(out: Output, ctx: Context, family: Family): void {
775
- const menu = family === "ipv4" ? "/ip firewall raw" : "/ipv6 firewall raw";
776
-
777
- for (const prefix of ctx.device.firewall?.untracked ?? []) {
778
- if (familyOf(prefix) !== family) continue;
779
-
780
- for (const [direction, property] of [
781
- ["to", "dst-address"],
782
- ["from", "src-address"],
783
- ] as const) {
784
- out.add(
785
- menu,
786
- command("add", { action: "notrack", chain: "prerouting", comment: `Untracked ${direction} ${prefix}`, [property]: prefix })
787
- );
788
- }
789
- }
790
- }
791
-
792
- /**
793
- * The families a rule applies to. An address in the rule decides it; otherwise it applies to both.
794
- *
795
- * A rule naming an address set applies only to the families the set has members in.
796
- */
797
- function families(ctx: Context, match: FilterMatch<string, string> | undefined): Family[] {
798
- const sets = ctx.device.firewall?.address_sets ?? {};
799
- const address = match?.src ?? match?.dst;
800
-
801
- let candidates: Family[] = ["ipv4", "ipv6"];
802
- if (match?.family) candidates = [match.family];
803
- else if (address) candidates = [familyOf(address)];
804
-
805
- const setHas = (set: string | undefined, family: Family) => !set || (sets[set] ?? []).some((member) => familyOf(member) === family);
806
- return candidates.filter((family) => setHas(match?.src_set, family) && setHas(match?.dst_set, family));
807
- }
808
-
809
- /** The match properties of a rule, as the platform names them. */
810
- function matchProperties(ctx: Context, match: FilterMatch<string, string> | undefined, family: Family) {
811
- const protocol = match?.protocol === "icmp" && family === "ipv6" ? "icmpv6" : match?.protocol;
812
- const setFlags = match?.tcp_flags?.set ?? [];
813
- const unsetFlags = (match?.tcp_flags?.unset ?? []).map((flag) => `!${flag}`);
814
- const flags = match?.tcp_flags ? [...setFlags, ...unsetFlags].join(",") : undefined;
815
- return {
816
- "connection-state": match?.state?.join(","),
817
- "dst-address": match?.dst,
818
- "dst-address-list": match?.dst_set,
819
- "dst-port": portList(match?.dst_port),
820
- "in-interface": match?.in_interface && ctx.name(match.in_interface),
821
- "out-interface": match?.out_interface && ctx.name(match.out_interface),
822
- protocol,
823
- "src-address": match?.src,
824
- "src-address-list": match?.src_set,
825
- "src-port": portList(match?.src_port),
826
- "tcp-flags": flags,
827
- };
828
- }
829
-
830
- function filter(out: Output, ctx: Context, family: Family): void {
831
- const menu = family === "ipv4" ? "/ip firewall filter" : "/ipv6 firewall filter";
832
-
833
- for (const chain of CHAINS) {
834
- const settings = ctx.device.firewall?.filter?.[chain];
835
- if (!settings) continue;
836
-
837
- for (const rule of settings.rules ?? []) {
838
- if (!families(ctx, rule.match).includes(family)) continue;
839
-
840
- const match = matchProperties(ctx, rule.match, family);
841
- // The platform's fast path takes a flow only on IPv4. The accept below still applies on both.
842
- if (rule.offload && family === "ipv4") {
843
- out.add(menu, command("add", { action: "fasttrack-connection", chain, comment: rule.description, ...match }));
844
- }
845
-
846
- out.add(menu, command("add", { action: rule.action, chain, comment: rule.description, ...match }));
847
- }
848
-
849
- if (settings.default === "drop") out.add(menu, command("add", { action: "drop", chain, comment: "Default" }));
850
- }
851
- }
852
-
853
- function nat(out: Output, ctx: Context, family: Family): void {
854
- const menu = family === "ipv4" ? "/ip firewall nat" : "/ipv6 firewall nat";
855
- const chains: [string, readonly NatRule<string, string>[] | undefined][] = [
856
- ["srcnat", ctx.device.firewall?.nat?.source],
857
- ["dstnat", ctx.device.firewall?.nat?.destination],
858
- ];
859
-
860
- for (const [chain, rules] of chains) {
861
- for (const rule of rules ?? []) {
862
- if (!families(ctx, rule.match).includes(family)) continue;
863
- // A translation to an address belongs to that address's family.
864
- if (rule.action !== "masquerade" && familyOf(rule.to) !== family) continue;
865
-
866
- const action =
867
- rule.action === "masquerade"
868
- ? {
869
- action: "masquerade",
870
- }
871
- : { action: rule.action === "snat" ? "src-nat" : "dst-nat", "to-addresses": rule.to, "to-ports": rule.to_port };
872
- out.add(menu, command("add", { ...action, chain, comment: rule.description, ...matchProperties(ctx, rule.match, family) }));
873
- }
874
- }
875
- }
876
-
877
- function helpers(out: Output, ctx: Context): void {
878
- const enabled = ctx.device.firewall?.helpers ?? [];
879
-
880
- for (const helper of HELPERS) {
881
- out.add("/ip firewall service-port", command(`set ${helper}`, { disabled: yesNo(!enabled.includes(helper)) }));
882
- }
883
- }
884
-
885
- /**
886
- * ACLs run in the switch chip on the port traffic arrives on. The chip matches addresses with a dotted mask on IPv4, drops by forwarding to no port, and limits by rate. A rule takes one port or range, so a list becomes one rule for each.
887
- */
888
- function switchAcls(out: Output, ctx: Context): void {
889
- const each = (ports: Ports | undefined) => (ports === undefined ? [undefined] : eachPort(ports));
890
-
891
- for (const [port, settings] of Object.entries(ctx.ports)) {
892
- if (!settings.acl) continue;
893
-
894
- for (const rule of ctx.device.acls?.[settings.acl] ?? []) {
895
- const match = rule.match ?? {};
896
- const ipv6 = match.family === "ipv6" || [match.src, match.dst].some((address) => address && familyOf(address) === "ipv6");
897
- const address = (prefix: string | undefined) => (prefix && !ipv6 ? dottedMask(prefix) : prefix);
898
-
899
- for (const srcPort of each(match.src_port)) {
900
- for (const dstPort of each(match.dst_port)) {
901
- out.add(
902
- "/interface ethernet switch rule",
903
- command("add", {
904
- comment: rule.description,
905
- [ipv6 ? "dst-address6" : "dst-address"]: address(match.dst),
906
- "dst-port": dstPort,
907
- "mac-protocol": ipv6 ? "ipv6" : "ip",
908
- "new-dst-ports": rule.action === "drop" ? "" : undefined,
909
- ports: ctx.name(port),
910
- protocol: match.protocol,
911
- rate: rule.rate && bitrate(rule.rate),
912
- [ipv6 ? "src-address6" : "src-address"]: address(match.src),
913
- "src-port": srcPort,
914
- switch: "switch1",
915
- "vlan-id": match.vlan && ctx.vlanId(match.vlan),
916
- })
917
- );
918
- }
919
- }
920
- }
921
- }
922
- }
923
-
924
- /** The validator group every RPKI server joins. */
925
- export const RPKI_GROUP = "rpki";
926
-
927
- /** The chain a session without a policy uses, so that it learns or announces nothing. */
928
- export const REJECT_ALL = "REJECT-ALL";
929
-
930
- // RouterOS takes our own role. The config states the neighbor's, so each becomes its opposite: a neighbor that is our provider makes us its customer.
931
- const ROLES = {
932
- provider: "ebgp-customer",
933
- customer: "ebgp-provider",
934
- peer: "ebgp-peer",
935
- rs: "ebgp-rs-client",
936
- "rs-client": "ebgp-rs",
937
- } as const;
938
-
939
- type AnyRule = PolicyRule<string, string>;
940
- type AnyNeighbor = Neighbor<string, string>;
941
-
942
- export function renderRouting(out: Output, ctx: Context): void {
943
- staticRoutes(out, ctx);
944
- rpki(out, ctx);
945
- if (ctx.device.routing?.bgp) {
946
- bgpInstance(out, ctx);
947
- policies(out, ctx);
948
- bgpConnections(out, ctx);
949
- }
950
-
951
- ospf(out, ctx);
952
- bfd(out, ctx);
953
- }
954
-
955
- function staticRoutes(out: Output, ctx: Context): void {
956
- for (const route of ctx.device.routing?.static ?? []) {
957
- const menu = familyOf(host(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
958
- const verb = route.blackhole ? "add blackhole" : "add";
959
- const gateway = route.via ?? (route.interface ? ctx.name(route.interface) : undefined);
960
- out.add(
961
- menu,
962
- command(verb, {
963
- comment: route.description,
964
- distance: route.distance,
965
- "dst-address": route.prefix,
966
- gateway,
967
- "routing-table": route.vrf,
968
- })
969
- );
970
- }
971
- }
972
-
973
- function rpki(out: Output, ctx: Context): void {
974
- for (const server of ctx.device.routing?.rpki ?? []) {
975
- out.add(
976
- "/routing rpki",
977
- command("add", {
978
- address: server.address,
979
- comment: server.description,
980
- group: RPKI_GROUP,
981
- port: server.port,
982
- })
983
- );
984
- }
985
- }
986
-
987
- /** The default instance exists on every device; it is stated in the form the export prints it. */
988
- function bgpInstance(out: Output, ctx: Context): void {
989
- const bgp = ctx.device.routing!.bgp!;
990
- out.add("/routing bgp instance", command("add", { as: bgp.asn, name: "default", "router-id": ctx.device.routing?.router_id }));
991
- }
992
-
993
- /** A neighbor's settings with its group's beneath them. */
994
- function settingsOf(ctx: Context, neighbor: AnyNeighbor): NeighborSettings<string> {
995
- const group = neighbor.group ? ctx.device.routing?.bgp?.groups?.[neighbor.group] : undefined;
996
- return { ...group, ...neighbor };
997
- }
998
-
999
- /** The families a neighbor exchanges: the ones it states, or its address's. */
1000
- function familiesOf(ctx: Context, neighbor: AnyNeighbor): readonly Family[] {
1001
- return settingsOf(ctx, neighbor).families ?? [familyOf(neighbor.address)];
1002
- }
1003
-
1004
- /** The chain a policy is rendered as for one family. A policy can test prefix lengths that mean different things per family, so each family gets its own. */
1005
- const chainName = (policy: string, family: Family) => `${policy}-${familySuffix(family)}`;
1006
-
1007
- /** Does a rule belong on a family's chain? A rule naming a prefix, a family, or a prefix set of one family belongs only there. */
1008
- function appliesTo(ctx: Context, rule: AnyRule, family: Family): boolean {
1009
- const match = rule.match;
1010
- if (match?.family && match.family !== family) return false;
1011
-
1012
- if (match?.prefix && familyOf(host(match.prefix)) !== family) return false;
1013
-
1014
- if (match?.prefix_set) {
1015
- const set = ctx.device.prefix_sets?.[match.prefix_set];
1016
- if (!set) return false;
1017
-
1018
- const firstPrefix = "source" in set ? undefined : set.prefixes[0];
1019
- const setFamily = "source" in set ? set.source.family : firstPrefix && familyOf(host(firstPrefix));
1020
- if (setFamily !== family) return false;
1021
- }
1022
-
1023
- return true;
1024
- }
1025
-
1026
- /** The conditions of a rule, as the platform's filter language writes them. */
1027
- function conditions(rule: AnyRule): string[] {
1028
- const match = rule.match ?? {};
1029
- const tests: string[] = [];
1030
- if (match.prefix) tests.push(`dst==${match.prefix}`);
1031
- if (match.prefix_set) tests.push(`dst in ${match.prefix_set}`);
1032
- if (match.prefix_length?.min !== undefined) tests.push(`dst-len>${match.prefix_length.min - 1}`);
1033
- if (match.prefix_length?.max !== undefined) tests.push(`dst-len<${match.prefix_length.max + 1}`);
1034
- if (match.as_path_length?.min !== undefined) tests.push(`bgp-path-len>${match.as_path_length.min - 1}`);
1035
- if (match.as_path_length?.max !== undefined) tests.push(`bgp-path-len<${match.as_path_length.max + 1}`);
1036
- // The platform's AS path regexp matches each AS as a whole number, so a bare number is that AS anywhere in the path.
1037
- if (match.through_as !== undefined) tests.push(`bgp-as-path ${match.through_as}`);
1038
- if (match.rpki) tests.push(`rpki ${match.rpki}`);
1039
- if (match.community) tests.push(`bgp-communities includes ${match.community}`);
1040
- if (match.large_community) tests.push(`bgp-large-communities includes ${match.large_community}`);
1041
- return tests;
1042
- }
1043
-
1044
- /** What a rule does to a matching route, then its decision. */
1045
- function actions(rule: AnyRule, family: Family): string[] {
1046
- const set = rule.set ?? {};
1047
- const steps: string[] = [];
1048
- if (set.local_pref !== undefined) steps.push(`set bgp-local-pref ${set.local_pref}`);
1049
- if (set.med !== undefined) steps.push(`set bgp-med ${set.med}`);
1050
- if (set.prepend !== undefined) steps.push(`set bgp-path-prepend ${set.prepend}`);
1051
- for (const pattern of set.remove_communities ?? []) steps.push(`delete bgp-communities regexp "${communityPattern(pattern)}"`);
1052
- if (set.communities) steps.push(`set bgp-communities ${set.communities.join(",")}`);
1053
- if (set.add_communities) steps.push(`append bgp-communities ${set.add_communities.join(",")}`);
1054
-
1055
- for (const pattern of set.remove_large_communities ?? []) {
1056
- steps.push(`delete bgp-large-communities regexp "${communityPattern(pattern)}"`);
1057
- }
1058
-
1059
- if (set.large_communities) steps.push(`set bgp-large-communities ${set.large_communities.join(",")}`);
1060
- if (set.add_large_communities) steps.push(`append bgp-large-communities ${set.add_large_communities.join(",")}`);
1061
- // An address only means something to routes of its own family.
1062
- if (set.next_hop && familyOf(set.next_hop) === family) steps.push(`set gw ${set.next_hop}`);
1063
- if (set.preferred_source && familyOf(set.preferred_source) === family) steps.push(`set pref-src ${set.preferred_source}`);
1064
- if (set.blackhole) steps.push("set blackhole yes");
1065
- if (rule.action) steps.push(rule.action);
1066
- return steps;
1067
- }
1068
-
1069
- /** One rule as the platform writes it: hand off, ask the validator, test, change, decide. */
1070
- function ruleText(rule: AnyRule, family: Family): string {
1071
- const parts: string[] = [];
1072
- if (rule.call) parts.push(`jump ${chainName(rule.call, family)}`);
1073
- // An RPKI state is only known once the validator has been asked, per route, so the question goes on the same rule.
1074
- if (rule.match?.rpki) parts.push(`rpki-verify ${RPKI_GROUP}`);
1075
-
1076
- const tests = conditions(rule);
1077
- const steps = actions(rule, family);
1078
- if (tests.length) parts.push(`if (${tests.join(" && ")})${steps.length ? ` { ${steps.join("; ")} }` : ""}`);
1079
- else parts.push(...steps);
1080
- return parts.join("; ");
1081
- }
1082
-
1083
- /**
1084
- * Policies become filter chains, one per family a session uses, plus every chain those call.
1085
- *
1086
- * A chain a session uses directly ends in a reject when its last rule does not decide, so a route nothing accepted is rejected on any platform default. A chain only called returns to its caller instead.
1087
- */
1088
- function policies(out: Output, ctx: Context): void {
1089
- const bgp = ctx.device.routing!.bgp!;
1090
- const policies = ctx.device.policies ?? {};
1091
-
1092
- const wanted = new Map<string, Set<Family>>();
1093
- const want = (policy: string, family: Family) => {
1094
- const families = wanted.get(policy) ?? new Set();
1095
- if (families.has(family)) return;
1096
-
1097
- families.add(family);
1098
- wanted.set(policy, families);
1099
- for (const rule of policies[policy] ?? []) if (rule.call && appliesTo(ctx, rule, family)) want(rule.call, family);
1100
- };
1101
-
1102
- let rejectAllNeeded = false;
1103
-
1104
- for (const neighbor of Object.values(bgp.neighbors ?? {})) {
1105
- const settings = settingsOf(ctx, neighbor);
1106
-
1107
- for (const family of familiesOf(ctx, neighbor)) {
1108
- for (const policy of [settings.import, settings.export]) {
1109
- if (policy) want(policy, family);
1110
- else rejectAllNeeded = true;
1111
- }
1112
- }
1113
- }
1114
-
1115
- const called = new Set(Object.values(policies).flatMap((rules) => rules.flatMap((rule) => (rule.call ? [rule.call] : []))));
1116
-
1117
- for (const policy of [...wanted.keys()].sort()) {
1118
- for (const family of ["ipv4", "ipv6"] as const) {
1119
- if (!wanted.get(policy)!.has(family)) continue;
1120
-
1121
- const chain = chainName(policy, family);
1122
- const rules = (policies[policy] ?? []).filter((rule) => appliesTo(ctx, rule, family));
1123
-
1124
- for (const rule of rules) {
1125
- const text = ruleText(rule, family);
1126
- if (text) out.add("/routing filter rule", command("add", { chain, comment: rule.description, rule: text }));
1127
- }
1128
-
1129
- const last = rules.at(-1);
1130
- const decides = last !== undefined && !last.match && !last.call && last.action !== undefined;
1131
- if (!called.has(policy) && !decides) out.add("/routing filter rule", command("add", { chain, rule: "reject" }));
1132
- }
1133
- }
1134
-
1135
- if (rejectAllNeeded) {
1136
- for (const family of ["ipv4", "ipv6"] as const)
1137
- out.add(
1138
- "/routing filter rule",
1139
- command("add", {
1140
- chain: chainName(REJECT_ALL, family),
1141
- rule: "reject",
1142
- })
1143
- );
1144
- }
1145
- }
1146
-
1147
- /** One connection per neighbor. The platform carries one family per connection. */
1148
- function bgpConnections(out: Output, ctx: Context): void {
1149
- const bgp = ctx.device.routing!.bgp!;
1150
-
1151
- for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
1152
- const settings = settingsOf(ctx, neighbor);
1153
- const [family] = familiesOf(ctx, neighbor) as [Family];
1154
- const addressFamily = familyOf(neighbor.address);
1155
- const internal = settings.remote_as === bgp.asn;
1156
- const originates = bgp.networks?.some((prefix) => familyOf(host(prefix)) === family);
1157
- const role = settings.role ? ROLES[settings.role] : internal ? "ibgp" : "ebgp";
1158
-
1159
- out.add(
1160
- "/routing bgp connection",
1161
- command("add", {
1162
- // Only stated when the routes differ in family from the address they are carried over.
1163
- afi: addressFamily === family ? undefined : family === "ipv6" ? "ipv6" : "ip",
1164
- as: bgp.asn,
1165
- connect: settings.passive ? "no" : undefined,
1166
- "hold-time": settings.hold_time ? duration(settings.hold_time) : undefined,
1167
- "input.filter": chainName(settings.import ?? REJECT_ALL, family),
1168
- // The one instance bgpInstance adds. 7.23 refuses a new connection without it.
1169
- instance: "default",
1170
- [`input.limit-process-routes-${family}`]: settings.max_prefixes,
1171
- "keepalive-time": settings.keepalive ? duration(settings.keepalive) : undefined,
1172
- listen: settings.passive ? "yes" : undefined,
1173
- "local.address": settings.local_address,
1174
- "local.role": role,
1175
- multihop: settings.multihop ? "yes" : undefined,
1176
- name,
1177
- "output.filter-chain": chainName(settings.export ?? REJECT_ALL, family),
1178
- "output.network": originates ? NETWORKS[family] : undefined,
1179
- "remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
1180
- "remote.as": settings.remote_as,
1181
- "tcp-md5-key": settings.password && marker(settings.password),
1182
- "use-bfd": settings.bfd ? "yes" : undefined,
1183
- })
1184
- );
1185
- }
1186
- }
1187
-
1188
- /** One instance per OSPF version, an area per area and instance, and a template per interface. */
1189
- function ospf(out: Output, ctx: Context): void {
1190
- const ospf = ctx.device.routing?.ospf;
1191
- if (!ospf) return;
1192
-
1193
- for (const family of ospf.families ?? ["ipv4"]) {
1194
- const version = family === "ipv4" ? 2 : 3;
1195
- const instance = `ospf-v${version}`;
1196
- out.add(
1197
- "/routing ospf instance",
1198
- command("add", {
1199
- name: instance,
1200
- redistribute: ospf.redistribute?.join(","),
1201
- "router-id": ctx.device.routing?.router_id,
1202
- version,
1203
- })
1204
- );
1205
-
1206
- for (const [areaId, area] of Object.entries(ospf.areas)) {
1207
- const areaName = `${instance}-${areaId}`;
1208
- out.add("/routing ospf area", command("add", { "area-id": areaId, instance, name: areaName }));
1209
-
1210
- for (const [iface, settings] of Object.entries(area.interfaces)) {
1211
- // `passive` is a flag, written without a value.
1212
- const verb = settings?.passive ? "add passive" : "add";
1213
- out.add(
1214
- "/routing ospf interface-template",
1215
- command(verb, {
1216
- area: areaName,
1217
- cost: settings?.cost,
1218
- interfaces: ctx.name(iface),
1219
- type: settings?.network === "point-to-point" ? "ptp" : undefined,
1220
- "use-bfd": settings?.bfd ? "yes" : undefined,
1221
- })
1222
- );
1223
- }
1224
- }
1225
- }
1226
- }
1227
-
1228
- /** BFD runs where a session or an OSPF interface asks for it. One configuration covers every interface. */
1229
- function bfd(out: Output, ctx: Context): void {
1230
- const routing = ctx.device.routing;
1231
- const bgpWants = Object.values(routing?.bgp?.neighbors ?? {}).some((neighbor) => settingsOf(ctx, neighbor).bfd);
1232
- const ospfWants = Object.values(routing?.ospf?.areas ?? {}).some((area) => Object.values(area.interfaces).some((iface) => iface?.bfd));
1233
- if (bgpWants || ospfWants) out.add("/routing bfd configuration", command("add", { interfaces: "all" }));
1234
- }
1235
-
1236
- /** Neutral service name to the platform's. `native` is the platform's own desktop client. */
1237
- export const SERVICES = {
1238
- ftp: "ftp",
1239
- ssh: "ssh",
1240
- telnet: "telnet",
1241
- http: "www",
1242
- https: "www-ssl",
1243
- api: "api",
1244
- api_tls: "api-ssl",
1245
- native: "winbox",
1246
- } as const;
1247
-
1248
- const USER_GROUPS = { admin: "full", operator: "write", "read-only": "read" } as const;
1249
-
1250
- const SNMP_AUTH = { sha1: "SHA1", sha256: "SHA1" } as const;
1251
- const SNMP_PRIVACY = { aes128: "AES", des: "DES" } as const;
1252
-
1253
- export function renderManagement(out: Output, ctx: Context): void {
1254
- services(out, ctx);
1255
- layerTwo(out, ctx);
1256
- snmp(out, ctx);
1257
- users(out, ctx);
1258
- certificates(out, ctx);
1259
- }
1260
-
1261
- /** Every service the platform has is stated. One the config does not name is disabled. */
1262
- function services(out: Output, ctx: Context): void {
1263
- const management = ctx.device.management ?? {};
1264
-
1265
- for (const [neutral, platform] of Object.entries(SERVICES)) {
1266
- const service = management[neutral as keyof typeof SERVICES] as (Service & { certificate?: string }) | undefined;
1267
- if (!service) {
1268
- out.add("/ip service", command(`set ${platform}`, { disabled: "yes" }));
1269
- continue;
1270
- }
1271
-
1272
- const allow = service.allow ?? management.allow ?? [];
1273
- out.add(
1274
- "/ip service",
1275
- command(`set ${platform}`, {
1276
- address: allow.join(","),
1277
- certificate: service.certificate,
1278
- disabled: "no",
1279
- port: service.port,
1280
- })
1281
- );
1282
- }
1283
-
1284
- // A newer service with no neutral name. Off, as everything not stated is.
1285
- out.add("/ip service", command("set reverse-proxy", { disabled: "yes" }));
1286
- out.add("/ip ssh", command("set", { "strong-crypto": yesNo(!management.ssh?.weak_crypto) }));
1287
- }
1288
-
1289
- /** What works at layer 2: the platform's client over MAC, MAC ping, the bandwidth test server and neighbor discovery. */
1290
- function layerTwo(out: Output, ctx: Context): void {
1291
- const nativeList = ctx.device.management?.native?.interfaces ? "native" : "none";
1292
- out.add("/tool mac-server", command("set", { "allowed-interface-list": nativeList }));
1293
- out.add("/tool mac-server mac-winbox", command("set", { "allowed-interface-list": nativeList }));
1294
- out.add("/tool mac-server ping", command("set", { enabled: "no" }));
1295
- out.add("/tool bandwidth-server", command("set", { enabled: "no" }));
1296
-
1297
- let discovery = "none";
1298
- if (lldpMembers(ctx)) discovery = "lldp";
1299
- else if (ctx.device.lldp) discovery = "all";
1300
- out.add("/ip neighbor discovery-settings", command("set", { "discover-interface-list": discovery, protocol: "lldp" }));
1301
- }
1302
-
1303
- /**
1304
- * The platform keeps one community it will not delete. With version 2c it becomes the read community; without, it is disabled. Version 3 users are communities of their own.
1305
- */
1306
- function snmp(out: Output, ctx: Context): void {
1307
- const management = ctx.device.management ?? {};
1308
- const snmp = management.snmp;
1309
-
1310
- if (!snmp) {
1311
- out.add("/snmp", command("set", { enabled: "no" }));
1312
- out.add("/snmp community", command("set [ find default=yes ]", { addresses: "::/0", name: "public" }));
1313
- return;
1314
- }
1315
-
1316
- const allow = (snmp.allow ?? management.allow ?? []).join(",");
1317
- out.add("/snmp", command("set", { contact: snmp.contact ?? "", enabled: "yes", location: snmp.location ?? "" }));
1318
-
1319
- if (snmp.community) out.add("/snmp community", command("set [ find default=yes ]", { addresses: allow, name: marker(snmp.community) }));
1320
- else out.add("/snmp community", command("set [ find default=yes ]", { addresses: "::/0", disabled: "yes", name: "public" }));
1321
-
1322
- for (const [name, user] of Object.entries(snmp.users ?? {})) {
1323
- out.add(
1324
- "/snmp community",
1325
- command("add", {
1326
- addresses: allow,
1327
- "authentication-password": marker(user.auth_password),
1328
- "authentication-protocol": SNMP_AUTH[user.auth],
1329
- "encryption-password": marker(user.privacy_password),
1330
- "encryption-protocol": SNMP_PRIVACY[user.privacy],
1331
- name,
1332
- security: "private",
1333
- })
1334
- );
1335
- }
1336
- }
1337
-
1338
- function users(out: Output, ctx: Context): void {
1339
- for (const [name, user] of Object.entries(ctx.device.users ?? {})) {
1340
- out.add(
1341
- "/user",
1342
- command("add", {
1343
- comment: user.description,
1344
- group: USER_GROUPS[user.role],
1345
- name,
1346
- password: marker(user.password),
1347
- })
1348
- );
1349
- for (const key of user.ssh_keys ?? []) out.add("/user ssh-keys", command("add", { key, user: name }));
1350
- }
1351
- }
1352
-
1353
- /**
1354
- * Certificates are not part of an export, so the reader lists them in this same form: a name, the SHA-256 fingerprint of the certificate, and whether the device holds its private key. A difference is installed from the config, not edited.
1355
- */
1356
- function certificates(out: Output, ctx: Context): void {
1357
- for (const [name, certificate] of Object.entries(ctx.device.certificates ?? {})) {
1358
- out.add(
1359
- "/certificate",
1360
- command("add", {
1361
- fingerprint: fingerprint(certificate.certificate),
1362
- name,
1363
- "private-key": yesNo(Boolean(certificate.private_key)),
1364
- })
1365
- );
1366
- }
1367
- }
1368
-
1369
- /** SHA-256 of the certificate's DER encoding, lowercase hex, as the platform prints it. */
1370
- export function fingerprint(pem: string): string {
1371
- return new X509Certificate(pem).fingerprint256.replace(/:/g, "").toLowerCase();
1372
- }
1373
-
1374
- /** Severity threshold to the topics the platform logs under, most severe first. */
1375
- const TOPICS: [Severity, string][] = [
1376
- ["critical", "critical"],
1377
- ["error", "error"],
1378
- ["warning", "warning"],
1379
- ["info", "info"],
1380
- ["debug", "debug"],
1381
- ];
1382
- const SEVERITY_ORDER: Severity[] = ["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"];
1383
-
1384
- /** Every topic at or above a severity. Emergency and alert are logged as critical; the platform has no notice topic. */
1385
- function topicsFrom(level: Severity): string[] {
1386
- // Emergency and alert fall back to critical, the most severe topic the platform has.
1387
- const threshold = Math.max(SEVERITY_ORDER.indexOf(level), SEVERITY_ORDER.indexOf("critical"));
1388
-
1389
- return TOPICS.filter(([severity]) => SEVERITY_ORDER.indexOf(severity) <= threshold).map(([, topic]) => topic);
1390
- }
1391
-
1392
- export function renderSystem(out: Output, ctx: Context): void {
1393
- const system = ctx.device.system ?? {};
1394
-
1395
- out.add(
1396
- "/ip settings",
1397
- command("set", {
1398
- "send-redirects": yesNo(system.ip?.icmp_redirects),
1399
- "tcp-syncookies": yesNo(system.ip?.syn_cookies),
1400
- })
1401
- );
1402
- out.add("/system identity", command("set", { name: ctx.device.name }));
1403
-
1404
- // Autodetect wins after a restart and has moved a router to another zone, so it is always off.
1405
- out.add("/system clock", command("set", { "time-zone-autodetect": "no", "time-zone-name": system.timezone ?? "UTC" }));
1406
- out.add("/system note", command("set", { note: system.banner ?? "", "show-at-login": "yes" }));
1407
-
1408
- out.add("/system ntp client", command("set", { enabled: yesNo(Boolean(system.ntp?.servers.length)) }));
1409
- out.add("/system ntp server", command("set", { enabled: yesNo(system.ntp?.serve) }));
1410
- for (const server of system.ntp?.servers ?? []) out.add("/system ntp client servers", command("add", { address: server }));
1411
-
1412
- logging(out, ctx);
1413
-
1414
- out.add("/system package update", command("set", { channel: system.release_channel ?? "stable" }));
1415
- // Bootloader setup only from a deliberate key on the console, so line noise on a serial cable cannot enter it.
1416
- out.add("/system routerboard settings", command("set", { "enter-setup-on": "delete-key" }));
1417
-
1418
- flowExport(out, ctx);
1419
- }
1420
-
1421
- /** Local logging uses the platform's built-in disk action, number 1. Each remote collector gets an action of its own. */
1422
- function logging(out: Output, ctx: Context): void {
1423
- const logging = ctx.device.system?.logging;
1424
- const local = logging?.local;
1425
-
1426
- out.add(
1427
- "/system logging action",
1428
- command("set 1", {
1429
- "disk-file-count": local?.files ?? 2,
1430
- "disk-lines-per-file": local?.lines_per_file ?? 1000,
1431
- })
1432
- );
1433
- for (const topic of local ? topicsFrom(local.level) : []) out.add("/system logging", command("add", { action: "disk", topics: topic }));
1434
-
1435
- (logging?.remote ?? []).forEach((collector, index) => {
1436
- const action = `remote${index + 1}`;
1437
- out.add(
1438
- "/system logging action",
1439
- command("add", {
1440
- name: action,
1441
- remote: collector.address,
1442
- "remote-port": collector.port,
1443
- target: "remote",
1444
- })
1445
- );
1446
- for (const topic of topicsFrom(collector.level)) out.add("/system logging", command("add", { action, topics: topic }));
1447
- });
1448
- }
1449
-
1450
- /** NetFlow version 9 and IPFIX through traffic flow. The platform has no sFlow, which `unsupported()` reports. */
1451
- function flowExport(out: Output, ctx: Context): void {
1452
- const flow = ctx.device.flow_export;
1453
- if (!flow) {
1454
- out.add("/ip traffic-flow", command("set", { enabled: "no" }));
1455
- return;
1456
- }
1457
-
1458
- const sampled = flow.sampling !== undefined && flow.sampling > 1;
1459
- const interfaces = flow.interfaces?.map((iface) => ctx.name(iface)).join(",") ?? "all";
1460
- out.add(
1461
- "/ip traffic-flow",
1462
- command("set", {
1463
- enabled: "yes",
1464
- interfaces,
1465
- "packet-sampling": yesNo(sampled),
1466
- "sampling-interval": sampled ? flow.sampling : undefined,
1467
- "sampling-space": sampled ? 1 : undefined,
1468
- })
1469
- );
1470
-
1471
- const version = flow.protocol === "ipfix" ? "ipfix" : "9";
1472
-
1473
- for (const collector of flow.collectors) {
1474
- out.add("/ip traffic-flow target", command("add", { "dst-address": collector.address, port: collector.port ?? 2055, version }));
1475
- }
1476
- }
1477
-
1478
- export function render(device: Device): string {
1479
- const out = new Output();
1480
- const ctx = new Context(device);
1481
-
1482
- renderInterfaces(out, ctx);
1483
- renderAddressing(out, ctx);
1484
- renderFirewall(out, ctx);
1485
- renderRouting(out, ctx);
1486
- renderManagement(out, ctx);
1487
- renderSystem(out, ctx);
1488
-
1489
- return out.text();
1490
- }