@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
@@ -1,579 +0,0 @@
1
- // Building blocks most networks write the same way: address lists, edge filters, a BGP import sanity policy, checks, and helpers for checks of your own. Each returns plain config to place where it belongs, so nothing here applies itself.
2
-
3
- import { contains } from "../core/addr";
4
- import type { Check, Community, Finding } from "../core/define";
5
- import type { AclRule, Device, FilterRule, IP, PolicyRule, Port, Prefix, TcpFlag } from "../schema";
6
-
7
- /** IPv4 sources that never arrive from the internet: loopback, link-local and "this network", after RFC 6890. */
8
- export const MARTIANS = ["127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/8"] as const satisfies readonly Prefix[];
9
-
10
- /** IPv4 private space, RFC 1918. */
11
- export const PRIVATE_RANGES = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] as const satisfies readonly Prefix[];
12
-
13
- /** Hardware ACL rules dropping traffic arriving on a VLAN from sources that cannot be there: your own space, martians, private ranges. One rule per source. */
14
- export const antiSpoofing = <V extends string>(vlan: V, sources: readonly Prefix[]): AclRule<V>[] =>
15
- sources.map((src) => ({
16
- description: `${vlan}: drop source ${src}`,
17
- match: { vlan, src },
18
- action: "drop",
19
- }));
20
-
21
- /** UDP ports of services that answer a small request with a much larger reply, which attackers use to reflect floods at a victim. A reflected flood arrives from these ports. */
22
- export const AMPLIFIERS = {
23
- qotd: 17,
24
- chargen: 19,
25
- dns: 53,
26
- portmap: 111,
27
- ntp: 123,
28
- netbios: 137,
29
- snmp: 161,
30
- cldap: 389,
31
- slp: 427,
32
- rip: 520,
33
- "ms-sql": 1434,
34
- ssdp: 1900,
35
- ard: 3283,
36
- "ws-discovery": 3702,
37
- mdns: 5353,
38
- coap: 5683,
39
- ubiquiti: 10001,
40
- memcached: 11211,
41
- dvr: 37810,
42
- } as const satisfies Record<string, number>;
43
-
44
- /** Of the amplifiers, the services hosts do ask across the internet: resolvers, time servers, monitoring. A reply to a real request goes back to the asking port, 1024 or above, or to the service's own port when two servers talk. */
45
- const ASKED: ReadonlySet<number> = new Set([AMPLIFIERS.dns, AMPLIFIERS.ntp, AMPLIFIERS.snmp]);
46
-
47
- /** What `antiAmplification` leaves alone. */
48
- export type AntiAmplificationOptions = {
49
- /** Prefixes inside the destinations whose UDP fragments pass: for a host that receives UDP too large for one packet, such as SIP or some VPNs. Every fragment after the first is dropped when absent. */
50
- keep_fragments?: readonly Prefix[];
51
- };
52
-
53
- /**
54
- * Hardware ACL rules dropping reflected UDP floods toward `destinations`, in the switch chip, without limiting anything else.
55
- *
56
- * - Replies from an amplifier no host asks across the internet are dropped.
57
- * - Replies from DNS, NTP and SNMP are dropped only toward a port below 1024 that is not the service's own. A host that runs its own resolver, time server or monitoring keeps its answers. The service's own port passes first, so every drop takes the one range 0 to 1023: a switch chip has few port-range checkers, and on RouterOS a rule past them is invalid with every rule after it.
58
- * - The fragments after the first carry no port, and a switch chip reads them as port 0. They are dropped, but toward `keep_fragments`.
59
- */
60
- export function antiAmplification(destinations: readonly Prefix[], options: AntiAmplificationOptions = {}): AclRule<never>[] {
61
- return destinations.flatMap((dst) => [
62
- ...(options.keep_fragments ?? [])
63
- .filter((prefix) => contains(dst, prefix))
64
- .map((prefix): AclRule<never> => ({
65
- description: `UDP fragments to ${prefix} kept`,
66
- match: { protocol: "udp", dst: prefix, src_port: 0 },
67
- action: "accept",
68
- })),
69
- { description: `UDP fragments to ${dst}`, match: { protocol: "udp", dst, src_port: 0 }, action: "drop" },
70
- ...Object.entries(AMPLIFIERS).flatMap(([name, port]): AclRule<never>[] =>
71
- ASKED.has(port)
72
- ? [
73
- {
74
- description: `UDP to ${dst} from ${name}, between two servers`,
75
- match: { protocol: "udp", dst, src_port: port, dst_port: port },
76
- action: "accept",
77
- },
78
- {
79
- description: `UDP to ${dst} from ${name}, to a port below 1024`,
80
- match: { protocol: "udp", dst, src_port: port, dst_port: { min: 0, max: 1023 } },
81
- action: "drop",
82
- },
83
- ]
84
- : [{ description: `UDP to ${dst} from ${name}`, match: { protocol: "udp", dst, src_port: port }, action: "drop" }]
85
- ),
86
- ]);
87
- }
88
-
89
- /** TCP flag combinations no legitimate packet carries: name, flags set, flags unset. */
90
- const BAD_TCP_FLAGS = [
91
- ["FIN+SYN", ["fin", "syn"], []],
92
- ["SYN+RST", ["syn", "rst"], []],
93
- ["FIN+RST", ["fin", "rst"], []],
94
- ["FIN without ACK", ["fin"], ["ack"]],
95
- ["FIN+URG", ["fin", "urg"], []],
96
- ["Xmas scan", ["fin", "syn", "psh", "urg"], []],
97
- ] as const satisfies readonly (readonly [string, readonly TcpFlag[], readonly TcpFlag[]])[];
98
-
99
- /** Firewall rules dropping packets with a TCP flag combination no legitimate packet carries, for a forward chain. */
100
- export const badTcpFlags = (): FilterRule<never, never>[] =>
101
- BAD_TCP_FLAGS.map(([name, set, unset]) => ({
102
- description: `Bad flags ${name}`,
103
- match: { protocol: "tcp", tcp_flags: { set, unset } },
104
- action: "drop",
105
- }));
106
-
107
- /** The prefix lengths a BGP import accepts. */
108
- export type SanityLengths = {
109
- /** Shortest and longest IPv4 prefix accepted. /8 to /24 when absent. */
110
- ipv4?: { min: number; max: number };
111
- /** Shortest and longest IPv6 prefix accepted. /16 to /48 when absent. */
112
- ipv6?: { min: number; max: number };
113
- };
114
-
115
- /** 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. */
116
- export function bgpSanity(lengths: SanityLengths = {}): PolicyRule<never, never>[] {
117
- const ipv4 = lengths.ipv4 ?? { min: 8, max: 24 };
118
- const ipv6 = lengths.ipv6 ?? { min: 16, max: 48 };
119
-
120
- return [
121
- {
122
- description: `IPv4 shorter than /${ipv4.min}`,
123
- match: { family: "ipv4", prefix_length: { max: ipv4.min - 1 } },
124
- action: "reject",
125
- },
126
- { description: `IPv4 longer than /${ipv4.max}`, match: { family: "ipv4", prefix_length: { min: ipv4.max + 1 } }, action: "reject" },
127
- {
128
- description: `IPv6 shorter than /${ipv6.min}`,
129
- match: { family: "ipv6", prefix_length: { max: ipv6.min - 1 } },
130
- action: "reject",
131
- },
132
- { description: `IPv6 longer than /${ipv6.max}`, match: { family: "ipv6", prefix_length: { min: ipv6.max + 1 } }, action: "reject" },
133
- { description: "RPKI invalid", match: { rpki: "invalid" }, action: "reject" },
134
- ];
135
- }
136
-
137
- /** Each BGP neighbor's own policies and role, or its group's. */
138
- export function neighborPolicies(device: Device) {
139
- const bgp = device.routing?.bgp;
140
-
141
- return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
142
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
143
-
144
- return {
145
- name,
146
- address: neighbor.address,
147
- role: neighbor.role ?? group?.role,
148
- import: neighbor.import ?? group?.import,
149
- export: neighbor.export ?? group?.export,
150
- };
151
- });
152
- }
153
-
154
- /** The prefixes an export policy accepts by name. */
155
- export function offered(device: Device, policy: string | undefined): string[] {
156
- const rules = policy ? (device.policies?.[policy] ?? []) : [];
157
- return rules.filter((rule) => rule.action === "accept" && rule.match?.prefix).map((rule) => rule.match!.prefix!);
158
- }
159
-
160
- /**
161
- * 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.
162
- */
163
- export const exportsEndInReject: Check = (devices) => {
164
- const findings: Finding[] = [];
165
-
166
- for (const device of devices) {
167
- for (const neighbor of neighborPolicies(device)) {
168
- if (neighbor.role === "customer") continue;
169
-
170
- const last = neighbor.export ? device.policies?.[neighbor.export]?.at(-1) : undefined;
171
-
172
- if (neighbor.export && (last?.action !== "reject" || last.match)) {
173
- findings.push({
174
- level: "error",
175
- device: device.name,
176
- message: `${neighbor.name}: export ${neighbor.export} does not end in an unconditional reject`,
177
- });
178
- }
179
- }
180
- }
181
-
182
- return findings;
183
- };
184
-
185
- /** The VLANs a port carries, untagged or tagged. */
186
- export const vlansOf = (port: { access_vlan?: string; trunk_vlans?: readonly string[] }): string[] =>
187
- [port.access_vlan, ...(port.trunk_vlans ?? [])].filter((vlan): vlan is string => vlan !== undefined);
188
-
189
- /** 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. */
190
- export type TrustBoundary = {
191
- /** VLANs carrying traffic straight from outside: transits, exchanges, an ISP handoff. */
192
- untrusted: readonly string[];
193
- /** 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. */
194
- routers: readonly string[];
195
- /**
196
- * 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.
197
- */
198
- extended?: readonly (readonly [string, string])[];
199
- };
200
-
201
- /** 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. */
202
- export const trustBoundary =
203
- (boundary: TrustBoundary): Check =>
204
- (devices) => {
205
- const untrusted = new Set(boundary.untrusted);
206
- const routers = new Set(boundary.routers);
207
- const extended = (a: string, b: string) => (boundary.extended ?? []).some(([x, y]) => (x === a && y === b) || (x === b && y === a));
208
- const findings: Finding[] = [];
209
-
210
- for (const device of devices) {
211
- const isRouter = routers.has(device.name);
212
- const ports = Object.entries(device.ports ?? {}).flatMap(([name, port]) => (port ? [{ name, port }] : []));
213
-
214
- // What carries VLANs: a port on its own, or a LAG, which faces whatever its members link to.
215
- const carriers = [
216
- ...ports
217
- .filter(({ port }) => !port.lag)
218
- .map(({ name, port }) => ({
219
- what: `port ${name}`,
220
- carried: vlansOf(port),
221
- faces: port.link ? [port.link.device] : [],
222
- })),
223
- ...Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
224
- iface.type === "lag"
225
- ? [
226
- {
227
- what: `LAG ${name}`,
228
- carried: vlansOf(iface),
229
- faces: ports.filter(({ port }) => port.lag === name && port.link).map(({ port }) => port.link!.device),
230
- },
231
- ]
232
- : []
233
- ),
234
- ];
235
-
236
- for (const { what, carried, faces } of carriers) {
237
- const mixes = carried.some((vlan) => untrusted.has(vlan)) && carried.some((vlan) => !untrusted.has(vlan));
238
- const facesRouter = isRouter || faces.some((name) => routers.has(name));
239
- const isExtension = faces.length > 0 && faces.every((name) => extended(device.name, name));
240
-
241
- if (mixes && !facesRouter && !isExtension) {
242
- findings.push({
243
- level: "error",
244
- device: device.name,
245
- message: `${what} carries an untrusted VLAN beside a trusted one and does not face a router`,
246
- });
247
- }
248
- }
249
-
250
- if (isRouter) continue;
251
-
252
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
253
- if (iface.type === "vlan" && untrusted.has(iface.vlan)) {
254
- findings.push({
255
- level: "error",
256
- device: device.name,
257
- message: `interface ${name} puts an address on untrusted VLAN ${iface.vlan}; only a router may`,
258
- });
259
- }
260
- }
261
- }
262
-
263
- return findings;
264
- };
265
-
266
- /** 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. */
267
- export const tunnelsOutsideOffered: Check = (devices) => {
268
- const findings: Finding[] = [];
269
-
270
- for (const device of devices) {
271
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
272
- if (iface.type !== "gre" && iface.type !== "vxlan") continue;
273
-
274
- const subnets = (iface.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
275
-
276
- for (const neighbor of neighborPolicies(device)) {
277
- if (!subnets.some((subnet) => contains(subnet, neighbor.address))) continue;
278
-
279
- for (const prefix of offered(device, neighbor.export)) {
280
- if (contains(prefix, iface.local)) {
281
- findings.push({
282
- level: "error",
283
- device: device.name,
284
- message: `${name} is sourced from ${iface.local}, inside ${prefix}, which ${neighbor.name} is offered`,
285
- });
286
- }
287
- }
288
- }
289
- }
290
- }
291
-
292
- return findings;
293
- };
294
-
295
- /**
296
- * 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.
297
- */
298
- export type CommunityScheme<Class extends string = string> = {
299
- /** Your AS. It is the first part of every community. */
300
- asn: number;
301
- /** Set on import: the kind of neighbor a route came from, `asn:function:class`. */
302
- learned_from?: { function: number; classes: Readonly<Record<Class, number>> };
303
- /** Set on import: the site a route was learned at, `asn:function:site`. `sites` names the sites, for the catalogue. */
304
- learned_at?: { function: number; sites?: Readonly<Record<number, string>> };
305
- /** Set on import: the AS a route was learned from, `asn:function:asn`. */
306
- learned_from_as?: number;
307
- /** A neighbor asks: do not announce the route to an AS, `asn:function:asn`, or to anyone, `asn:function:0`. */
308
- do_not_announce?: number;
309
- /** A neighbor asks: prepend toward an AS once, twice or three times, `asn:function:asn`. */
310
- prepend?: { once?: number; twice?: number; three_times?: number };
311
- /**
312
- * 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`.
313
- *
314
- * `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.
315
- */
316
- blackhole?: { function: number; upstreams?: Readonly<Record<number, string>> };
317
- };
318
-
319
- const PREPENDS = [
320
- ["once", 1],
321
- ["twice", 2],
322
- ["three_times", 3],
323
- ] as const;
324
-
325
- const article = (word: string) => (/^[aeiou]/i.test(word) ? `an ${word}` : `a ${word}`);
326
-
327
- /**
328
- * 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.
329
- *
330
- * - `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`.
331
- * - `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.
332
- * - `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.
333
- * - `catalogue`: every community, for `communities` in `defineNetwork`.
334
- */
335
- export function communityScheme<const Class extends string>(scheme: CommunityScheme<Class>) {
336
- const { asn } = scheme;
337
- const community = (fn: number, parameter: number | string) => `${asn}:${fn}:${parameter}`;
338
- const ours = { standard: [`${asn}:*`], large: [`${asn}:*:*`] };
339
-
340
- const information = [scheme.learned_from?.function, scheme.learned_at?.function, scheme.learned_from_as].filter(
341
- (fn): fn is number => fn !== undefined
342
- );
343
-
344
- const tag = (
345
- neighborClass: Class,
346
- neighborAs: number,
347
- options: { site?: number; trusted?: boolean } = {}
348
- ): PolicyRule<never, never> => {
349
- const { site, trusted = false } = options;
350
- if (scheme.learned_at && site === undefined)
351
- throw new Error(`communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
352
-
353
- return {
354
- description: `Learned from AS${neighborAs}`,
355
- set: {
356
- remove_communities: ours.standard,
357
- // 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.
358
- remove_large_communities: trusted ? information.map((fn) => community(fn, "*")) : ours.large,
359
- add_large_communities: [
360
- ...(scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : []),
361
- ...(scheme.learned_at ? [community(scheme.learned_at.function, site!)] : []),
362
- ...(scheme.learned_from_as !== undefined ? [community(scheme.learned_from_as, neighborAs)] : []),
363
- ],
364
- },
365
- };
366
- };
367
-
368
- const blackhole = <S extends string>(prefixSets: readonly S[]): PolicyRule<never, S>[] => {
369
- const fn = scheme.blackhole?.function;
370
- if (fn === undefined) throw new Error("communityScheme: blackhole needs a function number in the scheme");
371
-
372
- return prefixSets.map((prefixSet) => ({
373
- description: "Blackhole, asked by the neighbor",
374
- match: { large_community: community(fn, 0), prefix_set: prefixSet },
375
- set: { blackhole: true },
376
- action: "accept",
377
- }));
378
- };
379
-
380
- const actions = (neighborAs: number): PolicyRule<never, never>[] => {
381
- // The upstream's own blackhole community, when it takes blackholes from us.
382
- const passOn = scheme.blackhole?.upstreams?.[neighborAs];
383
-
384
- return [
385
- ...(scheme.blackhole !== undefined && !passOn
386
- ? [
387
- {
388
- description: "Blackholed here, never announced",
389
- match: { large_community: community(scheme.blackhole.function, 0) },
390
- action: "reject" as const,
391
- },
392
- ]
393
- : []),
394
- ...(scheme.do_not_announce !== undefined
395
- ? [
396
- {
397
- description: "Asked not to be announced anywhere",
398
- match: { large_community: community(scheme.do_not_announce, 0) },
399
- action: "reject" as const,
400
- },
401
- {
402
- description: `Asked not to be announced to AS${neighborAs}`,
403
- match: { large_community: community(scheme.do_not_announce, neighborAs) },
404
- action: "reject" as const,
405
- },
406
- ]
407
- : []),
408
- ...(scheme.blackhole !== undefined && passOn
409
- ? [
410
- {
411
- description: `Blackhole passed on to AS${neighborAs}`,
412
- match: { large_community: community(scheme.blackhole.function, 0) },
413
- set: {
414
- remove_communities: ours.standard,
415
- remove_large_communities: ours.large,
416
- add_communities: [passOn],
417
- },
418
- action: "accept" as const,
419
- },
420
- ]
421
- : []),
422
- ...PREPENDS.flatMap(([name, times]) => {
423
- const fn = scheme.prepend?.[name];
424
- if (fn === undefined) return [];
425
-
426
- return [
427
- {
428
- description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
429
- match: { large_community: community(fn, neighborAs) },
430
- set: { prepend: times },
431
- },
432
- ];
433
- }),
434
- {
435
- description: "Our communities stay inside our network",
436
- set: { remove_communities: ours.standard, remove_large_communities: ours.large },
437
- },
438
- ];
439
- };
440
-
441
- const catalogue: Community[] = [
442
- ...Object.entries<number>(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
443
- community: community(scheme.learned_from!.function, value),
444
- description: `Learned from ${article(name)}`,
445
- })),
446
- ...(scheme.learned_at
447
- ? [
448
- ...Object.entries(scheme.learned_at.sites ?? {}).map(([site, name]) => ({
449
- community: community(scheme.learned_at!.function, site),
450
- description: `Learned in ${name}`,
451
- })),
452
- { community: community(scheme.learned_at.function, "nnn"), description: "Learned at site $0" },
453
- ]
454
- : []),
455
- ...(scheme.learned_from_as !== undefined
456
- ? [{ community: community(scheme.learned_from_as, "nnn"), description: "Learned from AS$0" }]
457
- : []),
458
- ...(scheme.do_not_announce !== undefined
459
- ? [
460
- { community: community(scheme.do_not_announce, 0), description: "Do not announce to anyone" },
461
- { community: community(scheme.do_not_announce, "nnn"), description: "Do not announce to AS$0" },
462
- ]
463
- : []),
464
- ...PREPENDS.flatMap(([name]) => {
465
- const fn = scheme.prepend?.[name];
466
- return fn === undefined ? [] : [{ community: community(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
467
- }),
468
- ...(scheme.blackhole !== undefined
469
- ? [
470
- {
471
- community: community(scheme.blackhole.function, 0),
472
- description: Object.keys(scheme.blackhole.upstreams ?? {}).length
473
- ? `Blackhole: dropped in AS${asn} and by its upstreams that take it`
474
- : `Blackhole: dropped in AS${asn}, not announced further`,
475
- },
476
- ]
477
- : []),
478
- ];
479
-
480
- return { community, tag, actions, blackhole, catalogue };
481
- }
482
-
483
- /** A member of an internet exchange: a network with a port on the fabric and an address on the peering LAN. */
484
- export type ExchangeMember = {
485
- /** Lowercase, and never reused. */
486
- name: string;
487
- asn: number;
488
- /** The member's port on each fabric switch, by switch name. */
489
- ports: Readonly<Record<string, string>>;
490
- /** Its addresses on the peering LAN. The route servers peer with these. */
491
- ipv4?: IP;
492
- ipv6?: IP;
493
- };
494
-
495
- /** An internet exchange's peering LAN, as its fabric carries it. */
496
- export type InternetExchange<V extends string> = {
497
- /** The peering VLAN, by the name the fabric switches give it. */
498
- vlan: V;
499
- /** The peering LAN's subnets. */
500
- lan: { ipv4?: Prefix; ipv6?: Prefix };
501
- /** Addresses on the peering LAN no member may take, such as the route servers'. */
502
- reserved?: readonly IP[];
503
- /** How much broadcast, multicast and unknown unicast a member port lets through, in percent of its speed. 1 when absent. */
504
- storm_percent?: number;
505
- };
506
-
507
- /**
508
- * Member ports and a check for an internet exchange's fabric.
509
- *
510
- * - `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.
511
- * - `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.
512
- */
513
- export function internetExchange<const V extends string>(exchange: InternetExchange<V>, members: readonly ExchangeMember[]) {
514
- const percent = exchange.storm_percent ?? 1;
515
-
516
- const ports = (device: string) => {
517
- const result: Record<string, Port<V, string, string, string>> = {};
518
-
519
- for (const member of members) {
520
- const port = member.ports[device];
521
- if (!port) continue;
522
-
523
- result[port] = {
524
- description: `${member.name}, AS${member.asn}`,
525
- access_vlan: exchange.vlan,
526
- stp: false,
527
- lldp: false,
528
- storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
529
- };
530
- }
531
-
532
- return result;
533
- };
534
-
535
- const check: Check = (devices) => {
536
- const findings: Finding[] = [];
537
- const error = (message: string, device?: string) => findings.push({ level: "error", ...(device ? { device } : {}), message });
538
- const taken = new Map<string, string>((exchange.reserved ?? []).map((address) => [address, "reserved"]));
539
- // `ports()` writes one port per name, so a second member on a port would silently replace the first.
540
- const portHolders = new Map<string, string>();
541
-
542
- members.forEach((member, index) => {
543
- for (const other of members.slice(index + 1)) {
544
- if (other.name === member.name) error(`member ${member.name} is declared twice`);
545
- if (other.asn === member.asn) error(`members ${member.name} and ${other.name} share AS${member.asn}`);
546
- }
547
-
548
- for (const [address, lan] of [
549
- [member.ipv4, exchange.lan.ipv4],
550
- [member.ipv6, exchange.lan.ipv6],
551
- ] as const) {
552
- if (!address) continue;
553
- if (!lan || !contains(lan, address)) error(`${address} of ${member.name} is outside the peering LAN`);
554
-
555
- const holder = taken.get(address);
556
- if (holder) error(`${address} of ${member.name} is already ${holder === "reserved" ? "reserved" : `${holder}'s`}`);
557
- taken.set(address, member.name);
558
- }
559
-
560
- for (const [deviceName, portName] of Object.entries(member.ports)) {
561
- const holder = portHolders.get(`${deviceName} ${portName}`);
562
- if (holder) error(`port ${portName} is given to both ${holder} and ${member.name}`, deviceName);
563
- portHolders.set(`${deviceName} ${portName}`, member.name);
564
-
565
- const device = devices.find((candidate) => candidate.name === deviceName);
566
- const port = device?.ports?.[portName as keyof typeof device.ports];
567
- const carried = port ? vlansOf(port) : [];
568
-
569
- if (carried.length !== 1 || carried[0] !== exchange.vlan) {
570
- error(`port ${portName} of ${member.name} does not carry the peering VLAN alone`, deviceName);
571
- }
572
- }
573
- });
574
-
575
- return findings;
576
- };
577
-
578
- return { ports, check };
579
- }