@takodotid/circuit 0.34.3 → 0.35.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 +13 -14
  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 +17 -11
  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,594 @@
1
+ import {
2
+ Address2
3
+ } from "../chunks/index-n7w3tg5y.js";
4
+
5
+ // src/helpers/bgp.ts
6
+ class Bgp {
7
+ static PREPENDS = [
8
+ ["once", 1],
9
+ ["twice", 2],
10
+ ["three_times", 3]
11
+ ];
12
+ static sanity(lengths = {}) {
13
+ const ipv4 = lengths.ipv4 ?? { min: 8, max: 24 };
14
+ const ipv6 = lengths.ipv6 ?? { min: 16, max: 48 };
15
+ return [
16
+ {
17
+ description: `IPv4 shorter than /${ipv4.min}`,
18
+ match: { family: "ipv4", prefix_length: { max: ipv4.min - 1 } },
19
+ action: "reject"
20
+ },
21
+ {
22
+ description: `IPv4 longer than /${ipv4.max}`,
23
+ match: { family: "ipv4", prefix_length: { min: ipv4.max + 1 } },
24
+ action: "reject"
25
+ },
26
+ {
27
+ description: `IPv6 shorter than /${ipv6.min}`,
28
+ match: { family: "ipv6", prefix_length: { max: ipv6.min - 1 } },
29
+ action: "reject"
30
+ },
31
+ {
32
+ description: `IPv6 longer than /${ipv6.max}`,
33
+ match: { family: "ipv6", prefix_length: { min: ipv6.max + 1 } },
34
+ action: "reject"
35
+ },
36
+ { description: "RPKI invalid", match: { rpki: "invalid" }, action: "reject" }
37
+ ];
38
+ }
39
+ static neighborPolicies(device) {
40
+ const bgp = device.routing?.bgp;
41
+ return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
42
+ const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
43
+ return {
44
+ name,
45
+ address: neighbor.address,
46
+ role: neighbor.role ?? group?.role,
47
+ import: neighbor.import ?? group?.import,
48
+ export: neighbor.export ?? group?.export
49
+ };
50
+ });
51
+ }
52
+ static offered(device, policy) {
53
+ const rules = policy ? device.policies?.[policy] ?? [] : [];
54
+ return rules.filter((rule) => rule.action === "accept" && rule.match?.prefix).map((rule) => rule.match.prefix);
55
+ }
56
+ static communityScheme(scheme) {
57
+ const { asn } = scheme;
58
+ const community = (fn, parameter) => `${asn}:${fn}:${parameter}`;
59
+ const anyOf = (fn) => `${asn}:${fn}:*`;
60
+ const written = (fn, parameter) => `${asn}:${fn}:${parameter}`;
61
+ const ours = {
62
+ standard: [`${asn}:*`],
63
+ large: [`${asn}:*:*`]
64
+ };
65
+ const learnedFrom = Object.fromEntries(Object.entries(scheme.learned_from?.classes ?? {}).map(([name, value]) => [
66
+ name,
67
+ community(scheme.learned_from.function, value)
68
+ ]));
69
+ const information = [scheme.learned_from?.function, scheme.learned_at?.function, scheme.learned_from_as].filter((fn) => fn !== undefined);
70
+ const tag = (neighborClass, neighborAs, options = {}) => {
71
+ const { site, trusted = false } = options;
72
+ if (scheme.learned_at && site === undefined)
73
+ throw new Error(`Bgp.communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
74
+ return {
75
+ description: `Learned from AS${neighborAs}`,
76
+ set: {
77
+ remove_communities: ours.standard,
78
+ remove_large_communities: trusted ? information.map(anyOf) : ours.large,
79
+ add_large_communities: [
80
+ ...scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : [],
81
+ ...scheme.learned_at ? [community(scheme.learned_at.function, site)] : [],
82
+ ...scheme.learned_from_as !== undefined ? [community(scheme.learned_from_as, neighborAs)] : []
83
+ ]
84
+ }
85
+ };
86
+ };
87
+ const blackhole = (prefixSets) => {
88
+ const fn = scheme.blackhole?.function;
89
+ if (fn === undefined)
90
+ throw new Error("Bgp.communityScheme: blackhole needs a function number in the scheme");
91
+ return prefixSets.map((prefixSet) => ({
92
+ description: "Blackhole, asked by the neighbor",
93
+ match: { large_community: community(fn, 0), prefix_set: prefixSet },
94
+ set: { blackhole: true },
95
+ action: "accept"
96
+ }));
97
+ };
98
+ const actions = (neighborAs) => {
99
+ const passOn = scheme.blackhole?.upstreams?.[neighborAs];
100
+ return [
101
+ ...scheme.blackhole !== undefined && !passOn ? [
102
+ {
103
+ description: "Blackholed here, never announced",
104
+ match: { large_community: community(scheme.blackhole.function, 0) },
105
+ action: "reject"
106
+ }
107
+ ] : [],
108
+ ...scheme.do_not_announce !== undefined ? [
109
+ {
110
+ description: "Asked not to be announced anywhere",
111
+ match: { large_community: community(scheme.do_not_announce, 0) },
112
+ action: "reject"
113
+ },
114
+ {
115
+ description: `Asked not to be announced to AS${neighborAs}`,
116
+ match: { large_community: community(scheme.do_not_announce, neighborAs) },
117
+ action: "reject"
118
+ }
119
+ ] : [],
120
+ ...scheme.blackhole !== undefined && passOn ? [
121
+ {
122
+ description: `Blackhole passed on to AS${neighborAs}`,
123
+ match: { large_community: community(scheme.blackhole.function, 0) },
124
+ set: {
125
+ remove_communities: ours.standard,
126
+ remove_large_communities: ours.large,
127
+ add_communities: [passOn]
128
+ },
129
+ action: "accept"
130
+ }
131
+ ] : [],
132
+ ...Bgp.PREPENDS.flatMap(([name, times]) => {
133
+ const fn = scheme.prepend?.[name];
134
+ if (fn === undefined)
135
+ return [];
136
+ return [
137
+ {
138
+ description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
139
+ match: { large_community: community(fn, neighborAs) },
140
+ set: { prepend: times }
141
+ }
142
+ ];
143
+ }),
144
+ {
145
+ description: "Our communities stay inside our network",
146
+ set: { remove_communities: ours.standard, remove_large_communities: ours.large }
147
+ }
148
+ ];
149
+ };
150
+ const catalogue = [
151
+ ...Object.entries(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
152
+ community: written(scheme.learned_from.function, value),
153
+ description: `Learned from ${Bgp.article(name)}`
154
+ })),
155
+ ...scheme.learned_at ? [
156
+ ...Object.entries(scheme.learned_at.sites ?? {}).map(([site, name]) => ({
157
+ community: written(scheme.learned_at.function, site),
158
+ description: `Learned in ${name}`
159
+ })),
160
+ { community: written(scheme.learned_at.function, "nnn"), description: "Learned at site $0" }
161
+ ] : [],
162
+ ...scheme.learned_from_as !== undefined ? [{ community: written(scheme.learned_from_as, "nnn"), description: "Learned from AS$0" }] : [],
163
+ ...scheme.do_not_announce !== undefined ? [
164
+ { community: written(scheme.do_not_announce, 0), description: "Do not announce to anyone" },
165
+ { community: written(scheme.do_not_announce, "nnn"), description: "Do not announce to AS$0" }
166
+ ] : [],
167
+ ...Bgp.PREPENDS.flatMap(([name]) => {
168
+ const fn = scheme.prepend?.[name];
169
+ return fn === undefined ? [] : [{ community: written(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
170
+ }),
171
+ ...scheme.blackhole !== undefined ? [
172
+ {
173
+ community: written(scheme.blackhole.function, 0),
174
+ description: Object.keys(scheme.blackhole.upstreams ?? {}).length ? `Blackhole: dropped in AS${asn} and by its upstreams that take it` : `Blackhole: dropped in AS${asn}, not announced further`
175
+ }
176
+ ] : []
177
+ ];
178
+ return { community, learned_from: learnedFrom, tag, actions, blackhole, catalogue };
179
+ }
180
+ static article(word) {
181
+ return /^[aeiou]/i.test(word) ? `an ${word}` : `a ${word}`;
182
+ }
183
+ }
184
+ // src/helpers/checks.ts
185
+ class Checks {
186
+ static exportsEndInReject = (devices) => {
187
+ const findings = [];
188
+ for (const device of devices) {
189
+ for (const neighbor of Bgp.neighborPolicies(device)) {
190
+ if (neighbor.role === "customer")
191
+ continue;
192
+ const last = neighbor.export ? device.policies?.[neighbor.export]?.at(-1) : undefined;
193
+ if (neighbor.export && (last?.action !== "reject" || last.match)) {
194
+ findings.push({
195
+ level: "error",
196
+ device: device.name,
197
+ message: `${neighbor.name}: export ${neighbor.export} does not end in an unconditional reject`
198
+ });
199
+ }
200
+ }
201
+ }
202
+ return findings;
203
+ };
204
+ static tunnelsOutsideOffered = (devices) => {
205
+ const findings = [];
206
+ for (const device of devices) {
207
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
208
+ if (iface.type !== "gre" && iface.type !== "vxlan")
209
+ continue;
210
+ const subnets = (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
211
+ for (const neighbor of Bgp.neighborPolicies(device)) {
212
+ if (!subnets.some((subnet) => Address2.contains(subnet, neighbor.address)))
213
+ continue;
214
+ for (const prefix of Bgp.offered(device, neighbor.export)) {
215
+ if (Address2.contains(prefix, iface.local)) {
216
+ findings.push({
217
+ level: "error",
218
+ device: device.name,
219
+ message: `${name} is sourced from ${iface.local}, inside ${prefix}, which ${neighbor.name} is offered`
220
+ });
221
+ }
222
+ }
223
+ }
224
+ }
225
+ }
226
+ return findings;
227
+ };
228
+ static nativeVlansAreDeadEnds = (devices) => {
229
+ const findings = [];
230
+ for (const device of devices) {
231
+ const carriers = [
232
+ ...Object.entries(device.ports ?? {}).flatMap(([name, port]) => port ? [{ what: `port ${name}`, settings: port }] : []),
233
+ ...Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) => iface.type === "lag" ? [{ what: `LAG ${name}`, settings: iface }] : [])
234
+ ];
235
+ const nativeOn = new Map;
236
+ for (const { what, settings } of carriers) {
237
+ if (settings.native_vlan)
238
+ nativeOn.set(settings.native_vlan, [...nativeOn.get(settings.native_vlan) ?? [], what]);
239
+ }
240
+ for (const [vlan, trunks] of nativeOn) {
241
+ const report = (message) => findings.push({ level: "error", device: device.name, message: `native VLAN ${vlan} ${message}` });
242
+ if (trunks.length > 1)
243
+ report(`is native on ${trunks.join(", ")}: give each trunk its own`);
244
+ const members = carriers.filter(({ settings }) => Checks.vlansOf(settings).includes(vlan));
245
+ for (const { what } of members)
246
+ report(`is also an access or tagged VLAN on ${what}`);
247
+ }
248
+ }
249
+ return findings;
250
+ };
251
+ static vlansOf(port) {
252
+ return [port.access_vlan, ...port.trunk_vlans ?? []].filter((vlan) => vlan !== undefined);
253
+ }
254
+ static trustBoundary(boundary) {
255
+ return (devices) => {
256
+ const untrusted = new Set(boundary.untrusted);
257
+ const routers = new Set(boundary.routers);
258
+ const extended = (a, b) => (boundary.extended ?? []).some(([x, y]) => x === a && y === b || x === b && y === a);
259
+ const findings = [];
260
+ for (const device of devices) {
261
+ const isRouter = routers.has(device.name);
262
+ const ports = Object.entries(device.ports ?? {}).flatMap(([name, port]) => port ? [{ name, port }] : []);
263
+ const carriers = [
264
+ ...ports.filter(({ port }) => !port.lag).map(({ name, port }) => ({
265
+ what: `port ${name}`,
266
+ carried: Checks.vlansOf(port),
267
+ faces: port.link ? [port.link.device] : []
268
+ })),
269
+ ...Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) => iface.type === "lag" ? [
270
+ {
271
+ what: `LAG ${name}`,
272
+ carried: Checks.vlansOf(iface),
273
+ faces: ports.filter(({ port }) => port.lag === name && port.link).map(({ port }) => port.link.device)
274
+ }
275
+ ] : [])
276
+ ];
277
+ for (const { what, carried, faces } of carriers) {
278
+ const mixes = carried.some((vlan) => untrusted.has(vlan)) && carried.some((vlan) => !untrusted.has(vlan));
279
+ const facesRouter = isRouter || faces.some((name) => routers.has(name));
280
+ const isExtension = faces.length > 0 && faces.every((name) => extended(device.name, name));
281
+ if (mixes && !facesRouter && !isExtension) {
282
+ findings.push({
283
+ level: "error",
284
+ device: device.name,
285
+ message: `${what} carries an untrusted VLAN beside a trusted one and does not face a router`
286
+ });
287
+ }
288
+ }
289
+ if (isRouter)
290
+ continue;
291
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
292
+ if (iface.type === "vlan" && untrusted.has(iface.vlan)) {
293
+ findings.push({
294
+ level: "error",
295
+ device: device.name,
296
+ message: `interface ${name} puts an address on untrusted VLAN ${iface.vlan}; only a router may`
297
+ });
298
+ }
299
+ }
300
+ }
301
+ return findings;
302
+ };
303
+ }
304
+ }
305
+ // src/helpers/route-server.ts
306
+ class RouteServerConfig {
307
+ static LENGTHS = {
308
+ ipv4: { shortest: 8, longest: 24 },
309
+ ipv6: { shortest: 12, longest: 48 }
310
+ };
311
+ static RULES = [
312
+ "# 1. the first AS in its path is the member's own,",
313
+ "# 2. its next hop is the member's own address on the peering LAN,",
314
+ "# 3. it is not inside the peering LAN,",
315
+ `# 4. it is /${RouteServerConfig.LENGTHS.ipv4.shortest} to /${RouteServerConfig.LENGTHS.ipv4.longest} long for IPv4, /${RouteServerConfig.LENGTHS.ipv6.shortest} to /${RouteServerConfig.LENGTHS.ipv6.longest} for IPv6.`
316
+ ];
317
+ static bird(server, lan, members) {
318
+ const families = RouteServerConfig.families(server, lan);
319
+ const sessions = [];
320
+ const names = new Map;
321
+ for (const member of members) {
322
+ for (const family of families) {
323
+ const neighbor = member[family];
324
+ if (!neighbor)
325
+ continue;
326
+ const name = RouteServerConfig.protocolName(member.name, family);
327
+ const holder = names.get(name);
328
+ if (holder)
329
+ throw new Error(`members ${holder} and ${member.name} would both have session ${name}`);
330
+ names.set(name, member.name);
331
+ sessions.push(RouteServerConfig.birdSession(name, server, member, family, neighbor, lan[family]));
332
+ }
333
+ }
334
+ const header = [
335
+ `# ${server.name}, a route server of the exchange, for BIRD 2.`,
336
+ "# Circuit wrote it from the member list: change the list, not this file.",
337
+ "",
338
+ `router id ${server.router_id};`,
339
+ "",
340
+ "# Learns the server's interfaces and addresses. It sends no route anywhere.",
341
+ "protocol device {",
342
+ "}",
343
+ "",
344
+ "# One session per member and family. A route a member sends is kept only when:",
345
+ ...RouteServerConfig.RULES,
346
+ "# Every member gets every route the others sent, as they sent it.",
347
+ "# A route that is not kept stays visible: birdc show route filtered."
348
+ ];
349
+ return [...header, ...sessions.flatMap((session) => ["", session])].join(`
350
+ `) + `
351
+ `;
352
+ }
353
+ static openbgpd(server, lan, members) {
354
+ const families = RouteServerConfig.families(server, lan).filter((family) => members.some((member) => member[family]));
355
+ const lines = [
356
+ `# ${server.name}, a route server of the exchange, for OpenBGPD.`,
357
+ "# Circuit wrote it from the member list: change the list, not this file.",
358
+ "",
359
+ `AS ${server.asn}`,
360
+ `router-id ${server.router_id}`,
361
+ "",
362
+ "# The server passes routes on and carries no traffic, so it keeps them out of its own routing table.",
363
+ "fib-update no",
364
+ "",
365
+ "# Members see each other's paths as they were sent: the server never adds its own AS, and passes MED and communities on untouched.",
366
+ "transparent-as yes",
367
+ "",
368
+ "# A route that is not kept stays visible: bgpctl show rib filtered.",
369
+ "rde rib Loc-RIB include filtered"
370
+ ];
371
+ for (const family of families) {
372
+ lines.push("", ...RouteServerConfig.openbgpdGroup(server, family, members));
373
+ }
374
+ lines.push("", "# A route a member sends is kept only when:", ...RouteServerConfig.RULES);
375
+ lines.push("# The last rule a route matches decides, and a route no rule allows is dropped.");
376
+ for (const family of families) {
377
+ const { shortest, longest } = RouteServerConfig.LENGTHS[family];
378
+ const any = family === "ipv4" ? "0.0.0.0/0" : "::/0";
379
+ lines.push(`allow from group "${family} members" peer-as neighbor-as nexthop neighbor prefix ${any} prefixlen ${shortest} - ${longest}`);
380
+ }
381
+ for (const family of families) {
382
+ lines.push(`deny from any prefix ${Address2.prefixOf(lan[family])} or-longer`);
383
+ }
384
+ lines.push("", "# Every member gets every route the others sent, as they sent it.", "allow to any");
385
+ return lines.join(`
386
+ `) + `
387
+ `;
388
+ }
389
+ static families(server, lan) {
390
+ const families = ["ipv4", "ipv6"].filter((family) => server[family]);
391
+ if (!families.length)
392
+ throw new Error(`route server ${server.name} has no address on the peering LAN`);
393
+ for (const family of families) {
394
+ const prefix = lan[family];
395
+ const address = server[family];
396
+ if (!prefix || !Address2.contains(prefix, address))
397
+ throw new Error(`${address} of route server ${server.name} is outside the peering LAN`);
398
+ }
399
+ return families;
400
+ }
401
+ static protocolName(member, family) {
402
+ const name = `${member.toLowerCase().replace(/[^a-z0-9_]/g, "_")}_${family}`;
403
+ return /^[a-z]/.test(name) ? name : `m_${name}`;
404
+ }
405
+ static description(member) {
406
+ return `"${member.name.replace(/["\\]/g, "")}, AS${member.asn}"`;
407
+ }
408
+ static birdSession(name, server, member, family, neighbor, lan) {
409
+ const { shortest, longest } = RouteServerConfig.LENGTHS[family];
410
+ return [
411
+ `protocol bgp ${name} {`,
412
+ ` description ${RouteServerConfig.description(member)};`,
413
+ ` local ${server[family]} as ${server.asn};`,
414
+ ` neighbor ${neighbor} as ${member.asn};`,
415
+ " rs client;",
416
+ ` ${family} {`,
417
+ " import keep filtered on;",
418
+ " import filter {",
419
+ ` if bgp_path.first != ${member.asn} then reject;`,
420
+ ` if bgp_next_hop != ${neighbor} then reject;`,
421
+ ` if net ~ [ ${Address2.prefixOf(lan)}+ ] then reject;`,
422
+ ` if net.len < ${shortest} || net.len > ${longest} then reject;`,
423
+ " accept;",
424
+ " };",
425
+ " export all;",
426
+ " };",
427
+ "}"
428
+ ].join(`
429
+ `);
430
+ }
431
+ static openbgpdGroup(server, family, members) {
432
+ const lines = [
433
+ `# One session per member with an ${family === "ipv4" ? "IPv4" : "IPv6"} address.`,
434
+ `group "${family} members" {`,
435
+ ` local-address ${server[family]}`,
436
+ " # A route whose first AS is not the member's own is dropped by the rules below. The default would close the whole session.",
437
+ " enforce neighbor-as no"
438
+ ];
439
+ for (const member of members) {
440
+ const neighbor = member[family];
441
+ if (!neighbor)
442
+ continue;
443
+ lines.push(` neighbor ${neighbor} {`, ` remote-as ${member.asn}`, ` descr ${RouteServerConfig.description(member)}`, " }");
444
+ }
445
+ lines.push("}");
446
+ return lines;
447
+ }
448
+ }
449
+
450
+ // src/helpers/exchange.ts
451
+ class Exchange {
452
+ static build(exchange, members) {
453
+ const percent = exchange.storm_percent ?? 1;
454
+ const ports = (device) => {
455
+ const result = {};
456
+ for (const member of members) {
457
+ const port = member.ports[device];
458
+ if (!port)
459
+ continue;
460
+ result[port] = {
461
+ description: `${member.name}, AS${member.asn}`,
462
+ access_vlan: exchange.vlan,
463
+ stp: false,
464
+ lldp: false,
465
+ storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
466
+ ...exchange.mac_limit !== undefined ? { mac_limit: exchange.mac_limit } : {},
467
+ ...exchange.ethertypes ? { ethertypes: exchange.ethertypes } : {}
468
+ };
469
+ }
470
+ return result;
471
+ };
472
+ const check = (devices) => {
473
+ const findings = [];
474
+ const error = (message, device) => findings.push({ level: "error", ...device ? { device } : {}, message });
475
+ const taken = new Map((exchange.reserved ?? []).map((address) => [address, "reserved"]));
476
+ const portHolders = new Map;
477
+ members.forEach((member, index) => {
478
+ for (const other of members.slice(index + 1)) {
479
+ if (other.name === member.name)
480
+ error(`member ${member.name} is declared twice`);
481
+ if (other.asn === member.asn)
482
+ error(`members ${member.name} and ${other.name} share AS${member.asn}`);
483
+ }
484
+ for (const [address, lan] of [
485
+ [member.ipv4, exchange.lan.ipv4],
486
+ [member.ipv6, exchange.lan.ipv6]
487
+ ]) {
488
+ if (!address)
489
+ continue;
490
+ if (!lan || !Address2.contains(lan, address))
491
+ error(`${address} of ${member.name} is outside the peering LAN`);
492
+ const holder = taken.get(address);
493
+ if (holder)
494
+ error(`${address} of ${member.name} is already ${holder === "reserved" ? "reserved" : `${holder}'s`}`);
495
+ taken.set(address, member.name);
496
+ }
497
+ for (const [deviceName, portName] of Object.entries(member.ports)) {
498
+ const holder = portHolders.get(`${deviceName} ${portName}`);
499
+ if (holder)
500
+ error(`port ${portName} is given to both ${holder} and ${member.name}`, deviceName);
501
+ portHolders.set(`${deviceName} ${portName}`, member.name);
502
+ const device = devices.find((candidate) => candidate.name === deviceName);
503
+ const port = device?.ports?.[portName];
504
+ const carried = port ? Checks.vlansOf(port) : [];
505
+ if (carried.length !== 1 || carried[0] !== exchange.vlan) {
506
+ error(`port ${portName} of ${member.name} does not carry the peering VLAN alone`, deviceName);
507
+ }
508
+ }
509
+ });
510
+ return findings;
511
+ };
512
+ const bird = (server) => RouteServerConfig.bird(server, exchange.lan, members);
513
+ const openbgpd = (server) => RouteServerConfig.openbgpd(server, exchange.lan, members);
514
+ return { ports, check, bird, openbgpd };
515
+ }
516
+ }
517
+ // src/helpers/filters.ts
518
+ class Filters {
519
+ static MARTIANS = ["127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/8"];
520
+ static PRIVATE_RANGES = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"];
521
+ static AMPLIFIERS = {
522
+ qotd: 17,
523
+ chargen: 19,
524
+ dns: 53,
525
+ portmap: 111,
526
+ ntp: 123,
527
+ netbios: 137,
528
+ snmp: 161,
529
+ cldap: 389,
530
+ slp: 427,
531
+ rip: 520,
532
+ "ms-sql": 1434,
533
+ ssdp: 1900,
534
+ ard: 3283,
535
+ "ws-discovery": 3702,
536
+ mdns: 5353,
537
+ coap: 5683,
538
+ ubiquiti: 10001,
539
+ memcached: 11211,
540
+ dvr: 37810
541
+ };
542
+ static ASKED = new Set([Filters.AMPLIFIERS.dns, Filters.AMPLIFIERS.ntp, Filters.AMPLIFIERS.snmp]);
543
+ static BAD_TCP_FLAGS = [
544
+ ["FIN+SYN", ["fin", "syn"], []],
545
+ ["SYN+RST", ["syn", "rst"], []],
546
+ ["FIN+RST", ["fin", "rst"], []],
547
+ ["FIN without ACK", ["fin"], ["ack"]],
548
+ ["FIN+URG", ["fin", "urg"], []],
549
+ ["Xmas scan", ["fin", "syn", "psh", "urg"], []]
550
+ ];
551
+ static antiSpoofing(vlan, sources) {
552
+ return sources.map((src) => ({
553
+ description: `${vlan}: drop source ${src}`,
554
+ match: { vlan, src },
555
+ action: "drop"
556
+ }));
557
+ }
558
+ static antiAmplification(destinations, options = {}) {
559
+ return destinations.flatMap((dst) => [
560
+ ...(options.keep_fragments ?? []).filter((prefix) => Address2.contains(dst, prefix)).map((prefix) => ({
561
+ description: `UDP fragments to ${prefix} kept`,
562
+ match: { protocol: "udp", dst: prefix, src_port: 0 },
563
+ action: "accept"
564
+ })),
565
+ { description: `UDP fragments to ${dst}`, match: { protocol: "udp", dst, src_port: 0 }, action: "drop" },
566
+ ...Object.entries(Filters.AMPLIFIERS).flatMap(([name, port]) => Filters.ASKED.has(port) ? [
567
+ {
568
+ description: `UDP to ${dst} from ${name}, between two servers`,
569
+ match: { protocol: "udp", dst, src_port: port, dst_port: port },
570
+ action: "accept"
571
+ },
572
+ {
573
+ description: `UDP to ${dst} from ${name}, to a port below 1024`,
574
+ match: { protocol: "udp", dst, src_port: port, dst_port: { min: 0, max: 1023 } },
575
+ action: "drop"
576
+ }
577
+ ] : [{ description: `UDP to ${dst} from ${name}`, match: { protocol: "udp", dst, src_port: port }, action: "drop" }])
578
+ ]);
579
+ }
580
+ static badTcpFlags() {
581
+ return Filters.BAD_TCP_FLAGS.map(([name, set, unset]) => ({
582
+ description: `Bad flags ${name}`,
583
+ match: { protocol: "tcp", tcp_flags: { set, unset } },
584
+ action: "drop"
585
+ }));
586
+ }
587
+ }
588
+ export {
589
+ Bgp,
590
+ Checks,
591
+ Exchange,
592
+ Filters,
593
+ RouteServerConfig
594
+ };
package/dist/index.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import {
2
- familyOf2,
3
- isAddress2,
4
- isCidr2,
5
- network,
6
- contains2,
7
- host2,
8
- isGlobal2
9
- } from "./chunks/index-v8zgr7wm.js";
2
+ Utils2,
3
+ Secrets2
4
+ } from "./chunks/index-20kcjxjg.js";
5
+ import {
6
+ Address2
7
+ } from "./chunks/index-n7w3tg5y.js";
10
8
 
11
9
  // src/core/define.ts
12
10
  var secret = (name) => ({ secret: name });
@@ -19,29 +17,12 @@ function defineDevice(device) {
19
17
  return device;
20
18
  }
21
19
  var defineNetwork = (network) => network;
22
- // src/core/merge.ts
23
- function merge(...records) {
24
- const result = {};
25
- for (const record of records) {
26
- for (const [key, value] of Object.entries(record)) {
27
- if (key in result)
28
- throw new Error(`merge: ${key} is declared twice`);
29
- result[key] = value;
30
- }
31
- }
32
- return result;
33
- }
34
20
  export {
35
- contains2 as contains,
21
+ Address2 as Address,
22
+ Secrets2 as Secrets,
23
+ Utils2 as Utils,
36
24
  defineDevice,
37
25
  defineNetwork,
38
- familyOf2 as familyOf,
39
- host2 as host,
40
- isAddress2 as isAddress,
41
- isCidr2 as isCidr,
42
- isGlobal2 as isGlobal,
43
- merge,
44
- network as prefixOf,
45
26
  secret,
46
27
  secretFile
47
28
  };
@@ -37,10 +37,26 @@ export type AddressReport = {
37
37
  blocks: Block[];
38
38
  };
39
39
  };
