@takodotid/circuit 0.43.1 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/README.md +115 -37
  2. package/package.json +21 -58
  3. package/src/checks/across.ts +402 -0
  4. package/src/checks/built-in.ts +256 -0
  5. package/src/checks/device.ts +436 -0
  6. package/src/checks/index.ts +53 -0
  7. package/src/cli/advisories.ts +108 -0
  8. package/src/cli/app.ts +71 -0
  9. package/src/cli/editor-types.generated.ts +30 -0
  10. package/src/cli/editor-types.ts +69 -0
  11. package/src/cli/format.ts +124 -0
  12. package/src/cli/git-hooks.ts +79 -0
  13. package/src/cli/index.ts +350 -1170
  14. package/src/cli/migrate.ts +554 -0
  15. package/src/cli/project-commands.ts +656 -0
  16. package/src/cli/releases.ts +374 -0
  17. package/src/cli/remember.ts +339 -0
  18. package/src/cli/rename.ts +27 -33
  19. package/src/cli/secrets-password.ts +60 -0
  20. package/src/cli/secrets.ts +258 -0
  21. package/src/cli/setup.ts +300 -0
  22. package/src/cli/signing.ts +23 -0
  23. package/src/cli/text-editor.ts +268 -0
  24. package/src/cli/ui.ts +298 -0
  25. package/src/cli/version.ts +2 -0
  26. package/src/cli/versions.ts +158 -0
  27. package/src/config/address.ts +102 -0
  28. package/src/config/circuit.d.ts +30 -0
  29. package/src/config/common.ts +65 -0
  30. package/src/config/communities.ts +479 -0
  31. package/src/config/define.ts +684 -0
  32. package/src/config/device.ts +164 -0
  33. package/src/config/firewall.ts +150 -0
  34. package/src/config/index.ts +22 -0
  35. package/src/{schema → config}/interfaces.ts +121 -85
  36. package/src/config/keys.ts +434 -0
  37. package/src/config/management.ts +61 -0
  38. package/src/config/merge.ts +335 -0
  39. package/src/config/models.ts +70 -0
  40. package/src/config/monitoring.ts +64 -0
  41. package/src/config/network.ts +180 -0
  42. package/src/config/presets.ts +149 -0
  43. package/src/config/routing.ts +401 -0
  44. package/src/config/services.ts +51 -0
  45. package/src/config/switching.ts +84 -0
  46. package/src/config/system.ts +103 -0
  47. package/src/{schema → config}/time-zones.ts +1 -1
  48. package/src/core/address.ts +1 -1
  49. package/src/core/cabling.ts +8 -43
  50. package/src/core/graph.ts +40 -0
  51. package/src/{integrations → core}/registries.ts +5 -3
  52. package/src/core/secrets-file.ts +539 -0
  53. package/src/core/secrets.ts +83 -64
  54. package/src/core/topology.ts +103 -146
  55. package/src/core/utils.ts +12 -1
  56. package/src/model.ts +83 -0
  57. package/src/platforms/derive.ts +64 -37
  58. package/src/platforms/finding.ts +9 -0
  59. package/src/platforms/fortios/firewall.ts +76 -64
  60. package/src/platforms/fortios/index.ts +189 -125
  61. package/src/platforms/fortios/names.ts +51 -18
  62. package/src/platforms/fortios/render.ts +69 -62
  63. package/src/platforms/fortios/routing.ts +89 -58
  64. package/src/platforms/fortios/vdoms.ts +47 -16
  65. package/src/platforms/junos/TRAPS.md +2 -2
  66. package/src/platforms/junos/bgp.ts +63 -42
  67. package/src/platforms/junos/index.ts +122 -89
  68. package/src/platforms/junos/render.ts +139 -104
  69. package/src/platforms/platform.ts +10 -8
  70. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  71. package/src/platforms/raisecom-ros/index.ts +89 -70
  72. package/src/platforms/raisecom-ros/render.ts +49 -45
  73. package/src/platforms/routeros/TRAPS.md +3 -3
  74. package/src/platforms/routeros/index.ts +121 -60
  75. package/src/platforms/routeros/render.ts +214 -195
  76. package/src/platforms/vrp/TRAPS.md +1 -1
  77. package/src/platforms/vrp/index.ts +74 -50
  78. package/src/platforms/vrp/render.ts +67 -56
  79. package/src/project/project.ts +725 -0
  80. package/src/studio/app/env.d.ts +9 -0
  81. package/src/studio/app/index.html +18 -0
  82. package/src/studio/app/src/App.vue +95 -0
  83. package/src/studio/app/src/components/AddressesView.vue +38 -0
  84. package/src/studio/app/src/components/CablesView.vue +29 -0
  85. package/src/studio/app/src/components/CompareView.vue +160 -0
  86. package/src/studio/app/src/components/DataTable.vue +49 -0
  87. package/src/studio/app/src/components/DeviceView.vue +237 -0
  88. package/src/studio/app/src/components/DevicesView.vue +32 -0
  89. package/src/studio/app/src/components/Faceplate.vue +75 -0
  90. package/src/studio/app/src/components/GraphView.vue +282 -0
  91. package/src/studio/app/src/components/LineDiff.vue +24 -0
  92. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  93. package/src/studio/app/src/components/SearchView.vue +72 -0
  94. package/src/studio/app/src/components/SecretsView.vue +112 -0
  95. package/src/studio/app/src/components/SessionsView.vue +35 -0
  96. package/src/studio/app/src/components/Settings.vue +95 -0
  97. package/src/studio/app/src/components/ToolsView.vue +118 -0
  98. package/src/studio/app/src/components/VlansView.vue +50 -0
  99. package/src/studio/app/src/lib/csv.ts +19 -0
  100. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  101. package/src/studio/app/src/lib/format.ts +38 -0
  102. package/src/studio/app/src/lib/net.ts +68 -0
  103. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  104. package/src/studio/app/src/lib/store.ts +71 -0
  105. package/src/studio/app/src/lib/tools.ts +217 -0
  106. package/src/studio/app/src/main.ts +9 -0
  107. package/src/studio/{public → app/src}/style.css +14 -4
  108. package/src/studio/app/tsconfig.json +17 -0
  109. package/src/studio/app/vite.config.ts +16 -0
  110. package/src/studio/assets.generated.ts +3 -0
  111. package/src/studio/data.ts +96 -188
  112. package/src/studio/git.ts +27 -43
  113. package/src/studio/server.ts +196 -107
  114. package/src/studio/types.ts +141 -0
  115. package/src/transport/console.ts +3 -3
  116. package/src/transport/ssh.ts +1 -1
  117. package/dist/chunks/index-3b343xva.js +0 -177
  118. package/dist/chunks/index-6wb41q67.js +0 -62
  119. package/dist/chunks/index-n7w3tg5y.js +0 -149
  120. package/dist/chunks/index-yzw86rc8.js +0 -921
  121. package/dist/chunks/new-w3zys5y4.js +0 -172
  122. package/dist/chunks/server-cxr9ynxc.js +0 -259
  123. package/dist/cli/index.js +0 -12922
  124. package/dist/helpers/index.js +0 -881
  125. package/dist/index.js +0 -28
  126. package/dist/types/core/address-space.d.ts +0 -62
  127. package/dist/types/core/address.d.ts +0 -44
  128. package/dist/types/core/cabling.d.ts +0 -41
  129. package/dist/types/core/cross-checks.d.ts +0 -28
  130. package/dist/types/core/define.d.ts +0 -79
  131. package/dist/types/core/project-files.d.ts +0 -12
  132. package/dist/types/core/schema-docs.d.ts +0 -25
  133. package/dist/types/core/secrets.d.ts +0 -62
  134. package/dist/types/core/topology.d.ts +0 -96
  135. package/dist/types/core/utils.d.ts +0 -10
  136. package/dist/types/core/validator.d.ts +0 -23
  137. package/dist/types/helpers/bgp.d.ts +0 -195
  138. package/dist/types/helpers/cables.d.ts +0 -69
  139. package/dist/types/helpers/checks.d.ts +0 -36
  140. package/dist/types/helpers/exchange.d.ts +0 -55
  141. package/dist/types/helpers/filters.d.ts +0 -51
  142. package/dist/types/helpers/index.d.ts +0 -14
  143. package/dist/types/helpers/route-server.d.ts +0 -52
  144. package/dist/types/helpers/upstreams.d.ts +0 -192
  145. package/dist/types/index.d.ts +0 -10
  146. package/dist/types/integrations/librenms.d.ts +0 -60
  147. package/dist/types/integrations/netbox.d.ts +0 -89
  148. package/dist/types/integrations/peeringdb.d.ts +0 -24
  149. package/dist/types/integrations/prometheus.d.ts +0 -24
  150. package/dist/types/integrations/registries.d.ts +0 -8
  151. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  152. package/dist/types/platforms/catalog.d.ts +0 -109
  153. package/dist/types/platforms/derive.d.ts +0 -17
  154. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  155. package/dist/types/platforms/fortios/index.d.ts +0 -140
  156. package/dist/types/platforms/fortios/models.d.ts +0 -14
  157. package/dist/types/platforms/fortios/names.d.ts +0 -52
  158. package/dist/types/platforms/fortios/plan.d.ts +0 -114
  159. package/dist/types/platforms/fortios/render.d.ts +0 -77
  160. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  161. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  162. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  163. package/dist/types/platforms/index.d.ts +0 -13
  164. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  165. package/dist/types/platforms/junos/index.d.ts +0 -114
  166. package/dist/types/platforms/junos/models.d.ts +0 -14
  167. package/dist/types/platforms/junos/render.d.ts +0 -154
  168. package/dist/types/platforms/line-diff.d.ts +0 -67
  169. package/dist/types/platforms/platform.d.ts +0 -135
  170. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  171. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  172. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  173. package/dist/types/platforms/routeros/export.d.ts +0 -59
  174. package/dist/types/platforms/routeros/index.d.ts +0 -116
  175. package/dist/types/platforms/routeros/models.d.ts +0 -18
  176. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  177. package/dist/types/platforms/routeros/render.d.ts +0 -250
  178. package/dist/types/platforms/vrp/index.d.ts +0 -83
  179. package/dist/types/platforms/vrp/models.d.ts +0 -12
  180. package/dist/types/platforms/vrp/render.d.ts +0 -96
  181. package/dist/types/schema/common.d.ts +0 -30
  182. package/dist/types/schema/device.d.ts +0 -126
  183. package/dist/types/schema/firewall.d.ts +0 -131
  184. package/dist/types/schema/index.d.ts +0 -8
  185. package/dist/types/schema/interfaces.d.ts +0 -205
  186. package/dist/types/schema/policy.d.ts +0 -107
  187. package/dist/types/schema/routing.d.ts +0 -161
  188. package/dist/types/schema/system.d.ts +0 -198
  189. package/dist/types/schema/time-zones.d.ts +0 -2
  190. package/dist/types/transport/console.d.ts +0 -115
  191. package/dist/types/transport/path-check.d.ts +0 -32
  192. package/dist/types/transport/ssh.d.ts +0 -27
  193. package/src/cli/graph-format.ts +0 -91
  194. package/src/cli/new.ts +0 -234
  195. package/src/core/address-space.ts +0 -320
  196. package/src/core/cross-checks.ts +0 -369
  197. package/src/core/define.ts +0 -124
  198. package/src/core/project-files.ts +0 -35
  199. package/src/core/schema-docs.ts +0 -174
  200. package/src/core/validator.ts +0 -508
  201. package/src/helpers/bgp.ts +0 -473
  202. package/src/helpers/cables.ts +0 -74
  203. package/src/helpers/checks.ts +0 -201
  204. package/src/helpers/exchange.ts +0 -123
  205. package/src/helpers/filters.ts +0 -107
  206. package/src/helpers/index.ts +0 -25
  207. package/src/helpers/route-server.ts +0 -212
  208. package/src/helpers/upstreams.ts +0 -369
  209. package/src/index.ts +0 -13
  210. package/src/integrations/librenms.ts +0 -291
  211. package/src/integrations/netbox.ts +0 -620
  212. package/src/integrations/peeringdb.ts +0 -173
  213. package/src/integrations/prometheus.ts +0 -90
  214. package/src/schema/common.ts +0 -41
  215. package/src/schema/device.ts +0 -145
  216. package/src/schema/firewall.ts +0 -140
  217. package/src/schema/index.ts +0 -8
  218. package/src/schema/policy.ts +0 -115
  219. package/src/schema/routing.ts +0 -173
  220. package/src/schema/system.ts +0 -203
  221. package/src/studio/public/app.js +0 -186
  222. package/src/studio/public/compare.js +0 -250
  223. package/src/studio/public/dom.js +0 -138
  224. package/src/studio/public/evaluate.js +0 -687
  225. package/src/studio/public/export.js +0 -130
  226. package/src/studio/public/graph.js +0 -1541
  227. package/src/studio/public/index.html +0 -83
  228. package/src/studio/public/net.js +0 -134
  229. package/src/studio/public/tables.js +0 -1289
  230. package/src/studio/public/tools.js +0 -610
  231. package/templates/colocation/checks.ts +0 -53
  232. package/templates/colocation/circuit.config.ts +0 -9
  233. package/templates/colocation/edge-01.ts +0 -133
  234. package/templates/colocation/routing.ts +0 -13
  235. package/templates/colocation/tenancy.ts +0 -76
  236. package/templates/colocation/tenants/acme.ts +0 -19
  237. package/templates/colocation/tenants/index.ts +0 -6
  238. package/templates/colocation/tor-01.ts +0 -33
  239. package/templates/edge-router/checks.ts +0 -20
  240. package/templates/edge-router/circuit.config.ts +0 -8
  241. package/templates/edge-router/edge-01.ts +0 -110
  242. package/templates/edge-router/routing.ts +0 -13
  243. package/templates/project/AGENTS.md +0 -51
  244. package/templates/project/CLAUDE.md +0 -1
  245. package/templates/project/README.md +0 -16
  246. package/templates/project/gitignore +0 -5
  247. package/templates/project/tsconfig.json +0 -13
  248. package/templates/single-site/checks.ts +0 -26
  249. package/templates/single-site/circuit.config.ts +0 -8
  250. package/templates/single-site/router.ts +0 -86
  251. package/templates/single-site/site.ts +0 -18
  252. package/templates/single-site/switch.ts +0 -34
  253. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  254. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
