@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
@@ -1,6 +1,6 @@
1
1
  import type { Neighbor } from "../core/cabling";
2
- import type { Finding } from "../core/define";
3
- import type { Device, Family } from "../schema";
2
+ import type { Finding } from "./finding";
3
+ import type { Device, Family } from "../model";
4
4
  import type { ConsoleDialect } from "../transport/console";
5
5
  import type { WaitLimits } from "./bgp-wait";
6
6
  import type { PlatformName } from "./catalog";
@@ -23,7 +23,7 @@ export type ApplyOptions = {
23
23
  /** How a device is read. */
24
24
  export type ReadOptions = Pick<ApplyOptions, "console">;
25
25
 
26
- /** A login on a device for commands that only read, as `circuit show` runs them. */
26
+ /** A login on a device for commands that only read, as `circuit exec` runs them. */
27
27
  export type ReadSession = {
28
28
  /**
29
29
  * Run one command and return what it printed, without its echo or the prompt.
@@ -95,12 +95,14 @@ export abstract class Platform {
95
95
  /** The name a device's `platform` field takes. */
96
96
  abstract readonly name: PlatformName;
97
97
  abstract readonly os: OsSupport;
98
- /** File extension of a saved snapshot, for syntax highlighting. */
98
+ /** File extension of the device's config, for syntax highlighting. */
99
99
  abstract readonly extension: string;
100
- /** How the platform's serial console asks for a login and shows its prompt. Present when the platform can be read and applied through a console, `--console`, and opened with `circuit console`. */
100
+ /** How the platform's serial console asks for a login and shows its prompt. Present when the platform can be read and applied through a console, `--console`, and opened with `circuit access-console`. */
101
101
  readonly console: ConsoleDialect | undefined = undefined;
102
102
  /** Fields this platform cannot express. Validation fails on any of them, so nothing is skipped silently. */
103
103
  abstract unsupported(device: Device): string[];
104
+ /** Fields this platform renders but has not proved on a device yet. Validation refuses them, unless `--experimental` asks for them, which makes each a warning. Absent on a platform with none. */
105
+ experimental?(device: Device): string[];
104
106
  /** What the platform does with a config that it may not do elsewhere, each as `validate` reports it: an error stops `diff` and `apply`, a warning is reported only. Absent on a platform with none. */
105
107
  checks?(device: Device): Finding[];
106
108
  /** The switch rules the render makes from the device's hardware ACLs, so `validate` can hold them to the model's limits. Absent on a platform whose rules are not counted. */
@@ -109,7 +111,7 @@ export abstract class Platform {
109
111
  abstract render(device: Device): string;
110
112
  /** What the device runs now, secrets removed, in the form `render` produces where the platform allows. */
111
113
  abstract read(device: Device, network: readonly Device[], options?: ReadOptions): Promise<string>;
112
- /** The LLDP neighbors the device sees, by Circuit's port names, for `snapshot` to save. Absent on a platform whose LLDP command and its output are not proved on a device yet. */
114
+ /** The LLDP neighbors the device sees, by Circuit's port names, for `diff` and `apply` to compare with each port's `link`. Absent on a platform whose LLDP command and its output are not proved on a device yet. */
113
115
  neighbors?(device: Device, network: readonly Device[], options?: ReadOptions): Promise<Neighbor[]>;
114
116
  /** What it takes to go from `current` to `desired`. */
115
117
  abstract plan(desired: string, current: string, options: ApplyOptions): Plan;
@@ -117,7 +119,7 @@ export abstract class Platform {
117
119
  fill?(device: Device, network: readonly Device[], set: string, family: Family, prefixes: string[]): Promise<void>;
118
120
  /** Send a plan, keeping the device reachable or restoring it. */
119
121
  abstract apply(device: Device, network: readonly Device[], plan: Plan, options: ApplyOptions): Promise<void>;
120
- /** Whether a command only reads on this platform, so `circuit show` may send it. Anything not known to only read is refused. */
122
+ /** Whether a command only reads on this platform, so `circuit exec` may send it. Anything not known to only read is refused. */
121
123
  abstract readOnly(command: string): boolean;
122
124
  /** Log in for commands that only read, over SSH or on the console, the way the platform's read logs in. */
123
125
  abstract session(device: Device, options?: ReadOptions): Promise<ReadSession>;
@@ -128,7 +130,7 @@ export abstract class Platform {
128
130
  /** Open again these sessions, each closed on its prefix limit, read by `bgpSessions`. Only a session still closed on its limit is touched. */
129
131
  bgpClearLimit?(session: ReadSession, sessions: readonly BgpSession[]): Promise<void>;
130
132
  /**
131
- * Checks that the device is healthy after an apply, not only that it took the commands. Called before the plan is sent, to read what the checks compare against; the CLI runs them after the apply and the snapshot. Absent on a platform with no checks yet.
133
+ * Checks that the device is healthy after an apply, not only that it took the commands. Called before the plan is sent, to read what the checks compare against; the CLI runs them after the apply. Absent on a platform with no checks yet.
132
134
  */
133
135
  health?(device: Device, plan: Plan, options?: HealthOptions): Promise<HealthCheck>;
134
136
  }
@@ -35,6 +35,6 @@ Behaviour that looks like something else. Each one has been hit on a RAX721 runn
35
35
  - A MAC access-list a port filters with cannot change: `ACL 3000 is in use, and it can not be modified or destroyed`. Each port has its own list, 3001 and up by its place in the catalog, and the plan unbinds the port, changes the list and binds it again.
36
36
  - `no portswitch` asks for `y` like `portswitch`; a port the plan switched stays switched when it goes back to unused, which the read cannot see.
37
37
  - OSPF BFD is `ip ospf bfd` on the VLAN interface, printed as sent and undone with `no ip ospf bfd`.
38
- - A static route is watched by a BFD session: `bfd N bind peer-ip <next hop> interface vlan <V>` opens the session's view, where `session enable` starts it. It prints under `!command in bfd_template_mode`, with `session enable` indented, and goes with `no bfd N`. The route takes it as `ip route <prefix> <mask> <next hop> track bfd N` and prints `track bfd-session N`; `no ip route <prefix> <mask> <next hop>` removes the route whatever it tracks. A route takes a session number that does not exist yet. Only one session binds to a peer: a second one's `session enable` answers `Error input`, so the old session goes before the new one is made. Proved on tako-mmr-01, 2026-10-24.
38
+ - A static route is watched by a BFD session: `bfd N bind peer-ip <next hop> interface vlan <V>` opens the session's view, where `session enable` starts it. It prints under `!command in bfd_template_mode`, with `session enable` indented, and goes with `no bfd N`. The route takes it as `ip route <prefix> <mask> <next hop> track bfd N` and prints `track bfd-session N`; `no ip route <prefix> <mask> <next hop>` removes the route whatever it tracks. A route takes a session number that does not exist yet. Only one session binds to a peer: a second one's `session enable` answers `Error input`, so the old session goes before the new one is made. Proved on switch-01, 2026-10-24.
39
39
  - BFD timers are global, `bfd detect-multiplier N`, `bfd send-interval N` and `bfd receive-interval N`, but `send-interval` took 10 and refused 300 and 99999, so its unit and range are not known and Circuit refuses `routing.bfd` here.
40
40
  - The caret under `Error input` marks how far the CLI took a line, so sending a whole line with a made-up ending shows whether a keyword exists, without the `?` or Tab that run the line.
@@ -6,7 +6,7 @@ import type { ClientChannel } from "ssh2";
6
6
  import { Address } from "../../core/address";
7
7
  import type { Neighbor } from "../../core/cabling";
8
8
  import { Secrets } from "../../core/secrets";
9
- import type { Device } from "../../schema";
9
+ import type { Device } from "../../model";
10
10
  import { ConsoleShell, SerialConsole, type ConsoleDialect } from "../../transport/console";
11
11
  import { Ssh } from "../../transport/ssh";
12
12
  import { Catalog } from "../catalog";
@@ -139,60 +139,67 @@ export class RaisecomRos extends Platform {
139
139
 
140
140
  if (device.management?.interfaces) found.push("management.interfaces, which only FortiOS builds for now");
141
141
 
142
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
143
- if (iface.type === "bridge") found.push(`interfaces.${name}, a bridge, which only FortiOS builds for now`);
144
- if (iface.type === "vlan" && iface.mac) found.push(`interfaces.${name}.mac, which only RouterOS sets for now`);
142
+ const interfaces = Derive.interfaces(device);
143
+
144
+ for (const { name, kind, settings } of interfaces) {
145
+ if (kind === "bridge") found.push(`interfaces.bridge.${name}, which only FortiOS builds for now`);
146
+ if (kind === "vlan_interface" && "mac" in settings && settings.mac)
147
+ found.push(`interfaces.vlan_interface.${name}.mac, which only RouterOS sets for now`);
145
148
  }
146
149
 
147
- for (const [name, port] of Object.entries(device.ports ?? {})) {
148
- if (!port) continue;
150
+ for (const [name, port] of Derive.ports(device)) {
151
+ const where = `interfaces.ethernet.${name}`;
149
152
 
150
- for (const field of ["speed", "acl", "mtu", "addresses", "vrrp", "vrf"] as const) {
151
- if (port[field] !== undefined) found.push(`ports.${name}.${field}`);
153
+ for (const field of ["speed", "ingress_acl", "mtu", "addresses", "vrrp", "vrf", "ipv6_ra", "hardware_offload"] as const) {
154
+ if (port[field] !== undefined) found.push(`${where}.${field}`);
152
155
  }
153
156
 
154
- if (port.lldp === true) found.push(`ports.${name}.lldp true, LLDP is turned on per device`);
155
- if (port.lag && (port.mac_limit !== undefined || port.ethertypes))
156
- found.push(`ports.${name}.mac_limit and ethertypes on a LAG member, proved on a port alone`);
157
+ if (port.lldp === true) found.push(`${where}.lldp true, LLDP is turned on per device`);
158
+ if (port.in_lag && (port.mac_limit !== undefined || port.ethertypes))
159
+ found.push(`${where}.mac_limit and ethertypes on a LAG member, proved on a port alone`);
157
160
  if (port.mac_limit !== undefined && (port.mac_limit < 1 || port.mac_limit > RaisecomRosRender.MAC_LIMIT_MAX))
158
- found.push(`ports.${name}.mac_limit ${port.mac_limit}, port security takes 1 to ${RaisecomRosRender.MAC_LIMIT_MAX}`);
159
- if (typeof port.stp === "object" || port.stp === true) found.push(`ports.${name}.stp other than false`);
161
+ found.push(`${where}.mac_limit ${port.mac_limit}, port security takes 1 to ${RaisecomRosRender.MAC_LIMIT_MAX}`);
162
+ if (typeof port.stp === "object" || port.stp === true) found.push(`${where}.stp other than false`);
160
163
  const storm = port.storm_control;
161
164
  if (storm && (storm.multicast || storm.unknown_unicast || (storm.broadcast && "percent" in storm.broadcast))) {
162
- found.push(`ports.${name}.storm_control other than broadcast in pps`);
165
+ found.push(`${where}.storm_control other than broadcast in pps`);
163
166
  }
164
167
  }
165
168
 
166
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
167
- if (iface.type === "lag") {
168
- for (const field of ["addresses", "vrrp", "vrf", "mtu", "storm_control", "stp"] as const) {
169
- if (iface[field] !== undefined) found.push(`interfaces.${name}.${field}, a LAG here only switches`);
170
- }
171
- for (const field of ["mac_limit", "ethertypes"] as const) {
172
- if (iface[field] !== undefined) found.push(`interfaces.${name}.${field}, proved on a port alone`);
173
- }
174
- continue;
175
- }
169
+ for (const [name, lag] of Object.entries(device.interfaces?.lag ?? {})) {
170
+ if (!lag) continue;
176
171
 
177
- if (iface.type !== "vlan") {
178
- found.push(`interfaces.${name}, a ${iface.type}`);
179
- continue;
172
+ for (const field of ["addresses", "vrrp", "vrf", "mtu", "storm_control", "stp", "ipv6_ra", "hardware_offload"] as const) {
173
+ if (lag[field] !== undefined) found.push(`interfaces.lag.${name}.${field}, a LAG here only switches`);
174
+ }
175
+ for (const field of ["mac_limit", "ethertypes"] as const) {
176
+ if (lag[field] !== undefined) found.push(`interfaces.lag.${name}.${field}, proved on a port alone`);
180
177
  }
178
+ }
179
+
180
+ for (const [name, iface] of Object.entries(device.interfaces?.vlan_interface ?? {})) {
181
+ if (!iface) continue;
182
+ const where = `interfaces.vlan_interface.${name}`;
181
183
 
182
- for (const field of ["vrf", "description", "mtu", "ipv6_ra"] as const) {
183
- if (iface[field] !== undefined) found.push(`interfaces.${name}.${field}`);
184
+ for (const field of ["vrf", "description", "mtu", "ipv6_ra", "hardware_offload"] as const) {
185
+ if (iface[field] !== undefined) found.push(`${where}.${field}`);
184
186
  }
185
187
 
186
188
  for (const entry of iface.addresses ?? []) {
187
189
  const address = typeof entry === "string" ? entry : entry.address;
188
- if (Address.family(address) === "ipv6") found.push(`interfaces.${name}.addresses ${address}, only IPv4 here`);
190
+ if (Address.family(address) === "ipv6") found.push(`${where}.addresses ${address}, only IPv4 here`);
189
191
  }
190
192
 
191
193
  for (const group of iface.vrrp ?? []) {
192
- if (Address.family(group.address) === "ipv6") found.push(`interfaces.${name}.vrrp for IPv6`);
194
+ if (Address.family(group.address) === "ipv6") found.push(`${where}.vrrp for IPv6`);
193
195
  }
194
196
  }
195
197
 
198
+ // Only LAGs and VLAN interfaces are built here.
199
+ for (const { name, kind } of interfaces) {
200
+ if (kind !== "lag" && kind !== "vlan_interface") found.push(`interfaces.${kind}.${name}`);
201
+ }
202
+
196
203
  const routing = device.routing;
197
204
 
198
205
  for (const field of ["bgp", "rpki"] as const) {
@@ -200,73 +207,85 @@ export class RaisecomRos extends Platform {
200
207
  }
201
208
 
202
209
  const ospf = routing?.ospf;
203
- if (ospf?.families?.includes("ipv6")) found.push("routing.ospf for IPv6");
210
+ if (routing?.router_id && !ospf) found.push("routing.router_id without routing.ospf, only OSPF uses it here");
211
+ if (ospf?.address_families?.includes("ipv6")) found.push("routing.ospf.address_families ipv6, OSPF runs for IPv4 here");
204
212
 
205
213
  for (const [areaId, area] of Object.entries(ospf?.areas ?? {})) {
206
- for (const name of Object.keys(area.interfaces)) {
207
- if (device.interfaces?.[name]?.type !== "vlan")
208
- found.push(`routing.ospf.areas.${areaId}.${name}, OSPF runs on VLAN interfaces here`);
214
+ for (const [name, settings] of Object.entries(area.interfaces)) {
215
+ if (settings && !device.interfaces?.vlan_interface?.[name])
216
+ found.push(`routing.ospf.areas.${areaId}.interfaces.${name}, OSPF runs on VLAN interfaces here`);
209
217
  }
210
218
  }
211
219
 
212
- for (const route of routing?.static ?? []) {
213
- if (!route.via || route.vrf || Address.family(route.prefix) === "ipv6") {
214
- found.push(`routing.static ${route.prefix}, only an IPv4 next hop in the default table`);
220
+ for (const route of routing?.static_routes ?? []) {
221
+ const where = `routing.static_routes ${route.prefix}`;
222
+
223
+ if (!route.next_hop || route.vrf || Address.family(route.prefix) === "ipv6") {
224
+ found.push(`${where}, only an IPv4 next_hop in the default table`);
215
225
  }
216
226
 
217
- if (route.description || route.distance) found.push(`routing.static ${route.prefix} description and distance`);
227
+ if (route.description || route.distance) found.push(`${where} description and distance`);
218
228
 
219
- if (route.bfd && route.via && !RaisecomRosRender.bfdSessions(device).has(route.via)) {
220
- found.push(`routing.static ${route.prefix} bfd, the next hop must be on a VLAN interface's network`);
229
+ if (route.bfd && route.next_hop && !RaisecomRosRender.bfdSessions(device).has(route.next_hop)) {
230
+ found.push(`${where} bfd, the next hop must be on a VLAN interface's network`);
221
231
  }
222
232
  }
223
233
 
224
- for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
225
- if (vlan.description) found.push(`vlans.${name}.description`);
234
+ for (const [name, vlan] of Object.entries(device.switching?.vlans ?? {})) {
235
+ if (vlan?.description) found.push(`switching.vlans.${name}.description`);
226
236
  }
227
237
 
228
- const UNSUPPORTED_SECTIONS = [
229
- "firewall",
230
- "dhcp",
231
- "dhcp_relay",
232
- "acls",
233
- "policies",
234
- "prefix_sets",
235
- "hardware_offload",
236
- "certificates",
237
- "vrfs",
238
- "flow_export",
239
- ] as const;
240
-
241
- for (const field of UNSUPPORTED_SECTIONS) {
242
- if (device[field] !== undefined) found.push(field);
238
+ // Address sets alone build nothing: `management.allow` may name one, and the project has put its addresses there.
239
+ const { address_sets: _addressSets, ...firewall } = device.firewall ?? {};
240
+
241
+ const UNSUPPORTED_GROUPS: [string, unknown][] = [
242
+ ["firewall", Object.keys(firewall).length ? firewall : undefined],
243
+ ["services.dhcp_server", device.services?.dhcp_server],
244
+ ["services.dhcp_relay", device.services?.dhcp_relay],
245
+ ["switching.acls", device.switching?.acls],
246
+ ["routing.policies", routing?.policies],
247
+ ["routing.prefix_sets", routing?.prefix_sets],
248
+ ["system.hardware_offload", device.system?.hardware_offload],
249
+ ["system.certificates", device.system?.certificates],
250
+ ["routing.vrfs", routing?.vrfs],
251
+ ["monitoring.flow_export", device.monitoring?.flow_export],
252
+ ];
253
+
254
+ for (const [path, value] of UNSUPPORTED_GROUPS) {
255
+ if (value !== undefined) found.push(path);
243
256
  }
244
257
 
245
258
  const management = device.management ?? {};
246
259
 
247
- for (const field of ["http", "https", "api", "api_tls", "ftp", "native", "console"] as const) {
260
+ for (const field of ["http", "https", "api", "api_tls", "ftp", "native_protocol", "console"] as const) {
248
261
  if (management[field]) found.push(`management.${field}`);
249
262
  }
250
263
 
251
- for (const [name, user] of Object.entries(management.snmp?.users ?? {})) {
264
+ const snmp = device.monitoring?.snmp;
265
+
266
+ for (const [name, user] of Object.entries(snmp?.users ?? {})) {
252
267
  if (!RaisecomRosRender.SNMP_AUTH[user.auth])
253
- found.push(`management.snmp.users.${name}.auth ${user.auth}, the platform authenticates with SHA1 or MD5`);
268
+ found.push(`monitoring.snmp.users.${name}.auth ${user.auth}, the platform authenticates with SHA1 or MD5`);
254
269
  }
255
270
 
256
- if (management.snmp?.contact || management.snmp?.location) found.push("management.snmp.contact and location");
271
+ if (snmp?.contact || snmp?.location) found.push("monitoring.snmp.contact and location");
257
272
  if (management.ssh?.weak_crypto) found.push("management.ssh.weak_crypto");
258
273
 
259
- if (device.lldp && device.lldp !== true) found.push("lldp.interfaces, LLDP is per device");
260
- if (device.stp?.mode && device.stp.mode !== "mstp") found.push("stp.mode other than mstp");
274
+ const switching = device.switching;
275
+ if (switching?.lldp && switching.lldp !== true) found.push("switching.lldp.interfaces, LLDP is per device");
276
+ if (switching?.stp?.mode && switching.stp.mode !== "mstp") found.push("switching.stp.mode other than mstp");
261
277
 
262
- for (const [name, user] of Object.entries(device.users ?? {})) {
263
- if (user.role !== "admin") found.push(`users.${name}.role other than admin`);
278
+ for (const [name, user] of Object.entries(device.system?.users ?? {})) {
279
+ if (user && user.role !== "admin") found.push(`system.users.${name}.role other than admin`);
264
280
  }
265
281
 
266
282
  const system = device.system ?? {};
283
+ const services = device.services ?? {};
267
284
 
268
- if (system.dns || system.logging?.local || system.release_channel || system.banner || system.ntp?.serve) {
269
- found.push("system.dns, logging.local, release_channel, banner and ntp.serve");
285
+ if (system.dns || system.logging?.local || system.release_channel || system.banner || services.dns_server || services.ntp_server) {
286
+ found.push(
287
+ "system.dns, system.logging.local, system.release_channel, system.banner, services.dns_server and services.ntp_server"
288
+ );
270
289
  }
271
290
 
272
291
  // SNMP takes its own access-list; SSH and telnet share the management one.
@@ -682,7 +701,7 @@ export class RaisecomRos extends Platform {
682
701
  const opened = await SerialConsole.open(console);
683
702
 
684
703
  try {
685
- await SerialConsole.login(opened.channel, RaisecomRos.CONSOLE, user, password, console.passwordOnly);
704
+ await SerialConsole.login(opened.channel, RaisecomRos.CONSOLE, user, password, console.password_only);
686
705
  } catch (error) {
687
706
  opened.close();
688
707
  throw error;
@@ -695,7 +714,7 @@ export class RaisecomRos extends Platform {
695
714
  opened.close();
696
715
  },
697
716
  fresh: async () => {
698
- await SerialConsole.login(opened.channel, RaisecomRos.CONSOLE, user, password, console.passwordOnly);
717
+ await SerialConsole.login(opened.channel, RaisecomRos.CONSOLE, user, password, console.password_only);
699
718
  await Ssh.send(opened.channel, "terminal page-break disable", 15_000, 1000);
700
719
  return session;
701
720
  },
@@ -1,12 +1,10 @@
1
1
  import { Address } from "../../core/address";
2
2
  import { Secrets } from "../../core/secrets";
3
3
  import { Utils } from "../../core/utils";
4
- import type { Device, Ethertype, Port, Prefix, Severity, SnmpUser, Switched, Vrrp } from "../../schema";
4
+ import type { Device, Ethertype, Port, Prefix, Severity, SnmpUser, Switched, Vrrp } from "../../model";
5
5
  import { Catalog } from "../catalog";
6
6
  import { Derive } from "../derive";
7
7
 
8
- type AnyPort = Port<string, string, string, string>;
9
-
10
8
  /** The configuration a Raisecom ROS device should run. */
11
9
  export class RaisecomRosRender {
12
10
  /** The access-list that guards every management service. */
@@ -63,18 +61,18 @@ export class RaisecomRosRender {
63
61
  /** The whole configuration of a device. */
64
62
  static of(device: Device): string {
65
63
  const lines: string[] = [];
66
- const vlans = device.vlans ?? {};
64
+ const vlans = device.switching?.vlans ?? {};
67
65
  const vlanId = (name: string) => vlans[name]!.id;
68
66
  const management = device.management ?? {};
69
- const allow = management.allow ?? [];
67
+ const allow = RaisecomRosRender.managementAllow(device);
70
68
 
71
- const ids = Object.values(vlans).map((vlan) => vlan.id);
69
+ const ids = Object.values(vlans).flatMap((vlan) => (vlan ? [vlan.id] : []));
72
70
  if (ids.length) lines.push(`create vlan ${RaisecomRosRender.vlanList(ids)} active`, "!");
73
71
 
74
72
  const lagNumbers = Derive.lagIds(device);
75
73
 
76
- for (const [name, lag] of Object.entries(device.interfaces ?? {})) {
77
- if (lag.type !== "lag") continue;
74
+ for (const [name, lag] of Object.entries(device.interfaces?.lag ?? {})) {
75
+ if (!lag) continue;
78
76
 
79
77
  lines.push(`interface port-channel ${lagNumbers.get(name)}`);
80
78
  if (lag.description) lines.push(`description ${lag.description}`);
@@ -83,14 +81,15 @@ export class RaisecomRosRender {
83
81
  }
84
82
 
85
83
  if (allow.length) lines.push(...RaisecomRosRender.accessList(RaisecomRosRender.MANAGEMENT_ACL, "management", allow));
86
- const snmpSources = management.snmp?.allow;
84
+ const snmp = device.monitoring?.snmp;
85
+ const snmpSources = snmp?.allow;
87
86
  if (snmpSources) lines.push(...RaisecomRosRender.accessList(RaisecomRosRender.SNMP_ACL, "snmp", snmpSources));
88
87
 
89
88
  const ports = Catalog.ports(device.platform, device.model);
90
89
 
91
90
  // Each port that takes only some kinds of frame has a MAC access-list of its own, so changing one releases only that port.
92
91
  ports.forEach((port, index) => {
93
- const ethertypes = device.ports?.[port as keyof typeof device.ports]?.ethertypes;
92
+ const ethertypes = device.interfaces?.ethernet?.[port]?.ethertypes;
94
93
  if (!ethertypes) return;
95
94
 
96
95
  lines.push(`access-list ${RaisecomRosRender.ethertypeAcl(index)}`);
@@ -100,8 +99,8 @@ export class RaisecomRosRender {
100
99
  lines.push(" rule 100 deny any any", "!");
101
100
  });
102
101
 
103
- for (const [name, user] of Object.entries(device.users ?? {}))
104
- lines.push(`user name ${name} password ${Secrets.marker(user.password)}`);
102
+ for (const [name, user] of Object.entries(device.system?.users ?? {}))
103
+ if (user) lines.push(`user name ${name} password ${Secrets.marker(user.password)}`);
105
104
  if (management.privilege_password) lines.push(`enable password ${Secrets.marker(management.privilege_password)}`);
106
105
  lines.push(`hostname ${device.name}`);
107
106
 
@@ -115,7 +114,7 @@ export class RaisecomRosRender {
115
114
  lines.push("!");
116
115
 
117
116
  ports.forEach((port, index) => {
118
- const settings = device.ports?.[port as keyof typeof device.ports];
117
+ const settings = device.interfaces?.ethernet?.[port];
119
118
  lines.push(
120
119
  `interface ${Catalog.spell(device.platform, device.model, port)}`,
121
120
  ...RaisecomRosRender.physicalPort(settings, vlanId, lagNumbers),
@@ -126,8 +125,8 @@ export class RaisecomRosRender {
126
125
 
127
126
  const ospf = device.routing?.ospf;
128
127
 
129
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
130
- if (iface.type !== "vlan") continue;
128
+ for (const [name, iface] of Object.entries(device.interfaces?.vlan_interface ?? {})) {
129
+ if (!iface) continue;
131
130
 
132
131
  lines.push(`interface vlan ${vlanId(iface.vlan)}`);
133
132
 
@@ -153,7 +152,9 @@ export class RaisecomRosRender {
153
152
  lines.push(`router ospf ${RaisecomRosRender.OSPF_PROCESS}${routerId ? ` router-id ${routerId}` : ""}`);
154
153
 
155
154
  for (const [areaId, area] of Object.entries(ospf.areas)) {
156
- for (const iface of Object.keys(area.interfaces)) {
155
+ for (const [iface, settings] of Object.entries(area.interfaces)) {
156
+ if (!settings) continue;
157
+
157
158
  for (const prefix of Derive.ipv4Networks(device, iface)) {
158
159
  lines.push(`network ${Address.host(prefix)} ${Address.wildcard(Address.split(prefix).len)} area ${areaId}`);
159
160
  }
@@ -165,18 +166,18 @@ export class RaisecomRosRender {
165
166
  }
166
167
 
167
168
  // Storm control acts only once detection is on.
168
- if (Object.values(device.ports ?? {}).some((port) => port?.storm_control)) lines.push("storm-control detection enable");
169
- if (device.system?.transceiver_monitoring) lines.push("transceiver ddm enable");
169
+ if (Derive.ports(device).some(([, port]) => port.storm_control)) lines.push("storm-control detection enable");
170
+ if (device.monitoring?.transceivers) lines.push("transceiver ddm enable");
170
171
  // LLDP and spanning tree are off by default and turned on by the config.
171
- if (device.lldp) lines.push("lldp enable");
172
- if (device.stp) {
172
+ const switching = device.switching;
173
+ if (switching?.lldp) lines.push("lldp enable");
174
+ if (switching?.stp) {
173
175
  lines.push("spanning-tree enable");
174
- if (device.stp.priority !== undefined) lines.push(`spanning-tree priority ${device.stp.priority}`);
176
+ if (switching.stp.priority !== undefined) lines.push(`spanning-tree priority ${switching.stp.priority}`);
175
177
  }
176
178
 
177
179
  // The factory communities survive a reset, and one of them can write, so they are always removed.
178
180
  lines.push("no snmp-server community public", "no snmp-server community private");
179
- const snmp = management.snmp;
180
181
  if (snmp?.community) lines.push(`snmp-server community ${Secrets.marker(snmp.community)} ro`);
181
182
 
182
183
  const snmpUsers = Object.entries(snmp?.users ?? {});
@@ -201,12 +202,12 @@ export class RaisecomRosRender {
201
202
  lines.push(`bfd ${session.id} bind peer-ip ${via} interface vlan ${vlanId(session.vlan)}`, "session enable", "!");
202
203
  }
203
204
 
204
- for (const route of device.routing?.static ?? []) {
205
+ for (const route of device.routing?.static_routes ?? []) {
205
206
  const { addr, len } = Address.split(route.prefix);
206
- if (Address.family(addr) !== "ipv4" || !route.via) continue;
207
+ if (Address.family(addr) !== "ipv4" || !route.next_hop) continue;
207
208
 
208
- const session = route.bfd ? sessions.get(route.via) : undefined;
209
- lines.push(`ip route ${addr} ${Address.netmask(len)} ${route.via}${session ? ` track bfd ${session.id}` : ""}`);
209
+ const session = route.bfd ? sessions.get(route.next_hop) : undefined;
210
+ lines.push(`ip route ${addr} ${Address.netmask(len)} ${route.next_hop}${session ? ` track bfd ${session.id}` : ""}`);
210
211
  }
211
212
 
212
213
  if (management.telnet) {
@@ -222,7 +223,7 @@ export class RaisecomRosRender {
222
223
  const ntp = device.system?.ntp;
223
224
  if (ntp?.servers.length) lines.push("ntp enable", ...ntp.servers.map((server) => `ntp server ${server}`));
224
225
 
225
- for (const host of device.system?.logging?.remote ?? []) {
226
+ for (const host of device.monitoring?.syslog ?? []) {
226
227
  const level = RaisecomRosRender.LOG_LEVELS[host.level];
227
228
  const port = host.port ?? RaisecomRosRender.LOG_DEFAULTS.port;
228
229
  let line = `logging host ${host.address}`;
@@ -234,6 +235,14 @@ export class RaisecomRosRender {
234
235
  return lines.join("\n") + "\n";
235
236
  }
236
237
 
238
+ /** The sources every management service takes. The project has already turned an address set's name here into the set's addresses. */
239
+ private static managementAllow(device: Device): readonly Prefix[] {
240
+ const allow = device.management?.allow ?? [];
241
+ if (typeof allow === "string")
242
+ throw new Error(`${device.name}: management.allow is still the address set ${allow}, not its addresses`);
243
+ return allow;
244
+ }
245
+
237
246
  /** An access-list permitting `sources`, then denying the rest. Its description takes one word. */
238
247
  private static accessList(number: number, description: string, sources: readonly Prefix[]): string[] {
239
248
  const rules = sources.map(
@@ -245,7 +254,7 @@ export class RaisecomRosRender {
245
254
  }
246
255
 
247
256
  /** Access or trunk membership, for a port or a LAG. */
248
- private static switching(settings: Switched<string>, vlanId: (name: string) => number): string[] {
257
+ private static switching(settings: Switched, vlanId: (name: string) => number): string[] {
249
258
  const lines: string[] = [];
250
259
  if (settings.access_vlan) lines.push(`switchport access vlan ${vlanId(settings.access_vlan)}`);
251
260
 
@@ -271,16 +280,12 @@ export class RaisecomRosRender {
271
280
  }
272
281
 
273
282
  /** A port the config does not declare is shut down. A LAG member holds its membership and description; the LAG holds the rest. */
274
- private static physicalPort(
275
- settings: AnyPort | undefined,
276
- vlanId: (name: string) => number,
277
- lagNumbers: Map<string, number>
278
- ): string[] {
283
+ private static physicalPort(settings: Port | undefined, vlanId: (name: string) => number, lagNumbers: Map<string, number>): string[] {
279
284
  if (!settings) return ["shutdown"];
280
285
 
281
286
  const lines: string[] = [];
282
287
  if (settings.description) lines.push(`description ${settings.description}`);
283
- if (settings.lag) return [...lines, `port-channel ${lagNumbers.get(settings.lag)}`];
288
+ if (settings.in_lag) return [...lines, `port-channel ${lagNumbers.get(settings.in_lag)}`];
284
289
 
285
290
  lines.push(...RaisecomRosRender.switching(settings, vlanId));
286
291
 
@@ -299,29 +304,28 @@ export class RaisecomRosRender {
299
304
  return lines;
300
305
  }
301
306
 
302
- /** The MAC access-list of the port at `index` in the catalog, for its `ethertypes`. MAC access-lists are 3000 and up. */
303
307
  /** The BFD session for each next hop a static route watches, numbered from 1 in the order the routes come, with the VLAN its interface is on. A next hop on no VLAN interface's network has none. */
304
308
  static bfdSessions(device: Device): Map<string, { id: number; vlan: string }> {
305
309
  const sessions = new Map<string, { id: number; vlan: string }>();
306
310
 
307
- for (const route of device.routing?.static ?? []) {
308
- if (!route.bfd || !route.via || sessions.has(route.via)) continue;
311
+ for (const route of device.routing?.static_routes ?? []) {
312
+ const nextHop = route.next_hop;
313
+ if (!route.bfd || !nextHop || sessions.has(nextHop)) continue;
309
314
 
310
- const holder = Object.values(device.interfaces ?? {}).find(
311
- (iface) =>
312
- iface.type === "vlan" &&
313
- (iface.addresses ?? []).some((entry) => {
314
- const address = typeof entry === "string" ? entry : entry.address;
315
- return Address.family(address) === "ipv4" && Address.contains(Address.prefixOf(address), route.via!);
316
- })
315
+ const holder = Object.values(device.interfaces?.vlan_interface ?? {}).find((iface) =>
316
+ (iface?.addresses ?? []).some((entry) => {
317
+ const address = typeof entry === "string" ? entry : entry.address;
318
+ return Address.family(address) === "ipv4" && Address.contains(Address.prefixOf(address), nextHop);
319
+ })
317
320
  );
318
321
 
319
- if (holder?.type === "vlan") sessions.set(route.via, { id: sessions.size + 1, vlan: holder.vlan });
322
+ if (holder) sessions.set(nextHop, { id: sessions.size + 1, vlan: holder.vlan });
320
323
  }
321
324
 
322
325
  return sessions;
323
326
  }
324
327
 
328
+ /** The MAC access-list of the port at `index` in the catalog, for its `ethertypes`. MAC access-lists are 3000 and up. */
325
329
  static ethertypeAcl(index: number): number {
326
330
  return 3001 + index;
327
331
  }
@@ -3,7 +3,7 @@
3
3
  Behaviour that looks like something else. Each one has been hit on a real device.
4
4
 
5
5
  - Every command takes effect as it arrives; there is no candidate configuration. `apply` saves a backup, arms a scheduler that restores it, and restores at once if a command is refused.
6
- - A `/32` address gives no connected route, so a session over it never opens. Give the far end: `{ address, peer }` renders `network=<peer>`.
6
+ - A `/32` address gives no connected route, so a session over it never opens. Give the far end: `{ address, peer_address }` renders `network=<peer_address>`.
7
7
  - `[find address=X/Y]` unquoted matches nothing, silently. Every selector the plan builds is quoted.
8
8
  - `~` in `find` has no anchors. Match exactly.
9
9
  - A property of the same group is printed as a bare suffix, `output.filter-chain=X .network=Y`, which only reads back inside that command. The parser expands it before building a `set`.
@@ -64,8 +64,8 @@ Behaviour that looks like something else. Each one has been hit on a real device
64
64
  - No MAC learning limit on 7.23.5: `/interface bridge port` has `learn` but no limit, and `/interface ethernet switch port` has none (CCR2216). `mac_limit` stays refused.
65
65
  - A switch rule matches a kind of frame with `mac-protocol` (`ip`, `ipv6`, `arp` among 23 kinds), but cannot match "not" one. An ethertype filter is a rule that lets each kind through and a last one with an empty `new-dst-ports` for the rest, after the port's ACL rules, which match only `ip` or `ipv6`. Four such rules on a free port took the router to 70 switch rules, none invalid (CCR2216, 7.23.5).
66
66
  - A routing table holds no routes the router forwards by unless it has `fib`: `/routing table add fib name=...`. The export prints the flag between the properties, `add comment=... fib name=...`.
67
- - A BGP connection's `routing-table` is where the session installs what it learns and also where it announces from: an `output.network` prefix is announced only while that table has a route for it, and a route in main alone is not enough. On a CCR2216 on 7.23.5, a fresh session with `routing-table=<table>` installed its routes there, active, with the next hop resolved through main, and announced nothing until the table had the prefix too; then it did. Circuit's validate refuses a session in a table that holds none of `networks`, and warns for each it lacks. Routing filters cannot move a route to a table: `set routing-table` and `set table` are `invalid argument`.
68
- - A running session takes a new `routing-table` only when it restarts, but its announcements stop at once: set from main to a table, it withdrew everything within seconds, kept its learned routes in main, and announced nothing even once the table had the prefix; set back to main, it still announced nothing until it restarted. `/routing bgp session resend afi=ipv6` did not help either, on the router where this was found (#157). Disabling and enabling the connection restarted it in the new table. The plan does exactly that, written on the plan's line, and after an apply Circuit checks that every session still announces what it announced of `networks` before (CCR2216, 7.23.5).
67
+ - A BGP connection's `routing-table` is where the session installs what it learns and also where it announces from: an `output.network` prefix is announced only while that table has a route for it, and a route in main alone is not enough. On a CCR2216 on 7.23.5, a fresh session with `routing-table=<table>` installed its routes there, active, with the next hop resolved through main, and announced nothing until the table had the prefix too; then it did. Circuit's validate refuses a session in a table that holds none of `originated_prefixes`, and warns for each it lacks. Routing filters cannot move a route to a table: `set routing-table` and `set table` are `invalid argument`.
68
+ - A running session takes a new `routing-table` only when it restarts, but its announcements stop at once: set from main to a table, it withdrew everything within seconds, kept its learned routes in main, and announced nothing even once the table had the prefix; set back to main, it still announced nothing until it restarted. `/routing bgp session resend afi=ipv6` did not help either, on the router where this was found (#157). Disabling and enabling the connection restarted it in the new table. The plan does exactly that, written on the plan's line, and after an apply Circuit checks that every session still announces what it announced of `originated_prefixes` before (CCR2216, 7.23.5).
69
69
  - A passive connection without `local.address` listens only on its default local address, `local.default-address`, which may be a public one: a peer connecting to another address of the router gets a TCP reset (CCR2216, 7.23.5).
70
70
  - `/routing rule` is an ordered list without names, like a filter chain, and the plan rewrites it whole in the config's order. `lookup-only-in-table` drops a packet its table has no route for (a ping printed `no route`); `lookup` routes it by main instead (the ping left). An interface to match is `interface=`. A `drop` or `unreachable` rule is printed with `!realm`, which Circuit does not write (CCR2216, 7.23.5).
71
71
  - An eBGP connection to an address on no connected network, without `multihop=yes`, takes the TCP connection, logs `connection <local> from <remote>`, and closes it with no OPEN and nothing else in the log, even with `bgp,debug` logging on (CCR2216, 7.23.5).