40
- /** Every address in use, by segment, under the space the network owns. With `within`, only what falls inside that prefix, as the one space. */
41
- export declare function addressesOf(devices: readonly Device[], within?: string): AddressReport;
42
- /** Free prefixes of one length inside the free space, up to `limit`, and how many there are in all. */
43
- export declare function freeOfLength(free: readonly string[], length: number, limit: number): {
44
- prefixes: string[];
45
- total: bigint;
46
- };
40
+ export declare class AddressSpace {
41
+ private static isHostRoute;
42
+ private static numberOf;
43
+ /** Addresses a segment can give out: IPv4 leaves out the network and broadcast addresses below /31, and IPv6 the subnet-router anycast address. */
44
+ private static usable;
45
+ /** Ranges merged, so overlapping ones count once. */
46
+ private static merged;
47
+ /** What is inside `within` and not in any of the ranges. */
48
+ private static gaps;
49
+ /** `12` or, for a count too long to read, `about 2^64`, to the nearest power of two. */
50
+ private static count;
51
+ private static byAddress;
52
+ /** IPv4 before IPv6, then by first address. */
53
+ private static comparePrefixes;
54
+ private static byPrefix;
55
+ /** Every address in use, by segment, under the space the network owns. With `within`, only what falls inside that prefix, as the one space. */
56
+ static report(devices: readonly Device[], within?: string): AddressReport;
57
+ /** Free prefixes of one length inside the free space, up to `limit`, and how many there are in all. */
58
+ static freeOfLength(free: readonly string[], length: number, limit: number): {
59
+ prefixes: string[];
60
+ total: bigint;
61
+ };
62
+ }