@@ -10,30 +10,29 @@ import type {
10
10
  Ethertype,
11
11
  Family,
12
12
  FilterMatch,
13
- Interface,
13
+ Lag,
14
14
  NatRule,
15
15
  Neighbor,
16
16
  NeighborSettings,
17
17
  PolicyRule,
18
18
  Port,
19
19
  Ports,
20
+ Prefix,
21
+ RouteRule,
20
22
  Service,
21
23
  Severity,
22
- } from "../../schema";
24
+ VlanInterface,
25
+ } from "../../model";
23
26
  import { Catalog } from "../catalog";
27
+ import { Derive, type NamedInterface } from "../derive";
24
28
 
25
29
  /** A property value as a command takes it. `undefined` and `false` leave the property out. */
26
30
  export type Value = string | number | undefined | false;
27
31
 
28
- export type AnyPort = Port<string, string, string, string>;
29
-
30
32
  /** One switch rule: the port it is on, the ACL rule it comes from, and the one source and destination port or range it takes. */
31
- export type SwitchRule = { port: string; rule: AclRule<string>; srcPort?: string; dstPort?: string };
32
- export type AnyInterface = Interface<string, string>;
33
+ export type SwitchRule = { port: string; rule: AclRule; srcPort?: string; dstPort?: string };
33
34
 
34
- type Speed = NonNullable<AnyPort["speed"]>;
35
- type AnyRule = PolicyRule<string, string>;
36
- type AnyNeighbor = Neighbor<string, string, string>;
35
+ type Speed = NonNullable<Port["speed"]>;
37
36
 
38
37
  /** One device's render: the device, and the commands collected under their menu. */
