@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.
- package/README.md +13 -14
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11345 -9024
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +17 -11
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
- package/src/adapters/devices/raisecom-ros/index.ts +0 -700
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
package/src/presets/index.ts
DELETED
|
@@ -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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|