@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
@@ -0,0 +1,479 @@
1
+ // A network's BGP large communities, `asn:code:value` (RFC 8092), used as RFC 8195 describes. The scheme is written once, in
2
+ // `circuit({ communities })`; a neighbor group says which groups it is in, and a policy places `{ communities: "import" }` or
3
+ // `{ communities: "export" }`. Circuit writes the catalogue from the scheme, and each such rule as the rules it means for the neighbors
4
+ // that use the policy.
5
+
6
+ import type { Community } from "./network";
7
+ import type { LargeCommunity, StandardCommunity } from "./routing";
8
+
9
+ /** One option of the scheme. */
10
+ export type CommunityOption = {
11
+ /** The middle number of `asn:code:value` (RFC 8195 calls it the function). */
12
+ code: number;
13
+ /** The catalogue's words for it, in place of Circuit's. Each option says which line they are. */
14
+ description?: string;
15
+ };
16
+
17
+ /** A group of neighbors, such as `transit`. */
18
+ export type CommunityGroup = {
19
+ /** The last number of a community about the group, such as the `2` of `64500:1:2`. Neighbors write it into their routers: keep it. */
20
+ value: number;
21
+ /** How the catalogue names the group, such as `an IP transit`. Its name with `a` or `an` when absent: `a transit`. */
22
+ description?: string;
23
+ };
24
+
25
+ /**
26
+ * The network's large communities, `asn:code:value`. Write the options the network offers, and leave out the rest. Each option is an
27
+ * object with its `code`, and may have its own `description` for the catalogue.
28
+ */
29
+ export type CommunityScheme = {
30
+ /** The first number of every community. The network's `asn` when absent. */
31
+ asn?: number;
32
+ /** Groups of neighbors, by name, such as `{ transit: { value: 1 }, exchange: { value: 2 } }`. A neighbor group says which it is in. Needed by the `_group` options. */
33
+ groups?: Readonly<Record<string, CommunityGroup>>;
34
+ /** A tag: the groups of the neighbor a route was learned from, `asn:code:<group value>`. `description` is the words before the group: `Learned from` when absent. */
35
+ learned_from_group?: CommunityOption;
36
+ /** A tag: the site a route was learned at, `asn:code:<site number>`, from the site's `number` in `sites`. `description` is the `nnn` line: `Learned at site $0` when absent. */
37
+ learned_at_site?: CommunityOption;
38
+ /** A tag: the AS a route was learned from, `asn:code:<asn>`. Never on a customer's routes, so customers share one import policy. `description` is the `nnn` line: `Learned from AS$0` when absent. */
39
+ learned_from_as?: CommunityOption;
40
+ /** A request: do not announce to an AS, `asn:code:<asn>`, or to anyone, `asn:code:0`. `description` is the `nnn` line: `Do not announce to AS$0` when absent. */
41
+ do_not_announce_to_as?: CommunityOption;
42
+ /** A request: do not announce to the neighbors of a group, `asn:code:<group value>`. `description` is the words before the group: `Do not announce to` when absent. */
43
+ do_not_announce_to_group?: CommunityOption;
44
+ /** A request: prepend our AS toward an AS once, twice or three times, `asn:code:<asn>`. Each `description` is its `nnn` line: `Prepend once to AS$0` when absent. */
45
+ prepend?: { once?: CommunityOption; twice?: CommunityOption; three_times?: CommunityOption };
46
+ /** A request: drop all traffic to the route, `asn:code:0`. A neighbor group with its own `blackhole` community is passed the route with it, so it drops the traffic too; any other never gets the route. `description` is its line: `Blackhole: dropped in AS<asn>` when absent. */
47
+ blackhole?: CommunityOption;
48
+ /** Your own words for one community of the catalogue, by the community as the catalogue writes it: `{ "64500:2:0": "Learned at site-a" }`. */
49
+ custom_descriptions?: Readonly<Record<string, string>>;
50
+ };
51
+
52
+ /** The options a neighbor sends to ask something of us, which an import may accept. */
53
+ export type CommunityRequest = "do_not_announce_to_as" | "do_not_announce_to_group" | "prepend" | "blackhole";
54
+
55
+ /** The requests a scheme `C` offers. */
56
+ export type RequestsOf<C> = C extends object ? Extract<keyof C, CommunityRequest> : never;
57
+
58
+ /** The group names a scheme `C` writes. */
59
+ export type GroupsOf<C> = C extends { readonly groups: infer G } ? keyof G & string : never;
60
+
61
+ /**
62
+ * The scheme as written, `C`, and an error at each option that needs what is not written: a `_group` option without `groups`, and
63
+ * `learned_at_site` while no site has a `number` (`Numbered`). The editor still offers them. A key that is no option is an error too.
64
+ */
65
+ export type SchemeCheck<C, Numbered extends boolean> = {
66
+ [K in Exclude<keyof C, keyof CommunityScheme> & string]: `${K} is no option of communities`;
67
+ } & (C extends {
68
+ readonly groups: object;
69
+ }
70
+ ? unknown
71
+ : {
72
+ learned_from_group?: "learned_from_group needs communities.groups";
73
+ do_not_announce_to_group?: "do_not_announce_to_group needs communities.groups";
74
+ }) &
75
+ (Numbered extends true ? unknown : { learned_at_site?: "learned_at_site needs a number on a site, in sites" });
76
+
77
+ /** What a neighbor, or its group, says about communities. `G` is the scheme's groups. */
78
+ export type NeighborCommunities<G extends string> = {
79
+ /** The groups it is in, from the network's `communities.groups`. Its routes are tagged with each, and a request about a group acts on it. */
80
+ groups?: readonly ([G] extends [never] ? "needs communities.groups in circuit.ts" : G)[];
81
+ /** Its own blackhole community, when it takes blackholes from us, such as `64501:666`. A route asked to be blackholed is passed to it with this. */
82
+ blackhole?: StandardCommunity | LargeCommunity;
83
+ };
84
+
85
+ /**
86
+ * A route policy rule that Circuit writes out, in an import: it takes off the requests a neighbor may not send, and the tags only we
87
+ * set, then tags the route with where it was learned. `R` is the requests the scheme offers.
88
+ */
89
+ export type CommunitiesImport<R extends string> = {
90
+ /** The rules for an import. */
91
+ communities: "import";
92
+ /** The requests this neighbor may send, which stay on the route. None when absent: every request is taken off (RFC 7454). */
93
+ accept_requests?: readonly R[];
94
+ /** Tag the route with where it was learned: its groups, the site, its AS (not a customer's). `true` when absent. `false` leaves the tags as they came, for iBGP, where our own router set them. */
95
+ add_tags?: boolean;
96
+ /** A communities rule holds nothing else. */
97
+ call?: never;
98
+ };
99
+
100
+ /**
101
+ * A route policy rule that Circuit writes out, in an export, before the rules that accept: it does what routes ask (do not announce,
102
+ * prepend, blackhole) toward the neighbor, then takes our communities off.
103
+ */
104
+ export type CommunitiesExport = {
105
+ /** The rules for an export. */
106
+ communities: "export";
107
+ /** Keep our communities on the route. `false` when absent. Toward iBGP, write it: `true` sends every tag and request on, as it is; without it, `circuit validate` stops. */
108
+ keep_ours?: boolean;
109
+ /** Keep our tags on the route, for a neighbor that wants to see where a route was learned; the requests are still taken off. `false` when absent. */
110
+ send_tags?: boolean;
111
+ /** A communities rule holds nothing else. */
112
+ call?: never;
113
+ };
114
+
115
+ /** A route policy rule Circuit writes out from the scheme, when the network writes `communities`. */
116
+ export type CommunitiesRule<R extends string> = CommunitiesImport<R> | CommunitiesExport;
117
+
118
+ // ---------- what Circuit does with them ----------
119
+
120
+ // The network and a device's JSON, walked without their types.
121
+ type Json = any;
122
+
123
+ const PREPENDS = [
124
+ ["once", 1],
125
+ ["twice", 2],
126
+ ["three_times", 3],
127
+ ] as const;
128
+ const REQUESTS: CommunityRequest[] = ["do_not_announce_to_as", "do_not_announce_to_group", "prepend", "blackhole"];
129
+ const TAGS = ["learned_from_group", "learned_at_site", "learned_from_as"];
130
+
131
+ const article = (word: string) => `${/^[aeiou]/i.test(word) ? "an" : "a"} ${word}`;
132
+
133
+ /** The codes of the options named, a prepend's three included. */
134
+ const codesOf = (s: Json, names: string[]): number[] =>
135
+ names.flatMap((n) =>
136
+ n === "prepend" ? PREPENDS.flatMap(([p]) => (s.prepend?.[p] ? [s.prepend[p].code] : [])) : s[n] ? [s[n].code] : []
137
+ );
138
+
139
+ /** Every community of the scheme as the catalogue lists it, `nnn` for any number, with the scheme's own words where it has them. */
140
+ export function catalogue(network: Json): Community[] {
141
+ const s = network.communities;
142
+ if (!s) return [];
143
+ const asn = s.asn ?? network.asn;
144
+ const line = (option: Json, value: number | string, text: string) => ({
145
+ community: `${asn}:${option.code}:${value}`,
146
+ description: text,
147
+ });
148
+ const groups = Object.entries<Json>(s.groups ?? {});
149
+ const named = (name: string, group: Json) => group.description ?? article(name);
150
+ const sites = Object.entries<Json>(network.sites ?? {}).filter(([, site]) => site.number !== undefined);
151
+ const o = s;
152
+ const lines = [
153
+ ...(o.learned_from_group
154
+ ? groups.map(([n, g]) =>
155
+ line(o.learned_from_group, g.value, `${o.learned_from_group.description ?? "Learned from"} ${named(n, g)}`)
156
+ )
157
+ : []),
158
+ ...(o.learned_at_site
159
+ ? [
160
+ ...sites.map(([n, site]) => line(o.learned_at_site, site.number, `Learned at ${n}`)),
161
+ line(o.learned_at_site, "nnn", o.learned_at_site.description ?? "Learned at site $0"),
162
+ ]
163
+ : []),
164
+ ...(o.learned_from_as ? [line(o.learned_from_as, "nnn", o.learned_from_as.description ?? "Learned from AS$0")] : []),
165
+ ...(o.do_not_announce_to_as
166
+ ? [
167
+ line(o.do_not_announce_to_as, 0, "Do not announce to anyone"),
168
+ line(o.do_not_announce_to_as, "nnn", o.do_not_announce_to_as.description ?? "Do not announce to AS$0"),
169
+ ]
170
+ : []),
171
+ ...(o.do_not_announce_to_group
172
+ ? groups.map(([n, g]) =>
173
+ line(
174
+ o.do_not_announce_to_group,
175
+ g.value,
176
+ `${o.do_not_announce_to_group.description ?? "Do not announce to"} ${named(n, g)}`
177
+ )
178
+ )
179
+ : []),
180
+ ...PREPENDS.flatMap(([p]) =>
181
+ o.prepend?.[p] ? [line(o.prepend[p], "nnn", o.prepend[p].description ?? `Prepend ${p.replace("_", " ")} to AS$0`)] : []
182
+ ),
183
+ ...(o.blackhole ? [line(o.blackhole, 0, o.blackhole.description ?? `Blackhole: dropped in AS${asn}`)] : []),
184
+ ];
185
+ return lines.map((l) => ({ ...l, description: s.custom_descriptions?.[l.community] ?? l.description }));
186
+ }
187
+
188
+ /** What is wrong with the scheme, which the editor may have missed. */
189
+ export function schemeProblems(network: Json): string[] {
190
+ const s = network.communities;
191
+ if (!s) return [];
192
+ const found: string[] = [];
193
+ for (const option of ["learned_from_group", "do_not_announce_to_group"])
194
+ if (s[option] && !s.groups) found.push(`communities.${option} needs communities.groups.`);
195
+ if (s.learned_at_site && !Object.values<Json>(network.sites ?? {}).some((site) => site.number !== undefined))
196
+ found.push("communities.learned_at_site needs a number on a site, in sites.");
197
+ const listed = new Set(catalogue(network).map((l) => l.community));
198
+ for (const community of Object.keys(s.custom_descriptions ?? {}))
199
+ if (!listed.has(community))
200
+ found.push(
201
+ `communities.custom_descriptions: ${community} is no community of the scheme. The catalogue has ${[...listed].join(", ")}.`
202
+ );
203
+ return found;
204
+ }
205
+
206
+ /** One neighbor as a communities rule sees it: its AS, its groups, its blackhole community, and whether it is a customer. */
207
+ type Peer = { asn: number; groups: string[]; blackhole?: string; customer: boolean; ibgp: boolean };
208
+
209
+ /**
210
+ * The rules one communities rule is written as, toward one neighbor: `all` in place, for a policy one AS uses; for a policy several
211
+ * ASes use, `own`, the rules of this AS, which run first in a policy of their own, and `shared`, the same for every AS.
212
+ */
213
+ type Written = { all: Json[]; own: Json[]; shared: Json[] };
214
+
215
+ /**
216
+ * An import: takes off the requests not accepted and the tags, adds the tags, and blackholes a route that asks, when accepted. A
217
+ * customer's routes get no `learned_from_as` tag (decision 50), so the customers share one import policy.
218
+ */
219
+ function importRules(s: Json, asn: number, rule: Json, to: Peer, site: number | undefined, groupsShared: boolean): Written {
220
+ const accepted: string[] = rule.accept_requests ?? [];
221
+ const tags = rule.add_tags !== false;
222
+ const off = codesOf(s, [...(tags ? TAGS : []), ...REQUESTS.filter((r) => !accepted.includes(r))]);
223
+ const everything = tags && !REQUESTS.some((r) => accepted.includes(r) && (r === "prepend" ? s.prepend : s[r]));
224
+ const asTag = !!s.learned_from_as && !to.customer;
225
+ const add = tags
226
+ ? [
227
+ ...(s.learned_from_group ? to.groups.map((g) => `${asn}:${s.learned_from_group.code}:${s.groups[g].value}`) : []),
228
+ ...(s.learned_at_site ? [`${asn}:${s.learned_at_site.code}:${site}`] : []),
229
+ ...(asTag ? [`${asn}:${s.learned_from_as.code}:${to.asn}`] : []),
230
+ ]
231
+ : [];
232
+ const set = {
233
+ remove_standard_communities: [`${asn}:*`],
234
+ ...(everything
235
+ ? { remove_large_communities: [`${asn}:*:*`] }
236
+ : off.length
237
+ ? { remove_large_communities: off.map((c) => `${asn}:${c}:*`) }
238
+ : {}),
239
+ ...(add.length ? { add_large_communities: add } : {}),
240
+ };
241
+ const blackhole =
242
+ accepted.includes("blackhole") && s.blackhole
243
+ ? [
244
+ {
245
+ description: "Blackhole, asked by the neighbor",
246
+ if: { large_community: `${asn}:${s.blackhole.code}:0` },
247
+ set: { blackhole: true },
248
+ },
249
+ ]
250
+ : [];
251
+ // The tag is the AS's own when it names the AS, or groups that differ between the ASes.
252
+ const ownTag = tags && (asTag || (!!s.learned_from_group && to.groups.length > 0 && !groupsShared));
253
+ const tag = {
254
+ description: !tags ? "Requests it may not send taken off" : ownTag ? `Learned from AS${to.asn}` : "Where it was learned",
255
+ set,
256
+ };
257
+ return {
258
+ all: [{ ...tag, description: tags && !to.customer ? `Learned from AS${to.asn}` : tag.description }, ...blackhole],
259
+ own: ownTag ? [tag] : [],
260
+ shared: [...(ownTag ? [] : [tag]), ...blackhole],
261
+ };
262
+ }
263
+
264
+ /**
265
+ * An export, before the rules that accept: a blackholed route rejected, or passed on to a neighbor that takes it; a route asked not to
266
+ * go anywhere, to this AS or to one of its groups, rejected; the prepends it asks for; our communities taken off. Toward a customer, no
267
+ * request about one AS (decision 39). Rejects come first, so "do not announce" always wins over a prepend. Toward iBGP, where our
268
+ * other routers act on the requests themselves, only our communities taken off, unless `keep_ours`.
269
+ */
270
+ function exportRules(s: Json, asn: number, rule: Json, to: Peer, groupsShared: boolean): Written {
271
+ const requests = codesOf(s, REQUESTS);
272
+ const ours = rule.keep_ours
273
+ ? {}
274
+ : {
275
+ remove_standard_communities: [`${asn}:*`],
276
+ remove_large_communities: rule.send_tags ? requests.map((c) => `${asn}:${c}:*`) : [`${asn}:*:*`],
277
+ };
278
+ const asks = (option: Json, value: number) => ({ large_community: `${asn}:${option.code}:${value}` });
279
+ const dna = s.do_not_announce_to_as;
280
+ const blackholed = s.blackhole ? [{ description: "Blackholed here, never announced", if: asks(s.blackhole, 0), then: "reject" }] : [];
281
+ const anywhere = dna ? [{ description: "Asked not to be announced anywhere", if: asks(dna, 0), then: "reject" }] : [];
282
+ const toAs =
283
+ dna && !to.customer ? [{ description: `Asked not to be announced to AS${to.asn}`, if: asks(dna, to.asn), then: "reject" }] : [];
284
+ const toGroups = s.do_not_announce_to_group
285
+ ? to.groups.map((g) => ({
286
+ description: `Asked not to be announced to ${g}`,
287
+ if: asks(s.do_not_announce_to_group, s.groups[g].value),
288
+ then: "reject",
289
+ }))
290
+ : [];
291
+ const add =
292
+ to.blackhole?.split(":").length === 3 ? { add_large_communities: [to.blackhole] } : { add_standard_communities: [to.blackhole] };
293
+ const passOn =
294
+ s.blackhole && to.blackhole
295
+ ? [{ description: `Blackhole passed on to AS${to.asn}`, if: asks(s.blackhole, 0), set: { ...ours, ...add }, then: "accept" }]
296
+ : [];
297
+ const prepends = to.customer
298
+ ? []
299
+ : PREPENDS.flatMap(([p, times]) =>
300
+ s.prepend?.[p]
301
+ ? [
302
+ {
303
+ description: `Asked to prepend ${p.replace("_", " ")} to AS${to.asn}`,
304
+ if: asks(s.prepend[p], to.asn),
305
+ set: { prepend: times },
306
+ },
307
+ ]
308
+ : []
309
+ );
310
+ const strip = rule.keep_ours
311
+ ? []
312
+ : [{ description: rule.send_tags ? "Our requests stay inside our network" : "Our communities stay inside our network", set: ours }];
313
+ if (to.ibgp) return { all: strip, own: [], shared: strip };
314
+ return {
315
+ all: [...(passOn.length ? [] : blackholed), ...anywhere, ...toAs, ...toGroups, ...passOn, ...prepends, ...strip],
316
+ // A route passed on as a blackhole is accepted in the AS's own policy, so what must reject it first comes with it there.
317
+ own: passOn.length
318
+ ? [...anywhere, ...toAs, ...toGroups, ...passOn, ...prepends]
319
+ : [...toAs, ...(groupsShared ? [] : toGroups), ...prepends],
320
+ shared: [...blackholed, ...anywhere, ...(groupsShared ? toGroups : []), ...strip],
321
+ };
322
+ }
323
+
324
+ /** Each BGP neighbor of a device, with what it takes from its group. */
325
+ function neighborsOf(d: Json) {
326
+ const bgp = d.routing?.bgp ?? {};
327
+ return Object.entries<Json>(bgp.neighbors ?? {}).map(([name, x]) => {
328
+ const group = x.neighbor_group ? bgp.neighbor_groups?.[x.neighbor_group] : undefined;
329
+ const c = x.communities ?? group?.communities ?? {};
330
+ return {
331
+ name,
332
+ x,
333
+ group,
334
+ asn: x.remote_as ?? group?.remote_as,
335
+ groups: (c.groups ?? []) as string[],
336
+ blackhole: c.blackhole as string | undefined,
337
+ customer: (x.role ?? group?.role) === "customer",
338
+ import: x.import_policy ?? group?.import_policy,
339
+ export: x.export_policy ?? group?.export_policy,
340
+ };
341
+ });
342
+ }
343
+
344
+ const isCommunities = (rule: Json) => !!rule && typeof rule === "object" && "communities" in rule;
345
+
346
+ /**
347
+ * Writes out each `{ communities }` rule of a device, for the neighbors that use its policy: their AS, their groups, their blackhole
348
+ * community, and the device's site. A policy one AS uses gets the rules in place. A policy several ASes use keeps what they share, and
349
+ * each AS with rules of its own gets a policy `<policy>-as<asn>` with them, which then calls it; its neighbors use that one. Then
350
+ * takes the `communities` block off each neighbor and group: only Circuit reads it. Returns what is wrong, each starting with `at`.
351
+ */
352
+ export function writeCommunities(network: Json, d: Json, at: (map: object | undefined, kind: string, key: string) => string): string[] {
353
+ const found: string[] = [];
354
+ const policies = d.routing?.policies ?? {};
355
+ const bgp = d.routing?.bgp ?? {};
356
+ const neighbors = neighborsOf(d);
357
+ const s = network.communities;
358
+ const blocks = [
359
+ ...Object.entries<Json>(bgp.neighbor_groups ?? {}).map(([n, g]) => ["neighbor_groups", n, g]),
360
+ ...Object.entries<Json>(bgp.neighbors ?? {}).map(([n, x]) => ["neighbors", n, x]),
361
+ ];
362
+ for (const [kind, n, x] of blocks) {
363
+ if (!x.communities) continue;
364
+ const where = `${kind}.${n}`;
365
+ const here = at(bgp[kind], kind, n);
366
+ if (!s) found.push(`${here}routing.bgp.${where}.communities: needs communities in circuit.ts.`);
367
+ else
368
+ for (const g of x.communities.groups ?? [])
369
+ if (!s.groups?.[g])
370
+ found.push(
371
+ `${here}routing.bgp.${where}.communities: no group ${g} in communities.groups. The groups: ${Object.keys(s.groups ?? {}).join(", ") || "none"}.`
372
+ );
373
+ }
374
+ // A policy is written out only from blocks that are right.
375
+ if (found.length) {
376
+ for (const [, , x] of blocks) delete x.communities;
377
+ return found;
378
+ }
379
+
380
+ for (const [p, rules] of Object.entries<Json[]>(policies)) {
381
+ const written = rules.filter(isCommunities);
382
+ if (!written.length) continue;
383
+ const here = `${at(policies, "policies", p)}routing.policies.${p}: `;
384
+ if (!s) {
385
+ found.push(`${here}a communities rule needs communities in circuit.ts.`);
386
+ continue;
387
+ }
388
+ const start = found.length;
389
+ const kinds = [...new Set(written.map((r) => r.communities))];
390
+ if (kinds.length > 1) found.push(`${here}holds an import and an export communities rule. A policy is one or the other.`);
391
+ const kind: "import" | "export" = kinds[0];
392
+ const other = kind === "import" ? "export" : "import";
393
+ const wrong = neighbors.filter((n) => n[other] === p);
394
+ if (wrong.length)
395
+ found.push(
396
+ `${here}has a communities ${kind} rule, but ${wrong.map((n) => n.name).join(", ")} use it as their ${other} policy.`
397
+ );
398
+ const users = neighbors.filter((n) => n[kind] === p);
399
+ if (!users.length)
400
+ found.push(
401
+ `${here}no neighbor uses it as its ${kind} policy. Circuit writes a communities rule for the neighbors that use its policy, so it goes in a policy a neighbor names, not one another policy calls.`
402
+ );
403
+ for (const rule of written) {
404
+ for (const r of rule.accept_requests ?? [])
405
+ if (!REQUESTS.includes(r) || !(r === "prepend" ? s.prepend : s[r]))
406
+ found.push(
407
+ `${here}accept_requests: ${r} is no request of communities. It has: ${REQUESTS.filter((q) => (q === "prepend" ? s.prepend : s[q])).join(", ") || "none"}.`
408
+ );
409
+ if (kind === "import" && rule.add_tags !== false && s.learned_at_site && network.sites?.[d.site]?.number === undefined)
410
+ found.push(
411
+ `${here}learned_at_site tags each route with the site's number, but ${d.site} has none. Write number on it, in sites.`
412
+ );
413
+ }
414
+ const ours = bgp.asn ?? network.asn;
415
+ if (kind === "export")
416
+ for (const n of users)
417
+ if (n.asn === ours && written.some((r) => r.keep_ours === undefined))
418
+ found.push(
419
+ `${here}${n.name} is iBGP (AS${ours}), and its communities export doesn't write keep_ours. Write keep_ours: true to send our tags and requests on inside the network, or false to take them off.`
420
+ );
421
+ for (const n of users) if (n.asn === undefined) found.push(`${here}${n.name} has no remote_as, which a communities rule needs.`);
422
+
423
+ const byAs = new Map<number, typeof users>();
424
+ for (const n of users) byAs.set(n.asn, [...(byAs.get(n.asn) ?? []), n]);
425
+ const peer = (asn: number): Peer => {
426
+ const [n] = byAs.get(asn)!;
427
+ return { asn, groups: n!.groups, blackhole: n!.blackhole, customer: n!.customer, ibgp: asn === ours };
428
+ };
429
+ for (const [asn, ns] of byAs)
430
+ if (new Set(ns.map((n) => JSON.stringify([n.groups, n.blackhole, n.customer]))).size > 1)
431
+ found.push(
432
+ `${here}the neighbors of AS${asn} that use it (${ns.map((n) => n.name).join(", ")}) differ in their communities or role. Give them one neighbor group.`
433
+ );
434
+ const ases = [...byAs.keys()];
435
+ if (ases.length > 1 && (written.length > 1 || !isCommunities(rules[0])))
436
+ found.push(
437
+ `${here}several ASes use it (${ases.map((a) => `AS${a}`).join(", ")}), so each one's own rules run first, in a policy of its own that then calls this one. Write its communities rule first, and once.`
438
+ );
439
+ if (found.length > start) continue;
440
+
441
+ const site = network.sites?.[d.site]?.number;
442
+ const asn = s.asn ?? network.asn;
443
+ const groupsShared = new Set(users.map((n) => JSON.stringify(n.groups))).size === 1;
444
+ const expand = (rule: Json, to: Peer) =>
445
+ kind === "import" ? importRules(s, asn, rule, to, site, groupsShared) : exportRules(s, asn, rule, to, groupsShared);
446
+
447
+ // A policy one AS uses: the rules in place.
448
+ if (ases.length === 1) {
449
+ policies[p] = rules.flatMap((r) => (isCommunities(r) ? expand(r, peer(ases[0]!)).all : [r]));
450
+ continue;
451
+ }
452
+
453
+ // Several ASes: what they share once, in the policy, and each AS's own rules in a policy that runs first, then calls it.
454
+ const parts = ases.map((a) => ({ asn: a, ...expand(rules[0], peer(a)) }));
455
+ if (new Set(parts.map((x) => JSON.stringify(x.shared))).size > 1) {
456
+ found.push(`${here}its ASes share no common rules. Give each AS a policy of its own.`);
457
+ continue;
458
+ }
459
+ policies[p] = [...parts[0]!.shared, ...rules.slice(1)];
460
+ const field = kind === "import" ? "import_policy" : "export_policy";
461
+ // The ASes of the neighbors that take the policy from each group, before any of them changes.
462
+ const fromGroup = new Map<object, Set<number>>();
463
+ for (const u of users) if (u.x[field] === undefined) fromGroup.set(u.group, (fromGroup.get(u.group) ?? new Set()).add(u.asn));
464
+ for (const { asn: a, own } of parts) {
465
+ if (!own.length) continue;
466
+ const name = `${p}-as${a}`;
467
+ if (policies[name]) found.push(`${here}AS${a}'s own policy would be ${name}, which is taken.`);
468
+ policies[name] = [...own, { call: p }];
469
+ // Its neighbors use it: on their group, when every neighbor that takes the policy from the group is of this AS.
470
+ for (const n of byAs.get(a)!) {
471
+ if (n.x[field] !== undefined || fromGroup.get(n.group)!.size > 1) n.x[field] = name;
472
+ else n.group[field] = name;
473
+ }
474
+ }
475
+ }
476
+
477
+ for (const [, , x] of blocks) delete x.communities;
478
+ return found;
479
+ }