39
38
  export class RouterOsRender {
@@ -85,7 +84,7 @@ export class RouterOsRender {
85
84
  "rs-client": "ebgp-rs",
86
85
  } as const;
87
86
 
88
- /** Neutral service name to the platform's. `native` is the platform's own desktop client. */
87
+ /** Neutral service name to the platform's. `native_protocol` is the platform's own desktop client. */
89
88
  private static readonly SERVICES = {
90
89
  ftp: "ftp",
91
90
  ssh: "ssh",
@@ -94,12 +93,11 @@ export class RouterOsRender {
94
93
  https: "www-ssl",
95
94
  api: "api",
96
95
  api_tls: "api-ssl",
97
- native: "winbox",
96
+ native_protocol: "winbox",
98
97
  } as const;
99
98
 
100
99
  private static readonly USER_GROUPS = { admin: "full", operator: "write", "read-only": "read" } as const;
101
100
 
102
- private static readonly SNMP_AUTH = { sha1: "SHA1", sha256: "SHA1" } as const;
103
101
  private static readonly SNMP_PRIVACY = { aes128: "AES", des: "DES" } as const;
104
102
 
105
103
  /** Severity threshold to the topics the platform logs under, most severe first. */
@@ -112,17 +110,41 @@ export class RouterOsRender {
112
110
  ];
113
111
  private static readonly SEVERITY_ORDER: Severity[] = ["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"];
114
112
 
115
- private readonly ports: Record<string, AnyPort>;
116
- private readonly interfaces: Record<string, AnyInterface>;
113
+ private readonly ports: Record<string, Port>;
114
+
115
+ /** Every interface that is not a port, kind by kind. */
116
+ private readonly interfaces: NamedInterface[];
117
117
 
118
118
  /** Commands under their menu. */
119
119
  private readonly menus = new Map<string, string[]>();
120
120
 
121
121
  private constructor(private readonly device: Device) {
122
- this.ports = Object.fromEntries(
123
- Object.entries(device.ports ?? {}).filter((entry): entry is [string, AnyPort] => entry[1] !== undefined)
124
- );
125
- this.interfaces = device.interfaces ?? {};
122
+ this.ports = Object.fromEntries(Derive.ports(device));
123
+ this.interfaces = Derive.interfaces(device);
124
+ }
125
+
126
+ /** The entries of a map keyed by name, without the names whose value is left out. */
127
+ static entries<V>(map: { readonly [name: string]: V | undefined } | undefined): [string, V][] {
128
+ return Object.entries(map ?? {}).flatMap(([name, value]) => (value === undefined ? [] : [[name, value] as [string, V]]));
129
+ }
130
+
131
+ /** A value of a match: the one written, or the only one of a list. `unsupported()` refuses a list of more. */
132
+ static one<T>(value: T | readonly T[] | undefined): T | undefined {
133
+ return Array.isArray(value) ? (value as readonly T[])[0] : (value as T | undefined);
134
+ }
135
+
136
+ /** A route policy rule on routes: not a call, and not a `{ communities }` rule, which `unsupported()` refuses. */
137
+ static onRoutes(rule: PolicyRule): rule is RouteRule {
138
+ return rule.call === undefined && rule.communities === undefined;
139
+ }
140
+
141
+ /** A prefix set's prefixes, written as a list or as a map of named prefixes. Undefined for a set fetched from a registry. */
142
+ static prefixesOf(device: Device, set: string): readonly Prefix[] | undefined {
143
+ const declared = device.routing?.prefix_sets?.[set];
144
+ if (!declared || !("prefixes" in declared)) return undefined;
145
+
146
+ const prefixes = declared.prefixes;
147
+ return Array.isArray(prefixes) ? prefixes : Object.values(prefixes);
126
148
  }
127
149
 
128
150
  /** The whole configuration the device should run. */
@@ -261,8 +283,8 @@ export class RouterOsRender {
261
283
  }
262
284
 
263
285
  /** The conditions of a rule, as the platform's filter language writes them. */
264
- private static conditions(rule: AnyRule): string[] {
265
- const match = rule.match ?? {};
286
+ private static conditions(rule: RouteRule): string[] {
287
+ const match = rule.if ?? {};
266
288
  const tests: string[] = [];
267
289
  if (match.prefix) tests.push(`dst==${match.prefix}`);
268
290
  if (match.prefix_set) tests.push(`dst in ${match.prefix_set}`);
@@ -271,25 +293,25 @@ export class RouterOsRender {
271
293
  if (match.as_path_length?.min !== undefined) tests.push(`bgp-path-len>${match.as_path_length.min - 1}`);
272
294
  if (match.as_path_length?.max !== undefined) tests.push(`bgp-path-len<${match.as_path_length.max + 1}`);
273
295
  // The platform's AS path regexp matches each AS as a whole number, so a bare number is that AS anywhere in the path.
274
- if (match.through_as !== undefined) tests.push(`bgp-as-path ${match.through_as}`);
296
+ if (match.as_path_contains !== undefined) tests.push(`bgp-as-path ${match.as_path_contains}`);
275
297
  if (match.rpki) tests.push(`rpki ${match.rpki}`);
276
- if (match.community) tests.push(`bgp-communities includes ${match.community}`);
298
+ if (match.standard_community) tests.push(`bgp-communities includes ${match.standard_community}`);
277
299
  if (match.large_community) tests.push(`bgp-large-communities includes ${match.large_community}`);
278
300
  return tests;
279
301
  }
280
302
 
281
303
  /** What a rule does to a matching route, then its decision. */
282
- private static actions(rule: AnyRule, family: Family): string[] {
304
+ private static actions(rule: RouteRule, family: Family): string[] {
283
305
  const set = rule.set ?? {};
284
306
  const steps: string[] = [];
285
307
  if (set.local_pref !== undefined) steps.push(`set bgp-local-pref ${set.local_pref}`);
286
308
  if (set.med !== undefined) steps.push(`set bgp-med ${set.med}`);
287
309
  if (set.prepend !== undefined) steps.push(`set bgp-path-prepend ${set.prepend}`);
288
- for (const pattern of set.remove_communities ?? []) {
310
+ for (const pattern of set.remove_standard_communities ?? []) {
289
311
  steps.push(`delete bgp-communities regexp "${RouterOsRender.communityPattern(pattern)}"`);
290
312
  }
291
- if (set.communities) steps.push(`set bgp-communities ${set.communities.join(",")}`);
292
- if (set.add_communities) steps.push(`append bgp-communities ${set.add_communities.join(",")}`);
313
+ if (set.standard_communities) steps.push(`set bgp-communities ${set.standard_communities.join(",")}`);
314
+ if (set.add_standard_communities) steps.push(`append bgp-communities ${set.add_standard_communities.join(",")}`);
293
315
 
294
316
  for (const pattern of set.remove_large_communities ?? []) {
295
317
  steps.push(`delete bgp-large-communities regexp "${RouterOsRender.communityPattern(pattern)}"`);
@@ -299,18 +321,21 @@ export class RouterOsRender {
299
321
  if (set.add_large_communities) steps.push(`append bgp-large-communities ${set.add_large_communities.join(",")}`);
300
322
  // An address only means something to routes of its own family.
301
323
  if (set.next_hop && Address.family(set.next_hop) === family) steps.push(`set gw ${set.next_hop}`);
302
- if (set.preferred_source && Address.family(set.preferred_source) === family) steps.push(`set pref-src ${set.preferred_source}`);
324
+ const preferredSource = set.preferred_source && Address.host(set.preferred_source);
325
+ if (preferredSource && Address.family(preferredSource) === family) steps.push(`set pref-src ${preferredSource}`);
303
326
  if (set.blackhole) steps.push("set blackhole yes");
304
- if (rule.action) steps.push(rule.action);
327
+ if (rule.then) steps.push(rule.then);
305
328
  return steps;
306
329
  }
307
330
 
308
331
  /** One rule as the platform writes it: hand off, ask the validator, test, change, decide. */
309
- private static ruleText(rule: AnyRule, family: Family): string {
332
+ private static ruleText(rule: PolicyRule, family: Family): string {
333
+ if (rule.call) return `jump ${RouterOsRender.chainName(rule.call, family)}`;
334
+ if (!RouterOsRender.onRoutes(rule)) return "";
335
+
310
336
  const parts: string[] = [];
311
- if (rule.call) parts.push(`jump ${RouterOsRender.chainName(rule.call, family)}`);
312
337
  // An RPKI state is only known once the validator has been asked, per route, so the question goes on the same rule.
313
- if (rule.match?.rpki) parts.push(`rpki-verify ${RouterOsRender.RPKI_GROUP}`);
338
+ if (rule.if?.rpki) parts.push(`rpki-verify ${RouterOsRender.RPKI_GROUP}`);
314
339
 
315
340
  const tests = RouterOsRender.conditions(rule);
316
341
  const steps = RouterOsRender.actions(rule, family);
@@ -353,49 +378,34 @@ export class RouterOsRender {
353
378
 
354
379
  /** What the platform calls a port or an interface. A port is spelled by the model; an interface keeps its config name. */
355
380
  private name(portOrInterface: string): string {
356
- if (portOrInterface in this.interfaces) return portOrInterface;
381
+ if (this.interfaces.some((entry) => entry.name === portOrInterface)) return portOrInterface;
357
382
 
358
383
  return Catalog.spell(this.device.platform, this.device.model, portOrInterface);
359
384
  }
360
385
 
361
386
  /** The number of a VLAN, by its config name. */
362
387
  private vlanId(vlan: string): number {
363
- const declared = this.device.vlans?.[vlan];
388
+ const declared = this.device.switching?.vlans?.[vlan];
364
389
  if (!declared) throw new Error(`${this.device.name}: VLAN ${vlan} is not declared`);
365
390
  return declared.id;
366
391
  }
367
392
 
368
393
  /** Ports and LAGs that are layer 2 members of a VLAN, with their settings. A port in a LAG is represented by the LAG. */
369
- private switched(): [string, AnyPort | Extract<AnyInterface, { type: "lag" }>][] {
370
- const ports = Object.entries(this.ports).filter(([, port]) => !port.lag && (port.access_vlan || port.trunk_vlans));
371
- const lags = Object.entries(this.interfaces).filter((entry): entry is [string, Extract<AnyInterface, { type: "lag" }>] => {
372
- const lag = entry[1];
373
- return lag.type === "lag" && Boolean(lag.access_vlan || lag.trunk_vlans);
374
- });
394
+ private switched(): [string, Port | Lag][] {
395
+ const ports = Object.entries(this.ports).filter(([, port]) => !port.in_lag && (port.access_vlan || port.trunk_vlans));
396
+ const lags = RouterOsRender.entries(this.device.interfaces?.lag).filter(([, lag]) => Boolean(lag.access_vlan || lag.trunk_vlans));
375
397
  return [...ports, ...lags];
376
398
  }
377
399
 
378
400
  /** VLAN interfaces, with their settings. */
379
- private declaredVlanInterfaces(): [string, Extract<AnyInterface, { type: "vlan" }>][] {
380
- return Object.entries(this.interfaces).filter(
381
- (
382
- entry
383
- ): entry is [
384
- string,
385
- Extract<
386
- AnyInterface,
387
- {
388
- type: "vlan";
389
- }
390
- >,
391
- ] => entry[1].type === "vlan"
392
- );
401
+ private declaredVlanInterfaces(): [string, VlanInterface][] {
402
+ return RouterOsRender.entries(this.device.interfaces?.vlan_interface);
393
403
  }
394
404
 
395
405
  /** Every port and interface that holds addresses or other layer 3 settings. Ports are named as the platform spells them. */
396
- private routed(): { name: string; settings: AnyPort | AnyInterface }[] {
406
+ private routed(): { name: string; settings: Port | NamedInterface["settings"] }[] {
397
407
  const ports = Object.entries(this.ports).map(([port, settings]) => ({ name: this.name(port), settings }));
398
- const interfaces = Object.entries(this.interfaces).map(([name, settings]) => ({ name, settings }));
408
+ const interfaces = this.interfaces.map(({ name, settings }) => ({ name, settings }));
399
409
  return [...ports, ...interfaces];
400
410
  }
401
411
 
@@ -422,7 +432,7 @@ export class RouterOsRender {
422
432
  * Restarting the offload after a failure, and offloading IPv6 too, follow the same switch.
423
433
  */
424
434
  private hardwareOffload(): void {
425
- const on = RouterOsRender.yesNo(this.device.hardware_offload);
435
+ const on = RouterOsRender.yesNo(this.device.system?.hardware_offload);
426
436
  this.add("/interface ethernet switch", "set switch1", { "l3-hw-offloading": on });
427
437
  this.add("/interface ethernet switch l3hw-settings", "set", { autorestart: on, "ipv6-hw": on });
428
438
  }
@@ -433,7 +443,7 @@ export class RouterOsRender {
433
443
  const needsBridge = this.switched().length > 0 || this.declaredVlanInterfaces().length > 0;
434
444
 
435
445
  if (needsBridge) {
436
- const stp = device.stp;
446
+ const stp = device.switching?.stp;
437
447
  const protocolMode = stp ? (stp.mode ?? "rstp") : "none";
438
448
  const priority = stp?.priority === undefined ? undefined : `0x${stp.priority.toString(16)}`;
439
449
  this.add("/interface bridge", "add", {
@@ -444,10 +454,8 @@ export class RouterOsRender {
444
454
  });
445
455
  }
446
456
 
447
- for (const [name, iface] of Object.entries(this.interfaces)) {
448
- if (iface.type !== "loopback") continue;
449
-
450
- this.add("/interface bridge", "add", { comment: iface.description, name });
457
+ for (const [name, loopback] of RouterOsRender.entries(device.interfaces?.loopback)) {
458
+ this.add("/interface bridge", "add", { comment: loopback.description, name });
451
459
  }
452
460
  }
453
461
 
@@ -499,11 +507,9 @@ export class RouterOsRender {
499
507
 
500
508
  /** A LAG is a bonding interface. Its members are named as the platform spells them. */
501
509
  private lags(): void {
502
- for (const [name, lag] of Object.entries(this.interfaces)) {
503
- if (lag.type !== "lag") continue;
504
-
510
+ for (const [name, lag] of RouterOsRender.entries(this.device.interfaces?.lag)) {
505
511
  const members = Object.entries(this.ports)
506
- .filter(([, port]) => port.lag === name)
512
+ .filter(([, port]) => port.in_lag === name)
507
513
  .map(([port]) => this.name(port));
508
514
  const mode = lag.mode === "static" ? "balance-xor" : "802.3ad";
509
515
  this.add("/interface bonding", "add", {
@@ -524,7 +530,7 @@ export class RouterOsRender {
524
530
  */
525
531
  private vlanInterfaces(): void {
526
532
  for (const [name, iface] of this.declaredVlanInterfaces()) {
527
- const softwareOnly = this.device.hardware_offload && iface.hardware_offload === false;
533
+ const softwareOnly = this.device.system?.hardware_offload && iface.hardware_offload === false;
528
534
  const vlanName = iface.mac ? RouterOsRender.underMacvlan(name) : name;
529
535
 
530
536
  this.add("/interface vlan", "add", {
@@ -549,38 +555,34 @@ export class RouterOsRender {
549
555
  }
550
556
 
551
557
  private tunnels(): void {
552
- for (const [name, iface] of Object.entries(this.interfaces)) {
553
- if (iface.type === "gre") {
554
- this.add("/interface gre", "add", {
555
- comment: iface.description,
556
- "local-address": iface.local,
557
- mtu: iface.mtu,
558
- name,
559
- "remote-address": iface.remote,
560
- });
561
- }
558
+ for (const [name, gre] of RouterOsRender.entries(this.device.interfaces?.gre)) {
559
+ this.add("/interface gre", "add", {
560
+ comment: gre.description,
561
+ "local-address": Address.host(gre.local_address),
562
+ mtu: gre.mtu,
563
+ name,
564
+ "remote-address": gre.remote_address,
565
+ });
566
+ }
562
567
 
563
- if (iface.type === "vxlan") {
564
- // 4789 is the platform's default and is not written.
565
- const port = iface.port && iface.port !== 4789 ? iface.port : undefined;
566
- this.add("/interface vxlan", "add", {
567
- comment: iface.description,
568
- "local-address": iface.local,
569
- "mac-address": iface.mac,
570
- mtu: iface.mtu,
571
- name,
572
- port,
573
- vni: iface.vni,
574
- });
575
- this.add("/interface vxlan vteps", "add", { interface: name, "remote-ip": iface.remote });
576
- }
568
+ for (const [name, vxlan] of RouterOsRender.entries(this.device.interfaces?.vxlan)) {
569
+ // 4789 is the platform's default and is not written.
570
+ const port = vxlan.port && vxlan.port !== 4789 ? vxlan.port : undefined;
571
+ this.add("/interface vxlan", "add", {
572
+ comment: vxlan.description,
573
+ "local-address": Address.host(vxlan.local_address),
574
+ "mac-address": vxlan.mac,
575
+ mtu: vxlan.mtu,
576
+ name,
577
+ port,
578
+ vni: vxlan.vni,
579
+ });
580
+ this.add("/interface vxlan vteps", "add", { interface: name, "remote-ip": vxlan.remote_address });
577
581
  }
578
582
  }
579
583
 
580
584
  private wireguard(): void {
581
- for (const [name, iface] of Object.entries(this.interfaces)) {
582
- if (iface.type !== "wireguard") continue;
583
-
585
+ for (const [name, iface] of RouterOsRender.entries(this.device.interfaces?.wireguard)) {
584
586
  this.add("/interface wireguard", "add", {
585
587
  comment: iface.description,
586
588
  "listen-port": iface.listen_port,
@@ -652,7 +654,7 @@ export class RouterOsRender {
652
654
 
653
655
  /** A VLAN is tagged on every trunk carrying it, and on the bridge itself when the router has an interface in it. */
654
656
  private bridgeVlans(): void {
655
- for (const [vlanName, vlan] of Object.entries(this.device.vlans ?? {})) {
657
+ for (const [vlanName, vlan] of RouterOsRender.entries(this.device.switching?.vlans)) {
656
658
  const routedHere = this.declaredVlanInterfaces().some(([, iface]) => iface.vlan === vlanName);
657
659
  const trunks = this.switched()
658
660
  .filter(([, settings]) => settings.trunk_vlans?.includes(vlanName))
@@ -702,7 +704,7 @@ export class RouterOsRender {
702
704
  const lldp = this.lldpMembers();
703
705
  if (lldp) lists.set("lldp", lldp);
704
706
 
705
- const native = this.device.management?.native?.interfaces;
707
+ const native = this.device.management?.native_protocol?.interfaces;
706
708
  if (native)
707
709
  lists.set(
708
710
  "native",
@@ -722,7 +724,7 @@ export class RouterOsRender {
722
724
  * Where LLDP runs, when it is limited to some ports. Undefined when it runs everywhere or nowhere, which the discovery settings say directly.
723
725
  */
724
726
  private lldpMembers(): string[] | undefined {
725
- const lldp = this.device.lldp;
727
+ const lldp = this.device.switching?.lldp;
726
728
  if (!lldp) return undefined;
727
729
 
728
730
  const overridden = Object.entries(this.ports).filter(([, port]) => port.lldp !== undefined);
@@ -741,7 +743,7 @@ export class RouterOsRender {
741
743
 
742
744
  /** A VRF lists its interfaces. */
743
745
  private vrfs(): void {
744
- for (const [vrf, settings] of Object.entries(this.device.vrfs ?? {})) {
746
+ for (const [vrf, settings] of RouterOsRender.entries(this.device.routing?.vrfs)) {
745
747
  const members = this.routed()
746
748
  .filter(({ settings: iface }) => iface.vrf === vrf)
747
749
  .map(({ name }) => name);
@@ -762,7 +764,7 @@ export class RouterOsRender {
762
764
  for (const entry of settings.addresses ?? []) {
763
765
  const address = typeof entry === "string" ? entry : entry.address;
764
766
  // The far end of a point-to-point address, which the platform calls its network.
765
- const peer = typeof entry === "string" ? undefined : entry.peer;
767
+ const peer = typeof entry === "string" ? undefined : entry.peer_address;
766
768
 
767
769
  if (Address.family(address) === "ipv4") {
768
770
  this.add("/ip address", "add", { address, comment: settings.description, interface: name, network: peer });
@@ -795,7 +797,7 @@ export class RouterOsRender {
795
797
  }
796
798
 
797
799
  private dhcp(): void {
798
- for (const [name, server] of Object.entries(this.device.dhcp ?? {})) {
800
+ for (const [name, server] of Object.entries(this.device.services?.dhcp_server ?? {})) {
799
801
  const [first, last] = server.pool;
800
802
  // The pool shares the server's name.
801
803
  this.add("/ip pool", "add", { name, ranges: `${first}-${last}` });
@@ -821,14 +823,17 @@ export class RouterOsRender {
821
823
  }
822
824
  }
823
825
 
824
- for (const [name, relay] of Object.entries(this.device.dhcp_relay ?? {})) {
826
+ for (const [name, relay] of Object.entries(this.device.services?.dhcp_relay ?? {})) {
825
827
  this.add("/ip dhcp-relay", "add", { "dhcp-server": relay.servers.join(","), interface: this.name(relay.interface), name });
826
828
  }
827
829
  }
828
830
 
829
831
  private dns(): void {
830
- const dns = this.device.system?.dns;
831
- this.add("/ip dns", "set", { "allow-remote-requests": RouterOsRender.yesNo(dns?.serve), servers: dns?.servers.join(",") ?? "" });
832
+ const servers = this.device.system?.dns?.servers;
833
+ this.add("/ip dns", "set", {
834
+ "allow-remote-requests": RouterOsRender.yesNo(this.device.services?.dns_server),
835
+ servers: servers?.join(",") ?? "",
836
+ });
832
837
  }
833
838
 
834
839
  /**
@@ -840,16 +845,17 @@ export class RouterOsRender {
840
845
  const { device } = this;
841
846
  const entries: { list: string; address: string }[] = [];
842
847
 
843
- for (const [list, members] of Object.entries(device.firewall?.address_sets ?? {})) {
848
+ for (const [list, members] of RouterOsRender.entries(device.firewall?.address_sets)) {
844
849
  for (const address of members) entries.push({ list, address });
845
850
  }
846
851
 
847
- for (const [list, set] of Object.entries(device.prefix_sets ?? {})) {
848
- if ("prefixes" in set) for (const address of set.prefixes) entries.push({ list, address });
852
+ for (const [list] of RouterOsRender.entries(device.routing?.prefix_sets)) {
853
+ const prefixes = RouterOsRender.prefixesOf(device, list);
854
+ if (prefixes) for (const address of prefixes) entries.push({ list, address });
849
855
  else this.addLine("/ip firewall address-list", `# generated ${list}`);
850
856
  }
851
857
 
852
- for (const prefix of device.routing?.bgp?.networks ?? []) {
858
+ for (const prefix of device.routing?.bgp?.originated_prefixes ?? []) {
853
859
  entries.push({ list: RouterOsRender.NETWORKS[Address.family(Address.host(prefix))], address: prefix });
854
860
  }
855
861
 
@@ -918,12 +924,13 @@ export class RouterOsRender {
918
924
  /** Every address NAT translates to or from: each translation's target, and the address a destination translation matches. Once each, in rule order. */
919
925
  static natAddresses(device: Device): string[] {
920
926
  const nat = device.firewall?.nat;
927
+ const translatedTo = (rule: NatRule) => (rule.then === "masquerade" ? [] : [Address.host(rule.to_address)]);
921
928
  const addresses = [
922
- ...(nat?.source ?? []).flatMap((rule) => (rule.action === "masquerade" ? [] : [rule.to])),
923
- ...(nat?.destination ?? []).flatMap((rule) => [
924
- ...(rule.match?.dst ? [rule.match.dst] : []),
925
- ...(rule.action === "masquerade" ? [] : [rule.to]),
926
- ]),
929
+ ...(nat?.source ?? []).flatMap(translatedTo),
930
+ ...(nat?.destination ?? []).flatMap((rule) => {
931
+ const matched = RouterOsRender.one(rule.if?.dst);
932
+ return [...(matched ? [matched] : []), ...translatedTo(rule)];
933
+ }),
927
934
  ];
928
935
 
929
936
  return [...new Set(addresses)];
@@ -934,9 +941,9 @@ export class RouterOsRender {
934
941
  *
935
942
  * A rule naming an address set applies only to the families the set has members in.
936
943
  */
937
- private families(match: FilterMatch<string, string> | undefined): Family[] {
944
+ private families(match: FilterMatch | undefined): Family[] {
938
945
  const sets = this.device.firewall?.address_sets ?? {};
939
- const address = match?.src ?? match?.dst;
946
+ const address = RouterOsRender.one(match?.src) ?? RouterOsRender.one(match?.dst);
940
947
 
941
948
  let candidates: Family[] = ["ipv4", "ipv6"];
942
949
  if (match?.family) candidates = [match.family];
@@ -944,25 +951,27 @@ export class RouterOsRender {
944
951
 
945
952
  const setHas = (set: string | undefined, family: Family) =>
946
953
  !set || (sets[set] ?? []).some((member) => Address.family(member) === family);
947
- return candidates.filter((family) => setHas(match?.src_set, family) && setHas(match?.dst_set, family));
954
+ return candidates.filter((family) => setHas(match?.src_address_set, family) && setHas(match?.dst_address_set, family));
948
955
  }
949
956
 
950
957
  /** The match properties of a rule, as the platform names them. */
951
- private matchProperties(match: FilterMatch<string, string> | undefined, family: Family) {
958
+ private matchProperties(match: FilterMatch | undefined, family: Family) {
952
959
  const protocol = match?.protocol === "icmp" && family === "ipv6" ? "icmpv6" : match?.protocol;
953
960
  const setFlags = match?.tcp_flags?.set ?? [];
954
961
  const unsetFlags = (match?.tcp_flags?.unset ?? []).map((flag) => `!${flag}`);
955
962
  const flags = match?.tcp_flags ? [...setFlags, ...unsetFlags].join(",") : undefined;
963
+ const inInterface = RouterOsRender.one(match?.in_interface);
964
+ const outInterface = RouterOsRender.one(match?.out_interface);
956
965
  return {
957
- "connection-state": match?.state?.join(","),
958
- "dst-address": match?.dst,
959
- "dst-address-list": match?.dst_set,
966
+ "connection-state": match?.connection_state?.join(","),
967
+ "dst-address": RouterOsRender.one(match?.dst),
968
+ "dst-address-list": match?.dst_address_set,
960
969
  "dst-port": RouterOsRender.portList(match?.dst_port),
961
- "in-interface": match?.in_interface && this.name(match.in_interface),
962
- "out-interface": match?.out_interface && this.name(match.out_interface),
970
+ "in-interface": inInterface && this.name(inInterface),
971
+ "out-interface": outInterface && this.name(outInterface),
963
972
  protocol,
964
- "src-address": match?.src,
965
- "src-address-list": match?.src_set,
973
+ "src-address": RouterOsRender.one(match?.src),
974
+ "src-address-list": match?.src_address_set,
966
975
  "src-port": RouterOsRender.portList(match?.src_port),
967
976
  "tcp-flags": flags,
968
977
  };
@@ -976,47 +985,49 @@ export class RouterOsRender {
976
985
  if (!settings) continue;
977
986
 
978
987
  for (const rule of settings.rules ?? []) {
979
- if (!this.families(rule.match).includes(family)) continue;
988
+ if (!this.families(rule.if).includes(family)) continue;
980
989
 
981
- const match = this.matchProperties(rule.match, family);
990
+ const match = this.matchProperties(rule.if, family);
982
991
  // The platform's fast path takes a flow only on IPv4. The accept below still applies on both.
983
- if (rule.offload && family === "ipv4") {
992
+ if (rule.fast_path && family === "ipv4") {
984
993
  this.add(menu, "add", { action: "fasttrack-connection", chain, comment: rule.description, ...match });
985
994
  }
986
995
 
987
- this.add(menu, "add", { action: rule.action, chain, comment: rule.description, ...match });
996
+ this.add(menu, "add", { action: rule.then, chain, comment: rule.description, ...match });
988
997
  }
989
998
 
990
- if (settings.default === "drop") this.add(menu, "add", { action: "drop", chain, comment: "Default" });
999
+ if (settings.default_action === "drop") this.add(menu, "add", { action: "drop", chain, comment: "Default" });
991
1000
  }
992
1001
  }
993
1002
 
994
1003
  private nat(family: Family): void {
995
1004
  const menu = family === "ipv4" ? "/ip firewall nat" : "/ipv6 firewall nat";
996
- const chains: [string, readonly NatRule<string, string>[] | undefined][] = [
1005
+ const chains: [string, readonly NatRule[] | undefined][] = [
997
1006
  ["srcnat", this.device.firewall?.nat?.source],
998
1007
  ["dstnat", this.device.firewall?.nat?.destination],
999
1008
  ];
1000
1009
 
1001
1010
  for (const [chain, rules] of chains) {
1002
1011
  for (const rule of rules ?? []) {
1003
- if (!this.families(rule.match).includes(family)) continue;
1012
+ if (!this.families(rule.if).includes(family)) continue;
1004
1013
  // A translation to an address belongs to that address's family.
1005
- if (rule.action !== "masquerade" && Address.family(rule.to) !== family) continue;
1014
+ if (rule.then !== "masquerade" && Address.family(rule.to_address) !== family) continue;
1006
1015
 
1007
1016
  const action =
1008
- rule.action === "masquerade"
1009
- ? {
1010
- action: "masquerade",
1011
- }
1012
- : { action: rule.action === "snat" ? "src-nat" : "dst-nat", "to-addresses": rule.to, "to-ports": rule.to_port };
1013
- this.add(menu, "add", { ...action, chain, comment: rule.description, ...this.matchProperties(rule.match, family) });
1017
+ rule.then === "masquerade"
1018
+ ? { action: "masquerade" }
1019
+ : {
1020
+ action: rule.then === "snat" ? "src-nat" : "dst-nat",
1021
+ "to-addresses": Address.host(rule.to_address),
1022
+ "to-ports": rule.to_port,
1023
+ };
1024
+ this.add(menu, "add", { ...action, chain, comment: rule.description, ...this.matchProperties(rule.if, family) });
1014
1025
  }
1015
1026
  }
1016
1027
  }
1017
1028
 
1018
1029
  private helpers(): void {
1019
- const enabled = this.device.firewall?.helpers ?? [];
1030
+ const enabled = this.device.firewall?.conntrack_helpers ?? [];
1020
1031
 
1021
1032
  for (const helper of RouterOsRender.HELPERS) {
1022
1033
  this.add("/ip firewall service-port", `set ${helper}`, { disabled: RouterOsRender.yesNo(!enabled.includes(helper)) });
@@ -1028,23 +1039,26 @@ export class RouterOsRender {
1028
1039
  */
1029
1040
  private switchAcls(): void {
1030
1041
  for (const { port, rule, srcPort, dstPort } of RouterOsRender.switchRules(this.device)) {
1031
- const match = rule.match ?? {};
1032
- const ipv6 = match.family === "ipv6" || [match.src, match.dst].some((address) => address && Address.family(address) === "ipv6");
1042
+ const match = rule.if ?? {};
1043
+ const src = RouterOsRender.one(match.src);
1044
+ const dst = RouterOsRender.one(match.dst);
1045
+ const vlan = RouterOsRender.one(match.vlan);
1046
+ const ipv6 = match.family === "ipv6" || [src, dst].some((address) => address && Address.family(address) === "ipv6");
1033
1047
  const address = (prefix: string | undefined) => (prefix && !ipv6 ? RouterOsRender.dottedMask(prefix) : prefix);
1034
1048
 
1035
1049
  this.add("/interface ethernet switch rule", "add", {
1036
1050
  comment: rule.description,
1037
- [ipv6 ? "dst-address6" : "dst-address"]: address(match.dst),
1051
+ [ipv6 ? "dst-address6" : "dst-address"]: address(dst),
1038
1052
  "dst-port": dstPort,
1039
1053
  "mac-protocol": ipv6 ? "ipv6" : "ip",
1040
- "new-dst-ports": rule.action === "drop" ? "" : undefined,
1054
+ "new-dst-ports": rule.then === "drop" ? "" : undefined,
1041
1055
  ports: this.name(port),
1042
1056
  protocol: match.protocol,
1043
1057
  rate: rule.rate && RouterOsRender.bitrate(rule.rate),
1044
- [ipv6 ? "src-address6" : "src-address"]: address(match.src),
1058
+ [ipv6 ? "src-address6" : "src-address"]: address(src),
1045
1059
  "src-port": srcPort,
1046
1060
  switch: "switch1",
1047
- "vlan-id": match.vlan && this.vlanId(match.vlan),
1061
+ "vlan-id": vlan && this.vlanId(vlan),
1048
1062
  });
1049
1063
  }
1050
1064
 
@@ -1078,9 +1092,7 @@ export class RouterOsRender {
1078
1092
 
1079
1093
  /** The ports with an ethertype filter, with the kinds each takes. Each makes one switch rule per kind and one more. */
1080
1094
  static ethertypeFilters(device: Device): { port: string; ethertypes: readonly Ethertype[] }[] {
1081
- return Object.entries(device.ports ?? {}).flatMap(([port, settings]) =>
1082
- settings?.ethertypes ? [{ port, ethertypes: settings.ethertypes }] : []
1083
- );
1095
+ return Derive.ports(device).flatMap(([port, settings]) => (settings.ethertypes ? [{ port, ethertypes: settings.ethertypes }] : []));
1084
1096
  }
1085
1097
 
1086
1098
  /** Every switch rule the device's hardware ACLs make, in order: one for each port with an ACL, each rule of it, and each source and destination port or range of the rule. */
@@ -1088,12 +1100,12 @@ export class RouterOsRender {
1088
1100
  const each = (ports: Ports | undefined) => (ports === undefined ? [undefined] : RouterOsRender.eachPort(ports));
1089
1101
  const rules: SwitchRule[] = [];
1090
1102
 
1091
- for (const [port, settings] of Object.entries(device.ports ?? {})) {
1092
- if (!settings?.acl) continue;
1103
+ for (const [port, settings] of Derive.ports(device)) {
1104
+ if (!settings.ingress_acl) continue;
1093
1105
 
1094
- for (const rule of device.acls?.[settings.acl] ?? []) {
1095
- for (const srcPort of each(rule.match?.src_port)) {
1096
- for (const dstPort of each(rule.match?.dst_port)) rules.push({ port, rule, srcPort, dstPort });
1106
+ for (const rule of device.switching?.acls?.[settings.ingress_acl] ?? []) {
1107
+ for (const srcPort of each(rule.if?.src_port)) {
1108
+ for (const dstPort of each(rule.if?.dst_port)) rules.push({ port, rule, srcPort, dstPort });
1097
1109
  }
1098
1110
  }
1099
1111
  }
@@ -1118,37 +1130,38 @@ export class RouterOsRender {
1118
1130
 
1119
1131
  /** A table beside the main one. `fib` puts its routes in the forwarding table, so traffic can be routed by them. */
1120
1132
  private routingTables(): void {
1121
- for (const [name, table] of Object.entries(this.device.routing?.tables ?? {})) {
1133
+ for (const [name, table] of RouterOsRender.entries(this.device.routing?.tables)) {
1122
1134
  this.add("/routing table", "add fib", { comment: table.description, name });
1123
1135
  }
1124
1136
  }
1125
1137
 
1126
1138
  /** Rules in the order written. `lookup-only-in-table` drops what the table has no route for, and `lookup` falls back to the main table. */
1127
1139
  private routingRules(): void {
1128
- for (const rule of this.device.routing?.rules ?? []) {
1129
- const onlyInTable = rule.table !== "main" && (rule.fallback ?? "none") === "none";
1140
+ for (const rule of this.device.routing?.policy_routing ?? []) {
1141
+ const onlyInTable = rule.routing_table !== "main" && (rule.fallback ?? "none") === "none";
1130
1142
  this.add("/routing rule", "add", {
1131
1143
  action: onlyInTable ? "lookup-only-in-table" : "lookup",
1132
1144
  comment: rule.description,
1133
1145
  "dst-address": rule.match?.dst,
1134
1146
  interface: rule.match?.in_interface && this.name(rule.match.in_interface),
1135
1147
  "src-address": rule.match?.src,
1136
- table: rule.table,
1148
+ table: rule.routing_table,
1137
1149
  });
1138
1150
  }
1139
1151
  }
1140
1152
 
1141
1153
  private staticRoutes(): void {
1142
- for (const route of this.device.routing?.static ?? []) {
1154
+ for (const route of this.device.routing?.static_routes ?? []) {
1143
1155
  const menu = Address.family(Address.host(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
1144
1156
  const verb = route.blackhole ? "add blackhole" : "add";
1145
- const gateway = route.via ?? (route.interface ? this.name(route.interface) : undefined);
1157
+ const nextHop = route.next_hop && Address.host(route.next_hop);
1158
+ const gateway = nextHop ?? (route.interface ? this.name(route.interface) : undefined);
1146
1159
  this.add(menu, verb, {
1147
1160
  comment: route.description,
1148
1161
  distance: route.distance,
1149
1162
  "dst-address": route.prefix,
1150
1163
  gateway,
1151
- "routing-table": route.vrf ?? Utils.otherTable(route.table),
1164
+ "routing-table": route.vrf ?? Utils.otherTable(route.routing_table),
1152
1165
  });
1153
1166
  }
1154
1167
  }
@@ -1171,28 +1184,28 @@ export class RouterOsRender {
1171
1184
  }
1172
1185
 
1173
1186
  /** A neighbor's settings with its group's beneath them. */
1174
- private settingsOf(neighbor: AnyNeighbor): NeighborSettings<string, string> {
1175
- const group = neighbor.group ? this.device.routing?.bgp?.groups?.[neighbor.group] : undefined;
1187
+ private settingsOf(neighbor: Neighbor): NeighborSettings {
1188
+ const group = neighbor.neighbor_group ? this.device.routing?.bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
1176
1189
  return { ...group, ...neighbor };
1177
1190
  }
1178
1191
 
1179
1192
  /** The families a neighbor exchanges: the ones it states, or its address's. */
1180
- private familiesOf(neighbor: AnyNeighbor): readonly Family[] {
1181
- return this.settingsOf(neighbor).families ?? [Address.family(neighbor.address)];
1193
+ private familiesOf(neighbor: Neighbor): readonly Family[] {
1194
+ return this.settingsOf(neighbor).address_families ?? [Address.family(neighbor.address)];
1182
1195
  }
1183
1196
 
1184
1197
  /** 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. */
1185
- private appliesTo(rule: AnyRule, family: Family): boolean {
1186
- const match = rule.match;
1198
+ private appliesTo(rule: PolicyRule, family: Family): boolean {
1199
+ const match = RouterOsRender.onRoutes(rule) ? rule.if : undefined;
1187
1200
  if (match?.family && match.family !== family) return false;
1188
1201
 
1189
1202
  if (match?.prefix && Address.family(Address.host(match.prefix)) !== family) return false;
1190
1203
 
1191
1204
  if (match?.prefix_set) {
1192
- const set = this.device.prefix_sets?.[match.prefix_set];
1205
+ const set = this.device.routing?.prefix_sets?.[match.prefix_set];
1193
1206
  if (!set) return false;
1194
1207
 
1195
- const firstPrefix = "source" in set ? undefined : set.prefixes[0];
1208
+ const firstPrefix = RouterOsRender.prefixesOf(this.device, match.prefix_set)?.[0];
1196
1209
  const setFamily = "source" in set ? set.source.family : firstPrefix && Address.family(Address.host(firstPrefix));
1197
1210
  if (setFamily !== family) return false;
1198
1211
  }
@@ -1207,7 +1220,7 @@ export class RouterOsRender {
1207
1220
  */
1208
1221
  private policies(): void {
1209
1222
  const bgp = this.device.routing!.bgp!;
1210
- const policies = this.device.policies ?? {};
1223
+ const policies = this.device.routing?.policies ?? {};
1211
1224
 
1212
1225
  const wanted = new Map<string, Set<Family>>();
1213
1226
  const want = (policy: string, family: Family) => {
@@ -1225,14 +1238,14 @@ export class RouterOsRender {
1225
1238
  const settings = this.settingsOf(neighbor);
1226
1239
 
1227
1240
  for (const family of this.familiesOf(neighbor)) {
1228
- for (const policy of [settings.import, settings.export]) {
1241
+ for (const policy of [settings.import_policy, settings.export_policy]) {
1229
1242
  if (policy) want(policy, family);
1230
1243
  else rejectAllNeeded = true;
1231
1244
  }
1232
1245
  }
1233
1246
  }
1234
1247
 
1235
- const called = new Set(Object.values(policies).flatMap((rules) => rules.flatMap((rule) => (rule.call ? [rule.call] : []))));
1248
+ const called = new Set(Object.values(policies).flatMap((rules) => (rules ?? []).flatMap((rule) => (rule.call ? [rule.call] : []))));
1236
1249
 
1237
1250
  for (const policy of [...wanted.keys()].sort()) {
1238
1251
  for (const family of ["ipv4", "ipv6"] as const) {
@@ -1243,11 +1256,12 @@ export class RouterOsRender {
1243
1256
 
1244
1257
  for (const rule of rules) {
1245
1258
  const text = RouterOsRender.ruleText(rule, family);
1246
- if (text) this.add("/routing filter rule", "add", { chain, comment: rule.description, rule: text });
1259
+ const comment = RouterOsRender.onRoutes(rule) ? rule.description : undefined;
1260
+ if (text) this.add("/routing filter rule", "add", { chain, comment, rule: text });
1247
1261
  }
1248
1262
 
1249
1263
  const last = rules.at(-1);
1250
- const decides = last !== undefined && !last.match && !last.call && last.action !== undefined;
1264
+ const decides = last !== undefined && RouterOsRender.onRoutes(last) && !last.if && last.then !== undefined;
1251
1265
  if (!called.has(policy) && !decides) this.add("/routing filter rule", "add", { chain, rule: "reject" });
1252
1266
  }
1253
1267
  }
@@ -1270,7 +1284,7 @@ export class RouterOsRender {
1270
1284
  const [family] = this.familiesOf(neighbor) as [Family];
1271
1285
  const addressFamily = Address.family(neighbor.address);
1272
1286
  const internal = settings.remote_as === bgp.asn;
1273
- const originates = bgp.networks?.some((prefix) => Address.family(Address.host(prefix)) === family);
1287
+ const originates = bgp.originated_prefixes?.some((prefix) => Address.family(Address.host(prefix)) === family);
1274
1288
  const role = settings.role ? RouterOsRender.ROLES[settings.role] : internal ? "ibgp" : "ebgp";
1275
1289
 
1276
1290
  this.add("/routing bgp connection", "add", {
@@ -1279,21 +1293,21 @@ export class RouterOsRender {
1279
1293
  as: bgp.asn,
1280
1294
  connect: settings.passive ? "no" : undefined,
1281
1295
  "hold-time": settings.hold_time ? RouterOsRender.duration(settings.hold_time) : undefined,
1282
- "input.filter": RouterOsRender.chainName(settings.import ?? RouterOsRender.REJECT_ALL, family),
1296
+ "input.filter": RouterOsRender.chainName(settings.import_policy ?? RouterOsRender.REJECT_ALL, family),
1283
1297
  // The one instance bgpInstance adds. 7.23 refuses a new connection without it.
1284
1298
  instance: "default",
1285
1299
  [`input.limit-process-routes-${family}`]: settings.max_prefixes,
1286
1300
  "keepalive-time": settings.keepalive ? RouterOsRender.duration(settings.keepalive) : undefined,
1287
1301
  listen: settings.passive ? "yes" : undefined,
1288
- "local.address": settings.local_address,
1302
+ "local.address": settings.local_address && Address.host(settings.local_address),
1289
1303
  "local.role": role,
1290
1304
  multihop: settings.multihop ? "yes" : undefined,
1291
1305
  name,
1292
- "output.filter-chain": RouterOsRender.chainName(settings.export ?? RouterOsRender.REJECT_ALL, family),
1306
+ "output.filter-chain": RouterOsRender.chainName(settings.export_policy ?? RouterOsRender.REJECT_ALL, family),
1293
1307
  "output.network": originates ? RouterOsRender.NETWORKS[family] : undefined,
1294
1308
  "remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
1295
1309
  "remote.as": settings.remote_as,
1296
- "routing-table": Utils.otherTable(settings.table),
1310
+ "routing-table": Utils.otherTable(settings.routing_table),
1297
1311
  "tcp-md5-key": settings.password && Secrets.marker(settings.password),
1298
1312
  "use-bfd": settings.bfd ? "yes" : undefined,
1299
1313
  });
@@ -1305,7 +1319,7 @@ export class RouterOsRender {
1305
1319
  const ospf = this.device.routing?.ospf;
1306
1320
  if (!ospf) return;
1307
1321
 
1308
- for (const family of ospf.families ?? ["ipv4"]) {
1322
+ for (const family of ospf.address_families ?? ["ipv4"]) {
1309
1323
  const version = family === "ipv4" ? 2 : 3;
1310
1324
  const instance = `ospf-v${version}`;
1311
1325
  this.add("/routing ospf instance", "add", {
@@ -1367,6 +1381,12 @@ export class RouterOsRender {
1367
1381
  this.certificates();
1368
1382
  }
1369
1383
 
1384
+ /** The sources a service takes: its own, or `management.allow`. `unsupported()` refuses an address set named there. */
1385
+ private allowed(own: readonly Prefix[] | undefined): readonly string[] {
1386
+ const shared = this.device.management?.allow;
1387
+ return own ?? (typeof shared === "string" ? [] : shared) ?? [];
1388
+ }
1389
+
1370
1390
  /** Every service the platform has is stated. One the config does not name is disabled. */
1371
1391
  private services(): void {
1372
1392
  const management = this.device.management ?? {};
@@ -1378,7 +1398,7 @@ export class RouterOsRender {
1378
1398
  continue;
1379
1399
  }
1380
1400
 
1381
- const allow = service.allow ?? management.allow ?? [];
1401
+ const allow = this.allowed(service.allow);
1382
1402
  this.add("/ip service", `set ${platform}`, {
1383
1403
  address: allow.join(","),
1384
1404
  certificate: service.certificate,
@@ -1394,7 +1414,7 @@ export class RouterOsRender {
1394
1414
 
1395
1415
  /** What works at layer 2: the platform's client over MAC, MAC ping, the bandwidth test server and neighbor discovery. */
1396
1416
  private layerTwo(): void {
1397
- const nativeList = this.device.management?.native?.interfaces ? "native" : "none";
1417
+ const nativeList = this.device.management?.native_protocol?.interfaces ? "native" : "none";
1398
1418
  this.add("/tool mac-server", "set", { "allowed-interface-list": nativeList });
1399
1419
  this.add("/tool mac-server mac-winbox", "set", { "allowed-interface-list": nativeList });
1400
1420
  this.add("/tool mac-server ping", "set", { enabled: "no" });
@@ -1402,7 +1422,7 @@ export class RouterOsRender {
1402
1422
 
1403
1423
  let discovery = "none";
1404
1424
  if (this.lldpMembers()) discovery = "lldp";
1405
- else if (this.device.lldp) discovery = "all";
1425
+ else if (this.device.switching?.lldp) discovery = "all";
1406
1426
  this.add("/ip neighbor discovery-settings", "set", { "discover-interface-list": discovery, protocol: "lldp" });
1407
1427
  }
1408
1428
 
@@ -1410,8 +1430,7 @@ export class RouterOsRender {
1410
1430
  * 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.
1411
1431
  */
1412
1432
  private snmp(): void {
1413
- const management = this.device.management ?? {};
1414
- const snmp = management.snmp;
1433
+ const snmp = this.device.monitoring?.snmp;
1415
1434
 
1416
1435
  if (!snmp) {
1417
1436
  this.add("/snmp", "set", { enabled: "no" });
@@ -1419,7 +1438,7 @@ export class RouterOsRender {
1419
1438
  return;
1420
1439
  }
1421
1440
 
1422
- const allow = (snmp.allow ?? management.allow ?? []).join(",");
1441
+ const allow = this.allowed(snmp.allow).join(",");
1423
1442
  this.add("/snmp", "set", { contact: snmp.contact ?? "", enabled: "yes", location: snmp.location ?? "" });
1424
1443
 
1425
1444
  if (snmp.community)
@@ -1430,7 +1449,8 @@ export class RouterOsRender {
1430
1449
  this.add("/snmp community", "add", {
1431
1450
  addresses: allow,
1432
1451
  "authentication-password": Secrets.marker(user.auth_password),
1433
- "authentication-protocol": RouterOsRender.SNMP_AUTH[user.auth],
1452
+ // SHA1 only: unsupported() refuses sha256, so it is never written as SHA1.
1453
+ "authentication-protocol": "SHA1",
1434
1454
  "encryption-password": Secrets.marker(user.privacy_password),
1435
1455
  "encryption-protocol": RouterOsRender.SNMP_PRIVACY[user.privacy],
1436
1456
  name,
@@ -1440,7 +1460,7 @@ export class RouterOsRender {
1440
1460
  }
1441
1461
 
1442
1462
  private users(): void {
1443
- for (const [name, user] of Object.entries(this.device.users ?? {})) {
1463
+ for (const [name, user] of RouterOsRender.entries(this.device.system?.users)) {
1444
1464
  this.add("/user", "add", {
1445
1465
  comment: user.description,
1446
1466
  group: RouterOsRender.USER_GROUPS[user.role],
@@ -1455,9 +1475,9 @@ export class RouterOsRender {
1455
1475
  * 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.
1456
1476
  */
1457
1477
  private certificates(): void {
1458
- for (const [name, certificate] of Object.entries(this.device.certificates ?? {})) {
1478
+ for (const [name, certificate] of RouterOsRender.entries(this.device.system?.certificates)) {
1459
1479
  this.add("/certificate", "add", {
1460
- fingerprint: RouterOsRender.fingerprint(certificate.certificate),
1480
+ fingerprint: RouterOsRender.fingerprint(Utils.projectFile(certificate.certificate_file)),
1461
1481
  name,
1462
1482
  "private-key": RouterOsRender.yesNo(Boolean(certificate.private_key)),
1463
1483
  });
@@ -1468,8 +1488,8 @@ export class RouterOsRender {
1468
1488
  const system = this.device.system ?? {};
1469
1489
 
1470
1490
  this.add("/ip settings", "set", {
1471
- "send-redirects": RouterOsRender.yesNo(system.ip?.icmp_redirects),
1472
- "tcp-syncookies": RouterOsRender.yesNo(system.ip?.syn_cookies),
1491
+ "send-redirects": RouterOsRender.yesNo(system.ip_stack?.icmp_redirects),
1492
+ "tcp-syncookies": RouterOsRender.yesNo(system.ip_stack?.syn_cookies),
1473
1493
  });
1474
1494
  this.add("/system identity", "set", { name: this.device.name });
1475
1495
 
@@ -1478,7 +1498,7 @@ export class RouterOsRender {
1478
1498
  this.add("/system note", "set", { note: system.banner ?? "", "show-at-login": "yes" });
1479
1499
 
1480
1500
  this.add("/system ntp client", "set", { enabled: RouterOsRender.yesNo(Boolean(system.ntp?.servers.length)) });
1481
- this.add("/system ntp server", "set", { enabled: RouterOsRender.yesNo(system.ntp?.serve) });
1501
+ this.add("/system ntp server", "set", { enabled: RouterOsRender.yesNo(this.device.services?.ntp_server) });
1482
1502
  for (const server of system.ntp?.servers ?? []) this.add("/system ntp client servers", "add", { address: server });
1483
1503
 
1484
1504
  this.logging();
@@ -1490,10 +1510,9 @@ export class RouterOsRender {
1490
1510
  this.flowExport();
1491
1511
  }
1492
1512
 
1493
- /** Local logging uses the platform's built-in disk action, number 1. Each remote collector gets an action of its own. */
1513
+ /** Local logging uses the platform's built-in disk action, number 1. Each syslog collector gets an action of its own. */
1494
1514
  private logging(): void {
1495
- const logging = this.device.system?.logging;
1496
- const local = logging?.local;
1515
+ const local = this.device.system?.logging?.local;
1497
1516
 
1498
1517
  this.add("/system logging action", "set 1", {
1499
1518
  "disk-file-count": local?.files ?? 2,
@@ -1503,7 +1522,7 @@ export class RouterOsRender {
1503
1522
  this.add("/system logging", "add", { action: "disk", topics: topic });
1504
1523
  }
1505
1524
 
1506
- (logging?.remote ?? []).forEach((collector, index) => {
1525
+ (this.device.monitoring?.syslog ?? []).forEach((collector, index) => {
1507
1526
  const action = `remote${index + 1}`;
1508
1527
  this.add("/system logging action", "add", {
1509
1528
  name: action,
@@ -1517,7 +1536,7 @@ export class RouterOsRender {
1517
1536
 
1518
1537
  /** NetFlow version 9 and IPFIX through traffic flow. The platform has no sFlow, which `unsupported()` reports. */
1519
1538
  private flowExport(): void {
1520
- const flow = this.device.flow_export;
1539
+ const flow = this.device.monitoring?.flow_export;
1521
1540
  if (!flow) {
1522
1541
  this.add("/ip traffic-flow", "set", { enabled: "no" });
1523
1542
  return;