@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,301 @@
1
+ import type { Community } from "../core/define";
2
+ import type { Device, LargeCommunityPattern, PolicyRule, Prefix, StandardCommunity, StandardCommunityPattern } from "../schema";
3
+
4
+ /** The prefix lengths a BGP import accepts. */
5
+ export type SanityLengths = {
6
+ /** Shortest and longest IPv4 prefix accepted. /8 to /24 when absent. */
7
+ ipv4?: { min: number; max: number };
8
+ /** Shortest and longest IPv6 prefix accepted. /16 to /48 when absent. */
9
+ ipv6?: { min: number; max: number };
10
+ };
11
+
12
+ /** Kinds of neighbor and the number each is tagged with, such as `{ transit: 1, customer: 3 }`. */
13
+ export type CommunityClasses = Readonly<Record<string, number>>;
14
+
15
+ /**
16
+ * A BGP community scheme of large communities, `asn:function:parameter`, in the style of RFC 8195. Each field names the function number your network uses for it; leave out what your network does not offer.
17
+ *
18
+ * The type parameters keep the exact numbers you write, so the communities `Bgp.communityScheme` gives back show them in the editor: Asn your AS, LearnedFrom the function of `learned_from`, Classes its classes.
19
+ */
20
+ export type CommunityScheme<
21
+ Asn extends number = number,
22
+ LearnedFrom extends number = number,
23
+ Classes extends CommunityClasses = CommunityClasses,
24
+ > = {
25
+ /** Your AS. It is the first part of every community. */
26
+ asn: Asn;
27
+ /** Set on import: the kind of neighbor a route came from, `asn:function:class`. */
28
+ learned_from?: { function: LearnedFrom; classes: Classes };
29
+ /** Set on import: the site a route was learned at, `asn:function:site`. `sites` names the sites, for the catalogue. */
30
+ learned_at?: { function: number; sites?: Readonly<Record<number, string>> };
31
+ /** Set on import: the AS a route was learned from, `asn:function:asn`. */
32
+ learned_from_as?: number;
33
+ /** A neighbor asks: do not announce the route to an AS, `asn:function:asn`, or to anyone, `asn:function:0`. */
34
+ do_not_announce?: number;
35
+ /** A neighbor asks: prepend toward an AS once, twice or three times, `asn:function:asn`. */
36
+ prepend?: { once?: number; twice?: number; three_times?: number };
37
+ /**
38
+ * A neighbor asks: drop all traffic to this route, `asn:function:0`, usually one address under attack. Your router drops it, and so does every upstream in `upstreams`.
39
+ *
40
+ * `upstreams` maps an upstream's AS to its own blackhole community, as its NOC gives it. The route is passed to that upstream with it, so the traffic stops before it reaches your links. An upstream not listed never receives the route.
41
+ */
42
+ blackhole?: { function: number; upstreams?: Readonly<Record<number, StandardCommunity>> };
43
+ };
44
+
45
+ /** The class names a scheme declares, or any name when it declares none. */
46
+ type ClassName<Classes extends CommunityClasses> = {} extends Classes ? string : Extract<keyof Classes, string>;
47
+
48
+ /** BGP policy: an import sanity policy, a community scheme, and helpers that read a device's BGP neighbors. */
49
+ export class Bgp {
50
+ private static readonly PREPENDS = [
51
+ ["once", 1],
52
+ ["twice", 2],
53
+ ["three_times", 3],
54
+ ] as const;
55
+
56
+ /** Route policy rules rejecting what no neighbor should send: prefixes too short or too long for the global table, and RPKI invalid. Place it first in an import, or as a policy other imports call. */
57
+ static sanity(lengths: SanityLengths = {}): PolicyRule<never, never>[] {
58
+ const ipv4 = lengths.ipv4 ?? { min: 8, max: 24 };
59
+ const ipv6 = lengths.ipv6 ?? { min: 16, max: 48 };
60
+
61
+ return [
62
+ {
63
+ description: `IPv4 shorter than /${ipv4.min}`,
64
+ match: { family: "ipv4", prefix_length: { max: ipv4.min - 1 } },
65
+ action: "reject",
66
+ },
67
+ {
68
+ description: `IPv4 longer than /${ipv4.max}`,
69
+ match: { family: "ipv4", prefix_length: { min: ipv4.max + 1 } },
70
+ action: "reject",
71
+ },
72
+ {
73
+ description: `IPv6 shorter than /${ipv6.min}`,
74
+ match: { family: "ipv6", prefix_length: { max: ipv6.min - 1 } },
75
+ action: "reject",
76
+ },
77
+ {
78
+ description: `IPv6 longer than /${ipv6.max}`,
79
+ match: { family: "ipv6", prefix_length: { min: ipv6.max + 1 } },
80
+ action: "reject",
81
+ },
82
+ { description: "RPKI invalid", match: { rpki: "invalid" }, action: "reject" },
83
+ ];
84
+ }
85
+
86
+ /** Each BGP neighbor's own policies and role, or its group's. */
87
+ static neighborPolicies(device: Device) {
88
+ const bgp = device.routing?.bgp;
89
+
90
+ return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
91
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
92
+
93
+ return {
94
+ name,
95
+ address: neighbor.address,
96
+ role: neighbor.role ?? group?.role,
97
+ import: neighbor.import ?? group?.import,
98
+ export: neighbor.export ?? group?.export,
99
+ };
100
+ });
101
+ }
102
+
103
+ /** The prefixes an export policy accepts by name. */
104
+ static offered(device: Device, policy: string | undefined): Prefix[] {
105
+ const rules = policy ? (device.policies?.[policy] ?? []) : [];
106
+ return rules.filter((rule) => rule.action === "accept" && rule.match?.prefix).map((rule) => rule.match!.prefix!);
107
+ }
108
+
109
+ /**
110
+ * Policy rules and a catalogue from one community scheme, so the communities your routers set, the ones they act on, and the ones you publish never disagree.
111
+ *
112
+ * - `tag(neighborClass, neighborAs, { site, trusted })`: a rule for an import policy. It removes your communities a neighbor set, then adds where the route was learned. A `trusted` neighbor keeps its requests: do not announce, prepend, blackhole. `site` is needed when the scheme has `learned_at`.
113
+ * - `actions(neighborAs, { blackhole })`: rules for an export policy toward one neighbor. They act on a customer's action communities, then remove all of yours. Place them before the rules that accept. A blackholed route goes no further, unless `blackhole` names the neighbor's own blackhole community: then it is passed on with it, so the neighbor drops the traffic too.
114
+ * - `blackhole(prefixSets)`: rules for the import policy of a neighbor you trust with blackholes, before anything that rejects long prefixes. They take a route the neighbor asked to blackhole, but only inside `prefixSets`, its own space, and drop traffic to it here. One rule per set, so one call covers both families.
115
+ * - `catalogue`: every community, for `communities` in `defineNetwork`.
116
+ * - `community(fn, parameter)`: one community of yours, for a rule of your own.
117
+ * - `learned_from`: the community each class is tagged with, by class name, such as `learned_from.customer`.
118
+ */
119
+ static communityScheme<
120
+ const Asn extends number,
121
+ const LearnedFrom extends number = number,
122
+ const Classes extends CommunityClasses = {},
123
+ >(scheme: CommunityScheme<Asn, LearnedFrom, Classes>) {
124
+ type Class = ClassName<Classes>;
125
+
126
+ const { asn } = scheme;
127
+
128
+ /** One community of yours, `asn:fn:parameter`. */
129
+ const community = <const Fn extends number, const Parameter extends number>(fn: Fn, parameter: Parameter) =>
130
+ `${asn}:${fn}:${parameter}` as `${Asn}:${Fn}:${Parameter}`;
131
+
132
+ /** Every value of one of your functions, `asn:fn:*`, for a list of communities to remove. */
133
+ const anyOf = (fn: number): LargeCommunityPattern => `${asn}:${fn}:*`;
134
+
135
+ /** A line of the catalogue, where `nnn` stands for any number. */
136
+ const written = (fn: number, parameter: number | string) => `${asn}:${fn}:${parameter}`;
137
+
138
+ const ours = {
139
+ standard: [`${asn}:*`] as StandardCommunityPattern[],
140
+ large: [`${asn}:*:*`] as LargeCommunityPattern[],
141
+ };
142
+
143
+ const learnedFrom = Object.fromEntries(
144
+ Object.entries<number>(scheme.learned_from?.classes ?? {}).map(([name, value]) => [
145
+ name,
146
+ community(scheme.learned_from!.function, value),
147
+ ])
148
+ ) as { readonly [Name in keyof Classes]: `${Asn}:${LearnedFrom}:${Classes[Name]}` };
149
+
150
+ const information = [scheme.learned_from?.function, scheme.learned_at?.function, scheme.learned_from_as].filter(
151
+ (fn): fn is number => fn !== undefined
152
+ );
153
+
154
+ const tag = (
155
+ neighborClass: Class,
156
+ neighborAs: number,
157
+ options: { site?: number; trusted?: boolean } = {}
158
+ ): PolicyRule<never, never> => {
159
+ const { site, trusted = false } = options;
160
+ if (scheme.learned_at && site === undefined)
161
+ throw new Error(`Bgp.communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
162
+
163
+ return {
164
+ description: `Learned from AS${neighborAs}`,
165
+ set: {
166
+ remove_communities: ours.standard,
167
+ // A trusted neighbor keeps its requests and loses only the "where it came from" tags, which are ours to set. Anyone else loses every community of ours.
168
+ remove_large_communities: trusted ? information.map(anyOf) : ours.large,
169
+ add_large_communities: [
170
+ ...(scheme.learned_from
171
+ ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])]
172
+ : []),
173
+ ...(scheme.learned_at ? [community(scheme.learned_at.function, site!)] : []),
174
+ ...(scheme.learned_from_as !== undefined ? [community(scheme.learned_from_as, neighborAs)] : []),
175
+ ],
176
+ },
177
+ };
178
+ };
179
+
180
+ const blackhole = <S extends string>(prefixSets: readonly S[]): PolicyRule<never, S>[] => {
181
+ const fn = scheme.blackhole?.function;
182
+ if (fn === undefined) throw new Error("Bgp.communityScheme: blackhole needs a function number in the scheme");
183
+
184
+ return prefixSets.map((prefixSet) => ({
185
+ description: "Blackhole, asked by the neighbor",
186
+ match: { large_community: community(fn, 0), prefix_set: prefixSet },
187
+ set: { blackhole: true },
188
+ action: "accept",
189
+ }));
190
+ };
191
+
192
+ const actions = (neighborAs: number): PolicyRule<never, never>[] => {
193
+ // The upstream's own blackhole community, when it takes blackholes from us.
194
+ const passOn = scheme.blackhole?.upstreams?.[neighborAs];
195
+
196
+ return [
197
+ ...(scheme.blackhole !== undefined && !passOn
198
+ ? [
199
+ {
200
+ description: "Blackholed here, never announced",
201
+ match: { large_community: community(scheme.blackhole.function, 0) },
202
+ action: "reject" as const,
203
+ },
204
+ ]
205
+ : []),
206
+ ...(scheme.do_not_announce !== undefined
207
+ ? [
208
+ {
209
+ description: "Asked not to be announced anywhere",
210
+ match: { large_community: community(scheme.do_not_announce, 0) },
211
+ action: "reject" as const,
212
+ },
213
+ {
214
+ description: `Asked not to be announced to AS${neighborAs}`,
215
+ match: { large_community: community(scheme.do_not_announce, neighborAs) },
216
+ action: "reject" as const,
217
+ },
218
+ ]
219
+ : []),
220
+ ...(scheme.blackhole !== undefined && passOn
221
+ ? [
222
+ {
223
+ description: `Blackhole passed on to AS${neighborAs}`,
224
+ match: { large_community: community(scheme.blackhole.function, 0) },
225
+ set: {
226
+ remove_communities: ours.standard,
227
+ remove_large_communities: ours.large,
228
+ add_communities: [passOn],
229
+ },
230
+ action: "accept" as const,
231
+ },
232
+ ]
233
+ : []),
234
+ ...Bgp.PREPENDS.flatMap(([name, times]) => {
235
+ const fn = scheme.prepend?.[name];
236
+ if (fn === undefined) return [];
237
+
238
+ return [
239
+ {
240
+ description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
241
+ match: { large_community: community(fn, neighborAs) },
242
+ set: { prepend: times },
243
+ },
244
+ ];
245
+ }),
246
+ {
247
+ description: "Our communities stay inside our network",
248
+ set: { remove_communities: ours.standard, remove_large_communities: ours.large },
249
+ },
250
+ ];
251
+ };
252
+
253
+ const catalogue: Community[] = [
254
+ ...Object.entries<number>(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
255
+ community: written(scheme.learned_from!.function, value),
256
+ description: `Learned from ${Bgp.article(name)}`,
257
+ })),
258
+ ...(scheme.learned_at
259
+ ? [
260
+ ...Object.entries(scheme.learned_at.sites ?? {}).map(([site, name]) => ({
261
+ community: written(scheme.learned_at!.function, site),
262
+ description: `Learned in ${name}`,
263
+ })),
264
+ { community: written(scheme.learned_at.function, "nnn"), description: "Learned at site $0" },
265
+ ]
266
+ : []),
267
+ ...(scheme.learned_from_as !== undefined
268
+ ? [{ community: written(scheme.learned_from_as, "nnn"), description: "Learned from AS$0" }]
269
+ : []),
270
+ ...(scheme.do_not_announce !== undefined
271
+ ? [
272
+ { community: written(scheme.do_not_announce, 0), description: "Do not announce to anyone" },
273
+ { community: written(scheme.do_not_announce, "nnn"), description: "Do not announce to AS$0" },
274
+ ]
275
+ : []),
276
+ ...Bgp.PREPENDS.flatMap(([name]) => {
277
+ const fn = scheme.prepend?.[name];
278
+ return fn === undefined
279
+ ? []
280
+ : [{ community: written(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
281
+ }),
282
+ ...(scheme.blackhole !== undefined
283
+ ? [
284
+ {
285
+ community: written(scheme.blackhole.function, 0),
286
+ description: Object.keys(scheme.blackhole.upstreams ?? {}).length
287
+ ? `Blackhole: dropped in AS${asn} and by its upstreams that take it`
288
+ : `Blackhole: dropped in AS${asn}, not announced further`,
289
+ },
290
+ ]
291
+ : []),
292
+ ];
293
+
294
+ return { community, learned_from: learnedFrom, tag, actions, blackhole, catalogue };
295
+ }
296
+
297
+ /** A word with its article: "a customer", "an upstream". */
298
+ private static article(word: string): string {
299
+ return /^[aeiou]/i.test(word) ? `an ${word}` : `a ${word}`;
300
+ }
301
+ }
@@ -0,0 +1,178 @@
1
+ import { Address } from "../core/address";
2
+ import type { Check, Finding } from "../core/define";
3
+ import { Bgp } from "./bgp";
4
+
5
+ /** Where traffic from outside, before anything filters it, may go. Every VLAN not listed as untrusted is trusted, so a VLAN added later is protected without being listed. */
6
+ export type TrustBoundary = {
7
+ /** VLANs carrying traffic straight from outside: transits, exchanges, an ISP handoff. */
8
+ untrusted: readonly string[];
9
+ /** The devices that filter: the only ones that may give an untrusted VLAN an address, and the only ones a port carrying both kinds may face. */
10
+ routers: readonly string[];
11
+ /**
12
+ * Links between two switches that may carry both kinds, each named by its two devices: a site's switch extended to another building, with an exchange's VLAN and its own management on one cable. Neither end gives an untrusted VLAN an address.
13
+ */
14
+ extended?: readonly (readonly [string, string])[];
15
+ };
16
+
17
+ /** Checks for `checks` in `defineNetwork`, and helpers for checks of your own. */
18
+ export class Checks {
19
+ /**
20
+ * Every BGP export ends in a reject that matches everything, so nothing leaves by accident. An export to a neighbor whose `role` is `customer` is left out: sending a customer everything you know is what it pays for, and cannot leak a route to the rest of the internet.
21
+ */
22
+ static readonly exportsEndInReject: Check = (devices) => {
23
+ const findings: Finding[] = [];
24
+
25
+ for (const device of devices) {
26
+ for (const neighbor of Bgp.neighborPolicies(device)) {
27
+ if (neighbor.role === "customer") continue;
28
+
29
+ const last = neighbor.export ? device.policies?.[neighbor.export]?.at(-1) : undefined;
30
+
31
+ if (neighbor.export && (last?.action !== "reject" || last.match)) {
32
+ findings.push({
33
+ level: "error",
34
+ device: device.name,
35
+ message: `${neighbor.name}: export ${neighbor.export} does not end in an unconditional reject`,
36
+ });
37
+ }
38
+ }
39
+ }
40
+
41
+ return findings;
42
+ };
43
+
44
+ /** A GRE or VXLAN tunnel's local address is outside what the neighbors over it are offered, or their replies route back into the tunnel. */
45
+ static readonly tunnelsOutsideOffered: Check = (devices) => {
46
+ const findings: Finding[] = [];
47
+
48
+ for (const device of devices) {
49
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
50
+ if (iface.type !== "gre" && iface.type !== "vxlan") continue;
51
+
52
+ const subnets = (iface.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
53
+
54
+ for (const neighbor of Bgp.neighborPolicies(device)) {
55
+ if (!subnets.some((subnet) => Address.contains(subnet, neighbor.address))) continue;
56
+
57
+ for (const prefix of Bgp.offered(device, neighbor.export)) {
58
+ if (Address.contains(prefix, iface.local)) {
59
+ findings.push({
60
+ level: "error",
61
+ device: device.name,
62
+ message: `${name} is sourced from ${iface.local}, inside ${prefix}, which ${neighbor.name} is offered`,
63
+ });
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ return findings;
71
+ };
72
+
73
+ /**
74
+ * A native VLAN is a dead end: native on one trunk (a port or a LAG) of a device at most, and never an access VLAN or a tagged VLAN anywhere on that device. On a switch, a native VLAN is an untagged member of every trunk it is native on, so one native VLAN shared by two trunks joins them: every untagged frame from one leaves on the other.
75
+ */
76
+ static readonly nativeVlansAreDeadEnds: Check = (devices) => {
77
+ const findings: Finding[] = [];
78
+
79
+ for (const device of devices) {
80
+ const carriers = [
81
+ ...Object.entries(device.ports ?? {}).flatMap(([name, port]) => (port ? [{ what: `port ${name}`, settings: port }] : [])),
82
+ ...Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
83
+ iface.type === "lag" ? [{ what: `LAG ${name}`, settings: iface }] : []
84
+ ),
85
+ ];
86
+
87
+ const nativeOn = new Map<string, string[]>();
88
+ for (const { what, settings } of carriers) {
89
+ if (settings.native_vlan) nativeOn.set(settings.native_vlan, [...(nativeOn.get(settings.native_vlan) ?? []), what]);
90
+ }
91
+
92
+ for (const [vlan, trunks] of nativeOn) {
93
+ const report = (message: string) =>
94
+ findings.push({ level: "error", device: device.name, message: `native VLAN ${vlan} ${message}` });
95
+
96
+ if (trunks.length > 1) report(`is native on ${trunks.join(", ")}: give each trunk its own`);
97
+
98
+ const members = carriers.filter(({ settings }) => Checks.vlansOf(settings).includes(vlan));
99
+ for (const { what } of members) report(`is also an access or tagged VLAN on ${what}`);
100
+ }
101
+ }
102
+
103
+ return findings;
104
+ };
105
+
106
+ /** The VLANs a port carries, untagged or tagged. */
107
+ static vlansOf(port: { access_vlan?: string; trunk_vlans?: readonly string[] }): string[] {
108
+ return [port.access_vlan, ...(port.trunk_vlans ?? [])].filter((vlan): vlan is string => vlan !== undefined);
109
+ }
110
+
111
+ /** A check: traffic from outside reaches your own VLANs only through a router. A port or LAG carrying an untrusted VLAN beside any other is on a router or faces one, and only a router gives an untrusted VLAN an address. */
112
+ static trustBoundary(boundary: TrustBoundary): Check {
113
+ return (devices) => {
114
+ const untrusted = new Set(boundary.untrusted);
115
+ const routers = new Set(boundary.routers);
116
+ const extended = (a: string, b: string) =>
117
+ (boundary.extended ?? []).some(([x, y]) => (x === a && y === b) || (x === b && y === a));
118
+ const findings: Finding[] = [];
119
+
120
+ for (const device of devices) {
121
+ const isRouter = routers.has(device.name);
122
+ const ports = Object.entries(device.ports ?? {}).flatMap(([name, port]) => (port ? [{ name, port }] : []));
123
+
124
+ // What carries VLANs: a port on its own, or a LAG, which faces whatever its members link to.
125
+ const carriers = [
126
+ ...ports
127
+ .filter(({ port }) => !port.lag)
128
+ .map(({ name, port }) => ({
129
+ what: `port ${name}`,
130
+ carried: Checks.vlansOf(port),
131
+ faces: port.link ? [port.link.device] : [],
132
+ })),
133
+ ...Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
134
+ iface.type === "lag"
135
+ ? [
136
+ {
137
+ what: `LAG ${name}`,
138
+ carried: Checks.vlansOf(iface),
139
+ faces: ports
140
+ .filter(({ port }) => port.lag === name && port.link)
141
+ .map(({ port }) => port.link!.device),
142
+ },
143
+ ]
144
+ : []
145
+ ),
146
+ ];
147
+
148
+ for (const { what, carried, faces } of carriers) {
149
+ const mixes = carried.some((vlan) => untrusted.has(vlan)) && carried.some((vlan) => !untrusted.has(vlan));
150
+ const facesRouter = isRouter || faces.some((name) => routers.has(name));
151
+ const isExtension = faces.length > 0 && faces.every((name) => extended(device.name, name));
152
+
153
+ if (mixes && !facesRouter && !isExtension) {
154
+ findings.push({
155
+ level: "error",
156
+ device: device.name,
157
+ message: `${what} carries an untrusted VLAN beside a trusted one and does not face a router`,
158
+ });
159
+ }
160
+ }
161
+
162
+ if (isRouter) continue;
163
+
164
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
165
+ if (iface.type === "vlan" && untrusted.has(iface.vlan)) {
166
+ findings.push({
167
+ level: "error",
168
+ device: device.name,
169
+ message: `interface ${name} puts an address on untrusted VLAN ${iface.vlan}; only a router may`,
170
+ });
171
+ }
172
+ }
173
+ }
174
+
175
+ return findings;
176
+ };
177
+ }
178
+ }
@@ -0,0 +1,117 @@
1
+ import { Address } from "../core/address";
2
+ import type { Check, Finding } from "../core/define";
3
+ import type { Ethertype, IP, Port, Prefix } from "../schema";
4
+ import { Checks } from "./checks";
5
+ import { RouteServerConfig, type RouteServer } from "./route-server";
6
+
7
+ /** A member of an internet exchange: a network with a port on the fabric and an address on the peering LAN. */
8
+ export type ExchangeMember = {
9
+ /** Lowercase, and never reused. */
10
+ name: string;
11
+ asn: number;
12
+ /** The member's port on each fabric switch, by switch name. */
13
+ ports: Readonly<Record<string, string>>;
14
+ /** Its addresses on the peering LAN. The route servers peer with these. */
15
+ ipv4?: IP;
16
+ ipv6?: IP;
17
+ };
18
+
19
+ /** An internet exchange's peering LAN, as its fabric carries it. */
20
+ export type InternetExchange<V extends string> = {
21
+ /** The peering VLAN, by the name the fabric switches give it. */
22
+ vlan: V;
23
+ /** The peering LAN's subnets. */
24
+ lan: { ipv4?: Prefix; ipv6?: Prefix };
25
+ /** Addresses on the peering LAN no member may take, such as the route servers'. */
26
+ reserved?: readonly IP[];
27
+ /** How much broadcast, multicast and unknown unicast a member port lets through, in percent of its speed. 1 when absent. */
28
+ storm_percent?: number;
29
+ /** The most MAC addresses a member port learns, usually 1. No limit when absent. No platform renders it yet, so `validate` refuses it. */
30
+ mac_limit?: number;
31
+ /** The only kinds of frame a member port takes in, usually `ipv4`, `ipv6` and `arp`. Every kind when absent. No platform renders it yet, so `validate` refuses it. */
32
+ ethertypes?: readonly Ethertype[];
33
+ };
34
+
35
+ /** An internet exchange's fabric: its member ports, a check of its members, and its route servers' config. */
36
+ export class Exchange {
37
+ /**
38
+ * Member ports and a check for an internet exchange's fabric.
39
+ *
40
+ * - `ports(device)`: every member's port on one switch. A member port is in the peering VLAN and nothing else, takes no part in spanning tree or LLDP, and lets only a trickle of broadcast, multicast and unknown unicast through. With `mac_limit` and `ethertypes`, it also learns that many MAC addresses at most and takes in only those kinds of frame.
41
+ * - `check`: members never share a name, an AS, an address or a port; every address is inside the peering LAN and not reserved; every member port carries the peering VLAN alone.
42
+ * - `bird(server)`: one route server's BIRD 2 config, with a session to every member. Write it to a file for the server; Circuit never logs in to a route server.
43
+ * - `openbgpd(server)`: the same route server's config for OpenBGPD, its `bgpd.conf`, with the same sessions and the same rules.
44
+ */
45
+ static build<const V extends string>(exchange: InternetExchange<V>, members: readonly ExchangeMember[]) {
46
+ const percent = exchange.storm_percent ?? 1;
47
+
48
+ const ports = (device: string) => {
49
+ const result: Record<string, Port<V, string, string, string>> = {};
50
+
51
+ for (const member of members) {
52
+ const port = member.ports[device];
53
+ if (!port) continue;
54
+
55
+ result[port] = {
56
+ description: `${member.name}, AS${member.asn}`,
57
+ access_vlan: exchange.vlan,
58
+ stp: false,
59
+ lldp: false,
60
+ storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
61
+ ...(exchange.mac_limit !== undefined ? { mac_limit: exchange.mac_limit } : {}),
62
+ ...(exchange.ethertypes ? { ethertypes: exchange.ethertypes } : {}),
63
+ };
64
+ }
65
+
66
+ return result;
67
+ };
68
+
69
+ const check: Check = (devices) => {
70
+ const findings: Finding[] = [];
71
+ const error = (message: string, device?: string) => findings.push({ level: "error", ...(device ? { device } : {}), message });
72
+ const taken = new Map<string, string>((exchange.reserved ?? []).map((address) => [address, "reserved"]));
73
+ // `ports()` writes one port per name, so a second member on a port would silently replace the first.
74
+ const portHolders = new Map<string, string>();
75
+
76
+ members.forEach((member, index) => {
77
+ for (const other of members.slice(index + 1)) {
78
+ if (other.name === member.name) error(`member ${member.name} is declared twice`);
79
+ if (other.asn === member.asn) error(`members ${member.name} and ${other.name} share AS${member.asn}`);
80
+ }
81
+
82
+ for (const [address, lan] of [
83
+ [member.ipv4, exchange.lan.ipv4],
84
+ [member.ipv6, exchange.lan.ipv6],
85
+ ] as const) {
86
+ if (!address) continue;
87
+ if (!lan || !Address.contains(lan, address)) error(`${address} of ${member.name} is outside the peering LAN`);
88
+
89
+ const holder = taken.get(address);
90
+ if (holder) error(`${address} of ${member.name} is already ${holder === "reserved" ? "reserved" : `${holder}'s`}`);
91
+ taken.set(address, member.name);
92
+ }
93
+
94
+ for (const [deviceName, portName] of Object.entries(member.ports)) {
95
+ const holder = portHolders.get(`${deviceName} ${portName}`);
96
+ if (holder) error(`port ${portName} is given to both ${holder} and ${member.name}`, deviceName);
97
+ portHolders.set(`${deviceName} ${portName}`, member.name);
98
+
99
+ const device = devices.find((candidate) => candidate.name === deviceName);
100
+ const port = device?.ports?.[portName as keyof typeof device.ports];
101
+ const carried = port ? Checks.vlansOf(port) : [];
102
+
103
+ if (carried.length !== 1 || carried[0] !== exchange.vlan) {
104
+ error(`port ${portName} of ${member.name} does not carry the peering VLAN alone`, deviceName);
105
+ }
106
+ }
107
+ });
108
+
109
+ return findings;
110
+ };
111
+
112
+ const bird = (server: RouteServer) => RouteServerConfig.bird(server, exchange.lan, members);
113
+ const openbgpd = (server: RouteServer) => RouteServerConfig.openbgpd(server, exchange.lan, members);
114
+
115
+ return { ports, check, bird, openbgpd };
116
+ }
117
+ }