@takodotid/circuit 0.34.3 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +14 -15
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11345 -9024
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +18 -12
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -700
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
@@ -0,0 +1,476 @@
1
+ // OSPF, OSPFv3 and BGP, with the route maps, prefix lists and community lists BGP's policies become.
2
+
3
+ import { Address } from "../../core/address";
4
+ import { Secrets } from "../../core/secrets";
5
+ import type { Device, Family, Neighbor, NeighborSettings, PolicyRule } from "../../schema";
6
+ import { FortiOsNames } from "./names";
7
+ import { FortiOsBlock, FortiOsTree } from "./tree";
8
+
9
+ type AnyRule = PolicyRule<string, string>;
10
+ type AnyNeighbor = Neighbor<string, string>;
11
+
12
+ /** The lengths a prefix list's rule takes within its prefix. */
13
+ type Lengths = { ge?: number; le?: number };
14
+
15
+ /** Each policy as a route map for each family, built once, with the lists its rules match. */
16
+ class RouteMaps {
17
+ private readonly built = new Set<string>();
18
+
19
+ constructor(
20
+ private readonly root: FortiOsBlock,
21
+ private readonly device: Device
22
+ ) {}
23
+
24
+ /** A name made of a value's words, for a list named after what it matches: `64500:*` becomes `64500-any`. */
25
+ private static slug(value: string): string {
26
+ return value.replace(/\*/g, "any").replace(/[^A-Za-z0-9]+/g, "-");
27
+ }
28
+
29
+ /** A community pattern as a regular expression, `*` standing for any value of its field. */
30
+ private static patternOf(pattern: string): string {
31
+ return `^${pattern.replace(/\*/g, "[0-9]+")}$`;
32
+ }
33
+
34
+ /** The route map for a policy in one family; one that rejects everything when there is no policy. */
35
+ of(policy: string | undefined, family: Family): string {
36
+ if (!policy) {
37
+ if (!this.built.has(FortiOsRouting.REJECT_ALL)) {
38
+ this.built.add(FortiOsRouting.REJECT_ALL);
39
+ this.root.block("router route-map").entry(FortiOsRouting.REJECT_ALL).block("rule").entry(1).set("action", "deny");
40
+ }
41
+
42
+ return FortiOsRouting.REJECT_ALL;
43
+ }
44
+
45
+ const name = family === "ipv4" ? policy : `${policy}-v6`;
46
+ if (this.built.has(name)) return name;
47
+ this.built.add(name);
48
+
49
+ const map = this.root.block("router route-map").entry(name);
50
+ let number = 0;
51
+
52
+ for (const rule of FortiOsRouting.flatten(this.device, policy)) {
53
+ const only = FortiOsRouting.familyOfRule(this.device, rule);
54
+ if (only && only !== family) continue;
55
+
56
+ this.rule(map.block("rule").entry(++number), rule, family, `${name}-${number}`);
57
+ }
58
+
59
+ return name;
60
+ }
61
+
62
+ private rule(entry: FortiOsBlock, rule: AnyRule, family: Family, label: string): void {
63
+ const match = rule.match ?? {};
64
+ const set = rule.set ?? {};
65
+ const address = family === "ipv4" ? "match-ip-address" : "match-ip6-address";
66
+ const bits = FortiOsRouting.BITS[family];
67
+
68
+ if (match.prefix) {
69
+ this.list(label, family, [[match.prefix, {}]]);
70
+ entry.set(address, FortiOsTree.quote(label));
71
+ } else if (match.prefix_set && match.prefix_length) {
72
+ const name = `${match.prefix_set}-${match.prefix_length.min ?? 0}-${match.prefix_length.max ?? bits}`;
73
+ const prefixes = this.prefixesOf(match.prefix_set);
74
+ this.list(
75
+ name,
76
+ family,
77
+ prefixes.map((prefix) => [
78
+ prefix,
79
+ FortiOsRouting.bounds(prefix, family, match.prefix_length?.min, match.prefix_length?.max),
80
+ ])
81
+ );
82
+ entry.set(address, FortiOsTree.quote(name));
83
+ } else if (match.prefix_set) {
84
+ entry.set(address, FortiOsTree.quote(match.prefix_set));
85
+ } else if (match.prefix_length) {
86
+ const { min = 0, max = bits } = match.prefix_length;
87
+ const name = `circuit-length-${min}-${max}`;
88
+ const everything = family === "ipv4" ? "0.0.0.0/0" : "::/0";
89
+ this.list(name, family, [[everything, FortiOsRouting.bounds(everything, family, min, max)]]);
90
+ entry.set(address, FortiOsTree.quote(name));
91
+ }
92
+
93
+ if (match.community)
94
+ entry.set(
95
+ "match-community",
96
+ FortiOsTree.quote(this.communities(`circuit-match-${RouteMaps.slug(match.community)}`, [match.community], false))
97
+ );
98
+
99
+ if (set.local_pref !== undefined) entry.set("set-local-preference", set.local_pref);
100
+ if (set.med !== undefined) entry.set("set-metric", set.med);
101
+ if (set.prepend !== undefined)
102
+ entry.set("set-aspath", FortiOsTree.quote(Array(set.prepend).fill(this.device.routing!.bgp!.asn).join(" ")));
103
+
104
+ // Replacing the communities makes removing some needless; added ones join the new ones.
105
+ if (set.communities) {
106
+ entry.set("set-community", FortiOsTree.quoteAll([...set.communities, ...(set.add_communities ?? [])]));
107
+ } else {
108
+ if (set.add_communities)
109
+ entry.set("set-community", FortiOsTree.quoteAll(set.add_communities)).set("set-community-additive", "enable");
110
+
111
+ if (set.remove_communities) {
112
+ const name = `circuit-remove-${RouteMaps.slug(set.remove_communities.join(" "))}`;
113
+ entry.set("set-community-delete", FortiOsTree.quote(this.communities(name, set.remove_communities, true)));
114
+ }
115
+ }
116
+
117
+ if (set.next_hop && Address.family(set.next_hop) === family)
118
+ entry.set(family === "ipv4" ? "set-ip-nexthop" : "set-ip6-nexthop", set.next_hop);
119
+
120
+ // Permit is the default and is not printed.
121
+ if (rule.action === "reject") entry.set("action", "deny");
122
+ }
123
+
124
+ /** The prefixes of a set the config lists. */
125
+ private prefixesOf(set: string): string[] {
126
+ const prefixes = this.device.prefix_sets?.[set];
127
+ return prefixes && "prefixes" in prefixes ? [...prefixes.prefixes] : [];
128
+ }
129
+
130
+ /** A prefix list of its own, rule by rule; a prefix no length fits is left out. */
131
+ private list(name: string, family: Family, rules: [string, Lengths | undefined][]): void {
132
+ const rulesBlock = this.root.block(FortiOsRouting.PREFIX_LIST[family]).entry(name).block("rule");
133
+ let number = 0;
134
+
135
+ for (const [prefix, lengths] of rules) if (lengths) FortiOsRouting.listRule(rulesBlock.entry(++number), family, prefix, lengths);
136
+ }
137
+
138
+ /** A community list matching these communities, or, expanded, these patterns. */
139
+ private communities(name: string, values: readonly string[], patterns: boolean): string {
140
+ const list = this.root.block("router community-list").entry(name);
141
+ if (patterns) list.set("type", "expanded");
142
+
143
+ values.forEach((value, index) => {
144
+ const rule = list
145
+ .block("rule")
146
+ .entry(index + 1)
147
+ .set("action", "permit");
148
+ if (patterns) rule.set("regexp", FortiOsTree.quote(RouteMaps.patternOf(value)));
149
+ else rule.set("match", FortiOsTree.quote(value));
150
+ });
151
+
152
+ return name;
153
+ }
154
+ }
155
+
156
+ export class FortiOsRouting {
157
+ /** The route map a session takes when its config states none, so it learns or announces nothing. */
158
+ static readonly REJECT_ALL = "circuit-reject-all";
159
+
160
+ /** The start of the comment on a prefix list `refresh` fills. The plan keeps the entries the device holds in one. */
161
+ static readonly FILLED = "Filled by circuit refresh";
162
+
163
+ /** The table a prefix list of one family sits in. */
164
+ static readonly PREFIX_LIST = { ipv4: "router prefix-list", ipv6: "router prefix-list6" } as const;
165
+
166
+ /** The longest prefix of each family. */
167
+ static readonly BITS = { ipv4: 32, ipv6: 128 } as const;
168
+
169
+ /** A neighbor's settings with its group's beneath them. */
170
+ static neighborSettings(device: Device, neighbor: AnyNeighbor): NeighborSettings<string> {
171
+ const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
172
+ return { ...group, ...neighbor };
173
+ }
174
+
175
+ /** The families a session carries: its own, or the family of its address. */
176
+ static familiesOfNeighbor(device: Device, neighbor: AnyNeighbor): Family[] {
177
+ return [...(FortiOsRouting.neighborSettings(device, neighbor).families ?? [Address.family(neighbor.address)])];
178
+ }
179
+
180
+ /** Whether a static route, OSPF or BGP detects a dead neighbor with BFD, which the device then runs. */
181
+ static usesBfd(device: Device): boolean {
182
+ const routes = (device.routing?.static ?? []).some((route) => route.bfd);
183
+ const areas = Object.values(device.routing?.ospf?.areas ?? {});
184
+ const ospf = areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
185
+ const bgp = Object.values(device.routing?.bgp?.neighbors ?? {}).some(
186
+ (neighbor) => FortiOsRouting.neighborSettings(device, neighbor).bfd
187
+ );
188
+ return routes || ospf || bgp;
189
+ }
190
+
191
+ static render(root: FortiOsBlock, device: Device): void {
192
+ FortiOsRouting.prefixLists(root, device);
193
+ FortiOsRouting.ospf(root, device);
194
+ FortiOsRouting.ospf6(root, device);
195
+ FortiOsRouting.bgp(root, device);
196
+ }
197
+
198
+ /** A prefix as the platform writes it in a prefix list of its family. */
199
+ private static listPrefix(prefix: string, family: Family): string {
200
+ return family === "ipv4" ? FortiOsNames.subnet(FortiOsNames.network(prefix)) : FortiOsNames.network(prefix);
201
+ }
202
+
203
+ /** One rule of a prefix list: a prefix, and the lengths it takes within it. The device prints a bound the rule does not hold as `unset`. */
204
+ static listRule(rule: FortiOsBlock, family: Family, prefix: string, lengths: Lengths = {}): void {
205
+ rule.set(family === "ipv4" ? "prefix" : "prefix6", FortiOsRouting.listPrefix(prefix, family));
206
+ rule.set("ge", lengths.ge ?? FortiOsTree.UNSET);
207
+ rule.set("le", lengths.le ?? FortiOsTree.UNSET);
208
+ }
209
+
210
+ /** The lengths a rule takes within a prefix to match lengths from `min` to `max`, or none when no length fits. A bound equal to the prefix's own length is left out. */
211
+ static bounds(prefix: string, family: Family, min = 0, max: number = FortiOsRouting.BITS[family]): Lengths | undefined {
212
+ const own = Address.split(FortiOsNames.network(prefix)).len;
213
+ const low = Math.max(min, own);
214
+ if (max < low) return undefined;
215
+ if (low === own && max === own) return {};
216
+
217
+ return { ...(low > own ? { ge: low } : {}), le: max };
218
+ }
219
+
220
+ /** Each prefix set as a prefix list matching its prefixes and any more specific. One a registry fills starts with a rule that denies every prefix, so it matches nothing until `refresh` fills it. */
221
+ private static prefixLists(root: FortiOsBlock, device: Device): void {
222
+ for (const [name, set] of Object.entries(device.prefix_sets ?? {})) {
223
+ if ("source" in set) {
224
+ const { registry, query, family } = set.source;
225
+ const entry = root
226
+ .block(FortiOsRouting.PREFIX_LIST[family])
227
+ .entry(name)
228
+ .set("comments", FortiOsTree.quote(`${FortiOsRouting.FILLED} from ${registry} ${query}`));
229
+ // Until `refresh` fills it, the list refuses every prefix, so a rule matching it matches nothing.
230
+ const rule = entry.block("rule").entry(1);
231
+ FortiOsRouting.listRule(rule, family, family === "ipv4" ? "0.0.0.0/0" : "::/0", { le: FortiOsRouting.BITS[family] });
232
+ rule.set("action", "deny");
233
+ continue;
234
+ }
235
+
236
+ const family = set.prefixes[0] ? FortiOsNames.familyOfPrefix(set.prefixes[0]) : "ipv4";
237
+ const entry = root.block(FortiOsRouting.PREFIX_LIST[family]).entry(name);
238
+ set.prefixes.forEach((prefix, index) =>
239
+ FortiOsRouting.listRule(entry.block("rule").entry(index + 1), family, prefix, FortiOsRouting.bounds(prefix, family))
240
+ );
241
+ }
242
+ }
243
+
244
+ /** The networks of an interface's addresses of one family, for an OSPF area to announce. */
245
+ private static networksOf(device: Device, name: string, family: Family): string[] {
246
+ return FortiOsNames.addressesOf(FortiOsNames.settingsOf(device, name))
247
+ .filter((address) => FortiOsNames.familyOfPrefix(address) === family)
248
+ .map(FortiOsNames.network);
249
+ }
250
+
251
+ /** OSPF for IPv4. An area runs on the networks of its interfaces; an interface named with a cost, a link type or BFD has settings of its own; a passive one forms no adjacency. */
252
+ private static ospf(root: FortiOsBlock, device: Device): void {
253
+ const ospf = device.routing?.ospf;
254
+ if (!ospf || !(ospf.families ?? ["ipv4"]).includes("ipv4")) return;
255
+
256
+ const block = root.block("router ospf");
257
+ if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
258
+
259
+ const passive: string[] = [];
260
+ let networks = 0;
261
+
262
+ for (const [areaId, area] of Object.entries(ospf.areas)) {
263
+ block.block("area").entry(areaId, true);
264
+
265
+ for (const [name, settings] of Object.entries(area.interfaces)) {
266
+ const iface = FortiOsNames.nameOf(device, name);
267
+ if (settings?.passive) passive.push(iface);
268
+
269
+ if (settings?.cost !== undefined || settings?.network === "point-to-point" || settings?.bfd) {
270
+ const entry = block.block("ospf-interface").entry(iface);
271
+ entry.set("interface", FortiOsTree.quote(iface));
272
+ if (settings.cost !== undefined) entry.set("cost", settings.cost);
273
+ if (settings.network === "point-to-point") entry.set("network-type", "point-to-point");
274
+ if (settings.bfd) entry.set("bfd", "enable");
275
+ }
276
+
277
+ for (const prefix of FortiOsRouting.networksOf(device, name, "ipv4")) {
278
+ const entry = block.block("network").entry(++networks);
279
+ entry.set("prefix", FortiOsNames.subnet(prefix));
280
+ // The backbone is the platform's default area and is not printed.
281
+ if (areaId !== "0.0.0.0") entry.set("area", areaId);
282
+ }
283
+ }
284
+ }
285
+
286
+ if (passive.length) block.set("passive-interface", FortiOsTree.quoteAll(passive));
287
+ for (const source of ospf.redistribute ?? []) block.block(`redistribute "${source}"`).set("status", "enable");
288
+ }
289
+
290
+ /** OSPFv3 for IPv6. Every interface of an area runs it in that area. */
291
+ private static ospf6(root: FortiOsBlock, device: Device): void {
292
+ const ospf = device.routing?.ospf;
293
+ if (!ospf?.families?.includes("ipv6")) return;
294
+
295
+ const block = root.block("router ospf6");
296
+ if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
297
+
298
+ const passive: string[] = [];
299
+
300
+ for (const [areaId, area] of Object.entries(ospf.areas)) {
301
+ block.block("area").entry(areaId, true);
302
+
303
+ for (const [name, settings] of Object.entries(area.interfaces)) {
304
+ const iface = FortiOsNames.nameOf(device, name);
305
+ if (settings?.passive) passive.push(iface);
306
+
307
+ const entry = block.block("ospf6-interface").entry(iface);
308
+ entry.set("interface", FortiOsTree.quote(iface));
309
+ if (areaId !== "0.0.0.0") entry.set("area-id", areaId);
310
+ if (settings?.cost !== undefined) entry.set("cost", settings.cost);
311
+ if (settings?.network === "point-to-point") entry.set("network-type", "point-to-point");
312
+ if (settings?.bfd) entry.set("bfd", "enable");
313
+ }
314
+ }
315
+
316
+ if (passive.length) block.set("passive-interface", FortiOsTree.quoteAll(passive));
317
+ for (const source of ospf.redistribute ?? []) block.block(`redistribute "${source}"`).set("status", "enable");
318
+ }
319
+
320
+ /** BGP: each session with its settings and group's, the route maps its policies become for each family it carries, and the prefixes the device originates. */
321
+ private static bgp(root: FortiOsBlock, device: Device): void {
322
+ const bgp = device.routing?.bgp;
323
+ if (!bgp) return;
324
+
325
+ const block = root.block("router bgp");
326
+ block.set("as", bgp.asn);
327
+ if (device.routing?.router_id) block.set("router-id", device.routing.router_id);
328
+
329
+ const maps = new RouteMaps(root, device);
330
+
331
+ for (const neighbor of Object.values(bgp.neighbors ?? {})) {
332
+ const settings = FortiOsRouting.neighborSettings(device, neighbor);
333
+ const families = FortiOsRouting.familiesOfNeighbor(device, neighbor);
334
+ const entry = block.block("neighbor").entry(neighbor.address);
335
+
336
+ // Both families are active unless told otherwise.
337
+ if (!families.includes("ipv4")) entry.set("activate", "disable");
338
+ if (!families.includes("ipv6")) entry.set("activate6", "disable");
339
+ if (settings.bfd) entry.set("bfd", "enable");
340
+ if (settings.multihop) entry.set("ebgp-enforce-multihop", "enable");
341
+ if (settings.passive) entry.set("passive", "enable");
342
+ if (settings.description) entry.set("description", FortiOsTree.quote(settings.description));
343
+ if (settings.remote_as !== undefined) entry.set("remote-as", settings.remote_as);
344
+
345
+ for (const family of families) {
346
+ const suffix = family === "ipv4" ? "" : "6";
347
+ if (settings.max_prefixes !== undefined) entry.set(`maximum-prefix${suffix}`, settings.max_prefixes);
348
+ entry.set(`route-map-in${suffix}`, FortiOsTree.quote(maps.of(settings.import, family)));
349
+ entry.set(`route-map-out${suffix}`, FortiOsTree.quote(maps.of(settings.export, family)));
350
+ }
351
+
352
+ if (settings.keepalive !== undefined) entry.set("keep-alive-timer", settings.keepalive);
353
+ if (settings.hold_time !== undefined) entry.set("holdtime-timer", settings.hold_time);
354
+
355
+ const source = settings.local_address ? FortiOsNames.holderOf(device, settings.local_address) : undefined;
356
+ if (source) entry.set("update-source", FortiOsTree.quote(FortiOsNames.nameOf(device, source)));
357
+
358
+ if (settings.password) entry.set("password", FortiOsTree.quote(Secrets.marker(settings.password)));
359
+ }
360
+
361
+ const networks = { ipv4: 0, ipv6: 0 };
362
+
363
+ for (const prefix of bgp.networks ?? []) {
364
+ const family = FortiOsNames.familyOfPrefix(prefix);
365
+ const entry = block.block(family === "ipv4" ? "network" : "network6").entry(++networks[family]);
366
+ entry.set(family === "ipv4" ? "prefix" : "prefix6", FortiOsRouting.listPrefix(prefix, family));
367
+ }
368
+ }
369
+
370
+ /** The family a rule is limited to, by its match: a family, a prefix, or a prefix set of one family. */
371
+ static familyOfRule(device: Device, rule: AnyRule): Family | undefined {
372
+ const match = rule.match;
373
+ if (match?.family) return match.family;
374
+ if (match?.prefix) return FortiOsNames.familyOfPrefix(match.prefix);
375
+
376
+ const set = match?.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
377
+ if (set && "source" in set) return set.source.family;
378
+ if (set?.prefixes[0]) return FortiOsNames.familyOfPrefix(set.prefixes[0]);
379
+
380
+ return undefined;
381
+ }
382
+
383
+ /** A policy's rules with each call replaced by the rules it calls, in order. A called policy that decides nothing hands back to the caller. */
384
+ static flatten(device: Device, policy: string, seen: string[] = [policy]): AnyRule[] {
385
+ const flat: AnyRule[] = [];
386
+
387
+ for (const rule of device.policies?.[policy] ?? []) {
388
+ if (rule.call && !seen.includes(rule.call)) flat.push(...FortiOsRouting.flatten(device, rule.call, [...seen, rule.call]));
389
+ if (rule.match || rule.set || rule.action) flat.push(rule);
390
+ }
391
+
392
+ return flat;
393
+ }
394
+
395
+ /** Routing the platform cannot express, or that this adapter does not yet write. */
396
+ static gaps(device: Device): string[] {
397
+ const found: string[] = [];
398
+ const routing = device.routing;
399
+
400
+ if (routing?.rpki) found.push("routing.rpki, the platform has no route origin validation");
401
+ if (routing?.router_id && !routing.ospf && !routing.bgp) found.push("routing.router_id without OSPF or BGP, which each hold it");
402
+
403
+ const ospf = routing?.ospf;
404
+ if (ospf?.families?.includes("ipv6") && !routing?.router_id) found.push("routing.ospf for IPv6 without routing.router_id");
405
+
406
+ for (const area of Object.values(ospf?.areas ?? {})) {
407
+ for (const name of Object.keys(area.interfaces)) {
408
+ if (FortiOsNames.settingsOf(device, name)?.vrf)
409
+ found.push(`routing.ospf on ${name}, which routes in a VRF: OSPF runs in the default table`);
410
+
411
+ const ipv4 = FortiOsNames.addressesOf(FortiOsNames.settingsOf(device, name)).some(
412
+ (address) => Address.family(Address.host(address)) === "ipv4"
413
+ );
414
+ if ((ospf?.families ?? ["ipv4"]).includes("ipv4") && !ipv4)
415
+ found.push(`routing.ospf on ${name}, which has no IPv4 address for OSPF to run on`);
416
+ }
417
+ }
418
+
419
+ for (const [name, neighbor] of Object.entries(routing?.bgp?.neighbors ?? {})) {
420
+ const where = `routing.bgp.neighbors.${name}`;
421
+ const settings = FortiOsRouting.neighborSettings(device, neighbor);
422
+
423
+ if (settings.role) found.push(`${where}.role, the platform has no BGP roles`);
424
+ if (settings.bfd && settings.multihop)
425
+ found.push(
426
+ `${where}.bfd, FortiOS 6.2 runs single-hop BFD on a multihop session, which a peer running multihop BFD never answers`
427
+ );
428
+ if (settings.local_address && !FortiOsNames.holderOf(device, settings.local_address)) {
429
+ found.push(`${where}.local_address, no interface holds ${settings.local_address}`);
430
+ }
431
+
432
+ if (Address.family(neighbor.address) === "ipv6" && FortiOsRouting.familiesOfNeighbor(device, neighbor).includes("ipv4")) {
433
+ found.push(`${where}.families ipv4 over an IPv6 session`);
434
+ }
435
+ }
436
+
437
+ for (const [name, rules] of Object.entries(device.policies ?? {})) {
438
+ rules.forEach((rule, index) => found.push(...FortiOsRouting.ruleGaps(device, `policies.${name}[${index}]`, rule)));
439
+ }
440
+
441
+ return found;
442
+ }
443
+
444
+ private static ruleGaps(device: Device, where: string, rule: AnyRule): string[] {
445
+ const found: string[] = [];
446
+ const match = rule.match ?? {};
447
+ const set = rule.set ?? {};
448
+
449
+ if ((rule.match || rule.set) && !rule.action)
450
+ found.push(`${where}, a rule without action: the platform stops at the first rule that matches`);
451
+
452
+ for (const field of ["as_path_length", "through_as", "rpki", "large_community"] as const) {
453
+ if (match[field] !== undefined) found.push(`${where}.match.${field}`);
454
+ }
455
+
456
+ for (const field of [
457
+ "large_communities",
458
+ "add_large_communities",
459
+ "remove_large_communities",
460
+ "preferred_source",
461
+ "blackhole",
462
+ ] as const) {
463
+ if (set[field] !== undefined) found.push(`${where}.set.${field}`);
464
+ }
465
+
466
+ if (match.prefix && match.prefix_length) found.push(`${where}.match.prefix with prefix_length`);
467
+ if (match.prefix && match.prefix_set) found.push(`${where}.match.prefix with prefix_set`);
468
+
469
+ const prefixSet = match.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
470
+ if (prefixSet && "source" in prefixSet && match.prefix_length) {
471
+ found.push(`${where}.match.prefix_length with a prefix set a registry fills`);
472
+ }
473
+
474
+ return found;
475
+ }
476
+ }
@@ -0,0 +1,129 @@
1
+ // FortiOS configuration as a tree. A `config` block holds settings, nested blocks and, in a table, entries opened with `edit`. The render builds a tree and prints it in the form `show` prints, and the read parses what `show` printed, so the plan compares two trees.
2
+
3
+ /** One `edit` of a table: its name as written, and what it holds. */
4
+ export type Entry = { id: string; block: FortiOsBlock };
5
+
6
+ export class FortiOsBlock {
7
+ readonly settings = new Map<string, string>();
8
+ readonly blocks = new Map<string, FortiOsBlock>();
9
+ readonly entries = new Map<string, Entry>();
10
+
11
+ /** Set a key. A value of several words is written as given. */
12
+ set(key: string, value: string | number): this {
13
+ this.settings.set(key, String(value));
14
+ return this;
15
+ }
16
+
17
+ /** The nested `config` block with this header, created when absent. */
18
+ block(header: string): FortiOsBlock {
19
+ if (!this.blocks.has(header)) this.blocks.set(header, new FortiOsBlock());
20
+ return this.blocks.get(header)!;
21
+ }
22
+
23
+ /** The table entry with this name, created when absent. A name is quoted unless it is a number, or the table prints it bare, such as an OSPF area or an IPv6 prefix. */
24
+ entry(id: string | number, plain = false): FortiOsBlock {
25
+ const written = typeof id === "number" || plain ? String(id) : FortiOsTree.quote(id);
26
+ const key = FortiOsTree.bare(written);
27
+ if (!this.entries.has(key)) this.entries.set(key, { id: written, block: new FortiOsBlock() });
28
+ return this.entries.get(key)!.block;
29
+ }
30
+
31
+ isEmpty(): boolean {
32
+ return !this.settings.size && !this.blocks.size && !this.entries.size;
33
+ }
34
+ }
35
+
36
+ export class FortiOsTree {
37
+ /** A setting the device prints as `unset <key>`, a key it holds empty on purpose, such as an ICMP service's type. */
38
+ static readonly UNSET = "\u0000unset";
39
+
40
+ /** A string as the CLI takes it, in double quotes. */
41
+ static quote(text: string): string {
42
+ return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
43
+ }
44
+
45
+ /** Each of several strings quoted, as a list setting takes them. */
46
+ static quoteAll(texts: readonly string[]): string {
47
+ return texts.map(FortiOsTree.quote).join(" ");
48
+ }
49
+
50
+ /** A value's words; a quoted string is one word with its quotes. */
51
+ static words(value: string): string[] {
52
+ return value.match(/"(?:[^"\\]|\\.)*"|'[^']*'|\S+/g) ?? [];
53
+ }
54
+
55
+ /** A word as the device means it, without quotes or escapes. */
56
+ static bare(word: string): string {
57
+ if (word.startsWith('"')) return word.slice(1, -1).replace(/\\(.)/g, "$1");
58
+ if (word.startsWith("'")) return word.slice(1, -1);
59
+ return word;
60
+ }
61
+
62
+ /** The lines that build a block, at an indent, in the form `show` prints and the CLI takes. */
63
+ static lines(block: FortiOsBlock, indent = ""): string[] {
64
+ const lines: string[] = [];
65
+
66
+ for (const [key, value] of block.settings)
67
+ lines.push(value === FortiOsTree.UNSET ? `${indent}unset ${key}` : `${indent}set ${key} ${value}`);
68
+
69
+ for (const [header, child] of block.blocks)
70
+ lines.push(`${indent}config ${header}`, ...FortiOsTree.lines(child, indent + " "), `${indent}end`);
71
+
72
+ for (const { id, block: entry } of block.entries.values()) {
73
+ lines.push(`${indent}edit ${id}`, ...FortiOsTree.lines(entry, indent + " "), `${indent}next`);
74
+ }
75
+
76
+ return lines;
77
+ }
78
+
79
+ /** The whole configuration: each top-level block as `config <section>` to `end`. */
80
+ static print(root: FortiOsBlock): string {
81
+ const lines: string[] = [];
82
+ for (const [section, block] of root.blocks) lines.push(`config ${section}`, ...FortiOsTree.lines(block, " "), "end");
83
+ return lines.join("\n") + "\n";
84
+ }
85
+
86
+ /** Whether a line opens a quoted value it does not close, so the value goes on over the next lines. */
87
+ private static unclosed(line: string): boolean {
88
+ return (line.replace(/\\./g, "").match(/"/g)?.length ?? 0) % 2 === 1;
89
+ }
90
+
91
+ /** The tree of what `show` printed. Lines that are not configuration, such as the version header, are skipped. */
92
+ static parse(text: string): FortiOsBlock {
93
+ const root = new FortiOsBlock();
94
+ const stack: { block: FortiOsBlock; isEntry: boolean }[] = [{ block: root, isEntry: false }];
95
+ const lines = text.replace(/\r/g, "").split("\n");
96
+
97
+ for (let index = 0; index < lines.length; index++) {
98
+ let line = lines[index]!.trim();
99
+ if (!line || line.startsWith("#")) continue;
100
+
101
+ while (FortiOsTree.unclosed(line) && index + 1 < lines.length) line += "\n" + lines[++index];
102
+
103
+ const current = stack.at(-1)!.block;
104
+ const [verb] = line.split(/\s+/, 1);
105
+ const rest = line.slice(verb!.length).trim();
106
+
107
+ if (verb === "config") {
108
+ const child = current.block(rest);
109
+ stack.push({ block: child, isEntry: false });
110
+ } else if (verb === "edit") {
111
+ const key = FortiOsTree.bare(rest);
112
+ if (!current.entries.has(key)) current.entries.set(key, { id: rest, block: new FortiOsBlock() });
113
+ stack.push({ block: current.entries.get(key)!.block, isEntry: true });
114
+ } else if (verb === "next") {
115
+ if (stack.at(-1)!.isEntry) stack.pop();
116
+ } else if (verb === "end") {
117
+ if (stack.at(-1)!.isEntry) stack.pop();
118
+ if (stack.length > 1) stack.pop();
119
+ } else if (verb === "set") {
120
+ const [key] = rest.split(/\s+/, 1);
121
+ current.settings.set(key!, rest.slice(key!.length).trim());
122
+ } else if (verb === "unset") {
123
+ current.settings.set(rest, FortiOsTree.UNSET);
124
+ }
125
+ }
126
+
127
+ return root;
128
+ }
129
+ }
@@ -0,0 +1,36 @@
1
+ import { FortiOs } from "./fortios";
2
+ import { Junos } from "./junos";
3
+ import type { Platform } from "./platform";
4
+ import { RaisecomRos } from "./raisecom-ros";
5
+ import { RouterOs } from "./routeros";
6
+ import { Vrp } from "./vrp";
7
+
8
+ /** Every platform the framework can render and apply, by name. */
9
+ export class Platforms {
10
+ private static readonly BY_NAME: ReadonlyMap<string, Platform> = new Map(
11
+ [new RouterOs(), new Vrp(), new RaisecomRos(), new Junos(), new FortiOs()].map((platform) => [platform.name, platform])
12
+ );
13
+
14
+ /** The platform of that name, or undefined when there is none. */
15
+ static find(name: string): Platform | undefined {
16
+ return Platforms.BY_NAME.get(name);
17
+ }
18
+
19
+ /** The platform of that name. Throws when there is none. */
20
+ static get(name: string): Platform {
21
+ const platform = Platforms.find(name);
22
+ if (!platform) throw new Error(`no adapter for platform ${name}`);
23
+
24
+ return platform;
25
+ }
26
+
27
+ /** Every platform, in the order the framework lists them. */
28
+ static all(): Platform[] {
29
+ return [...Platforms.BY_NAME.values()];
30
+ }
31
+
32
+ /** Every platform's name. */
33
+ static names(): string[] {
34
+ return [...Platforms.BY_NAME.keys()];
35
+ }
36
+ }
@@ -32,3 +32,11 @@ Behaviour that looks like something else. Each one has been hit on a QFX5100-48S
32
32
  - The serial console has no flow control. Statements pasted into `load set terminal` at once overrun it: some arrive garbled, the load reports syntax errors, and the `^D` that ends it can be lost, leaving the console in the load. Circuit sends each statement after the echo of the one before.
33
33
  - After a login the console prints `Last login: ...`, which ends in `login: ` like the login prompt; only a `login: ` on a line of its own is the prompt.
34
34
  - A factory-default switch runs Auto Image Upgrade, which prints on the console every few seconds until `delete chassis auto-image-upgrade` is committed.
35
+ - `show lldp neighbors` has no column for the far port: its `Port info` is the far port's description. `show lldp neighbors detail | display xml` has the port in `lldp-remote-port-id`, and Circuit reads that. In the detail form `lldp-local-port-id` is a number, `520`; the port's name is in `lldp-local-interface`.
36
+ - BFD on a BGP group, an OSPF interface and a static route takes `bfd-liveness-detection minimum-interval N` and `multiplier N`, and prints both as sent, `multiplier 3`, the default, too. A multihop group takes it without a `local-address`.
37
+ - BFD on a BGP session starts only once the session is up: `show bfd session` lists nothing for a session in `Connect`, though the session shows `<BfdEnabled>`.
38
+ - A static route with BFD is withdrawn while its next hop does not answer BFD, so `show route` shows nothing for it. A session that is down sends every 2 seconds, `Transmit Interval 2.000`, whatever the interval.
39
+ - `bfd-liveness-detection` on a static route also watches a `qualified-next-hop`: a session to that address starts.
40
+ - `set switch-options interface xe-0/0/1 interface-mac-limit 1 packet-action drop` is stored on the logical interface and in two statements: `... interface xe-0/0/1.0 interface-mac-limit 1` and `... interface-mac-limit packet-action drop`. The render writes those two.
41
+ - An `ethernet-switching` filter takes `from ether-type ipv4`, `ipv6` and `arp` by name. Ethertypes and an `acl` would both be the port's `filter input`, so Circuit refuses the two on one port rather than merge them.
42
+ - LLDP sends a port's SNMP index as its port ID, `PortIdSubtype: local`, `PortId: 520`, so a neighbor cannot tell which port it is. `set protocols lldp port-id-subtype interface-name` makes it send `et-0/0/52`, and the render sets it wherever LLDP runs.