@takodotid/circuit 0.43.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -37
- package/package.json +21 -58
- package/src/checks/across.ts +402 -0
- package/src/checks/built-in.ts +256 -0
- package/src/checks/device.ts +436 -0
- package/src/checks/index.ts +53 -0
- package/src/cli/advisories.ts +108 -0
- package/src/cli/app.ts +71 -0
- package/src/cli/editor-types.generated.ts +30 -0
- package/src/cli/editor-types.ts +69 -0
- package/src/cli/format.ts +124 -0
- package/src/cli/git-hooks.ts +79 -0
- package/src/cli/index.ts +350 -1170
- package/src/cli/migrate.ts +554 -0
- package/src/cli/project-commands.ts +656 -0
- package/src/cli/releases.ts +374 -0
- package/src/cli/remember.ts +339 -0
- package/src/cli/rename.ts +27 -33
- package/src/cli/secrets-password.ts +60 -0
- package/src/cli/secrets.ts +258 -0
- package/src/cli/setup.ts +300 -0
- package/src/cli/signing.ts +23 -0
- package/src/cli/text-editor.ts +268 -0
- package/src/cli/ui.ts +298 -0
- package/src/cli/version.ts +2 -0
- package/src/cli/versions.ts +158 -0
- package/src/config/address.ts +102 -0
- package/src/config/circuit.d.ts +30 -0
- package/src/config/common.ts +65 -0
- package/src/config/communities.ts +479 -0
- package/src/config/define.ts +684 -0
- package/src/config/device.ts +164 -0
- package/src/config/firewall.ts +150 -0
- package/src/config/index.ts +22 -0
- package/src/{schema → config}/interfaces.ts +121 -85
- package/src/config/keys.ts +434 -0
- package/src/config/management.ts +61 -0
- package/src/config/merge.ts +335 -0
- package/src/config/models.ts +70 -0
- package/src/config/monitoring.ts +64 -0
- package/src/config/network.ts +180 -0
- package/src/config/presets.ts +149 -0
- package/src/config/routing.ts +401 -0
- package/src/config/services.ts +51 -0
- package/src/config/switching.ts +84 -0
- package/src/config/system.ts +103 -0
- package/src/{schema → config}/time-zones.ts +1 -1
- package/src/core/address.ts +1 -1
- package/src/core/cabling.ts +8 -43
- package/src/core/graph.ts +40 -0
- package/src/{integrations → core}/registries.ts +5 -3
- package/src/core/secrets-file.ts +539 -0
- package/src/core/secrets.ts +83 -64
- package/src/core/topology.ts +103 -146
- package/src/core/utils.ts +12 -1
- package/src/model.ts +83 -0
- package/src/platforms/derive.ts +64 -37
- package/src/platforms/finding.ts +9 -0
- package/src/platforms/fortios/firewall.ts +76 -64
- package/src/platforms/fortios/index.ts +189 -125
- package/src/platforms/fortios/names.ts +51 -18
- package/src/platforms/fortios/render.ts +69 -62
- package/src/platforms/fortios/routing.ts +89 -58
- package/src/platforms/fortios/vdoms.ts +47 -16
- package/src/platforms/junos/TRAPS.md +2 -2
- package/src/platforms/junos/bgp.ts +63 -42
- package/src/platforms/junos/index.ts +122 -89
- package/src/platforms/junos/render.ts +139 -104
- package/src/platforms/platform.ts +10 -8
- package/src/platforms/raisecom-ros/TRAPS.md +1 -1
- package/src/platforms/raisecom-ros/index.ts +89 -70
- package/src/platforms/raisecom-ros/render.ts +49 -45
- package/src/platforms/routeros/TRAPS.md +3 -3
- package/src/platforms/routeros/index.ts +121 -60
- package/src/platforms/routeros/render.ts +214 -195
- package/src/platforms/vrp/TRAPS.md +1 -1
- package/src/platforms/vrp/index.ts +74 -50
- package/src/platforms/vrp/render.ts +67 -56
- package/src/project/project.ts +725 -0
- package/src/studio/app/env.d.ts +9 -0
- package/src/studio/app/index.html +18 -0
- package/src/studio/app/src/App.vue +95 -0
- package/src/studio/app/src/components/AddressesView.vue +38 -0
- package/src/studio/app/src/components/CablesView.vue +29 -0
- package/src/studio/app/src/components/CompareView.vue +160 -0
- package/src/studio/app/src/components/DataTable.vue +49 -0
- package/src/studio/app/src/components/DeviceView.vue +237 -0
- package/src/studio/app/src/components/DevicesView.vue +32 -0
- package/src/studio/app/src/components/Faceplate.vue +75 -0
- package/src/studio/app/src/components/GraphView.vue +282 -0
- package/src/studio/app/src/components/LineDiff.vue +24 -0
- package/src/studio/app/src/components/ProblemsView.vue +42 -0
- package/src/studio/app/src/components/SearchView.vue +72 -0
- package/src/studio/app/src/components/SecretsView.vue +112 -0
- package/src/studio/app/src/components/SessionsView.vue +35 -0
- package/src/studio/app/src/components/Settings.vue +95 -0
- package/src/studio/app/src/components/ToolsView.vue +118 -0
- package/src/studio/app/src/components/VlansView.vue +50 -0
- package/src/studio/app/src/lib/csv.ts +19 -0
- package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
- package/src/studio/app/src/lib/format.ts +38 -0
- package/src/studio/app/src/lib/net.ts +68 -0
- package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
- package/src/studio/app/src/lib/store.ts +71 -0
- package/src/studio/app/src/lib/tools.ts +217 -0
- package/src/studio/app/src/main.ts +9 -0
- package/src/studio/{public → app/src}/style.css +14 -4
- package/src/studio/app/tsconfig.json +17 -0
- package/src/studio/app/vite.config.ts +16 -0
- package/src/studio/assets.generated.ts +3 -0
- package/src/studio/data.ts +96 -188
- package/src/studio/git.ts +27 -43
- package/src/studio/server.ts +196 -107
- package/src/studio/types.ts +141 -0
- package/src/transport/console.ts +3 -3
- package/src/transport/ssh.ts +1 -1
- package/dist/chunks/index-3b343xva.js +0 -177
- package/dist/chunks/index-6wb41q67.js +0 -62
- package/dist/chunks/index-n7w3tg5y.js +0 -149
- package/dist/chunks/index-yzw86rc8.js +0 -921
- package/dist/chunks/new-w3zys5y4.js +0 -172
- package/dist/chunks/server-cxr9ynxc.js +0 -259
- package/dist/cli/index.js +0 -12922
- package/dist/helpers/index.js +0 -881
- package/dist/index.js +0 -28
- package/dist/types/core/address-space.d.ts +0 -62
- package/dist/types/core/address.d.ts +0 -44
- package/dist/types/core/cabling.d.ts +0 -41
- package/dist/types/core/cross-checks.d.ts +0 -28
- package/dist/types/core/define.d.ts +0 -79
- package/dist/types/core/project-files.d.ts +0 -12
- package/dist/types/core/schema-docs.d.ts +0 -25
- package/dist/types/core/secrets.d.ts +0 -62
- package/dist/types/core/topology.d.ts +0 -96
- package/dist/types/core/utils.d.ts +0 -10
- package/dist/types/core/validator.d.ts +0 -23
- package/dist/types/helpers/bgp.d.ts +0 -195
- package/dist/types/helpers/cables.d.ts +0 -69
- package/dist/types/helpers/checks.d.ts +0 -36
- package/dist/types/helpers/exchange.d.ts +0 -55
- package/dist/types/helpers/filters.d.ts +0 -51
- package/dist/types/helpers/index.d.ts +0 -14
- package/dist/types/helpers/route-server.d.ts +0 -52
- package/dist/types/helpers/upstreams.d.ts +0 -192
- package/dist/types/index.d.ts +0 -10
- package/dist/types/integrations/librenms.d.ts +0 -60
- package/dist/types/integrations/netbox.d.ts +0 -89
- package/dist/types/integrations/peeringdb.d.ts +0 -24
- package/dist/types/integrations/prometheus.d.ts +0 -24
- package/dist/types/integrations/registries.d.ts +0 -8
- package/dist/types/platforms/bgp-wait.d.ts +0 -24
- package/dist/types/platforms/catalog.d.ts +0 -109
- package/dist/types/platforms/derive.d.ts +0 -17
- package/dist/types/platforms/fortios/firewall.d.ts +0 -47
- package/dist/types/platforms/fortios/index.d.ts +0 -140
- package/dist/types/platforms/fortios/models.d.ts +0 -14
- package/dist/types/platforms/fortios/names.d.ts +0 -52
- package/dist/types/platforms/fortios/plan.d.ts +0 -114
- package/dist/types/platforms/fortios/render.d.ts +0 -77
- package/dist/types/platforms/fortios/routing.d.ts +0 -56
- package/dist/types/platforms/fortios/tree.d.ts +0 -37
- package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
- package/dist/types/platforms/index.d.ts +0 -13
- package/dist/types/platforms/junos/bgp.d.ts +0 -44
- package/dist/types/platforms/junos/index.d.ts +0 -114
- package/dist/types/platforms/junos/models.d.ts +0 -14
- package/dist/types/platforms/junos/render.d.ts +0 -154
- package/dist/types/platforms/line-diff.d.ts +0 -67
- package/dist/types/platforms/platform.d.ts +0 -135
- package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
- package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
- package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
- package/dist/types/platforms/routeros/export.d.ts +0 -59
- package/dist/types/platforms/routeros/index.d.ts +0 -116
- package/dist/types/platforms/routeros/models.d.ts +0 -18
- package/dist/types/platforms/routeros/plan.d.ts +0 -58
- package/dist/types/platforms/routeros/render.d.ts +0 -250
- package/dist/types/platforms/vrp/index.d.ts +0 -83
- package/dist/types/platforms/vrp/models.d.ts +0 -12
- package/dist/types/platforms/vrp/render.d.ts +0 -96
- package/dist/types/schema/common.d.ts +0 -30
- package/dist/types/schema/device.d.ts +0 -126
- package/dist/types/schema/firewall.d.ts +0 -131
- package/dist/types/schema/index.d.ts +0 -8
- package/dist/types/schema/interfaces.d.ts +0 -205
- package/dist/types/schema/policy.d.ts +0 -107
- package/dist/types/schema/routing.d.ts +0 -161
- package/dist/types/schema/system.d.ts +0 -198
- package/dist/types/schema/time-zones.d.ts +0 -2
- package/dist/types/transport/console.d.ts +0 -115
- package/dist/types/transport/path-check.d.ts +0 -32
- package/dist/types/transport/ssh.d.ts +0 -27
- package/src/cli/graph-format.ts +0 -91
- package/src/cli/new.ts +0 -234
- package/src/core/address-space.ts +0 -320
- package/src/core/cross-checks.ts +0 -369
- package/src/core/define.ts +0 -124
- package/src/core/project-files.ts +0 -35
- package/src/core/schema-docs.ts +0 -174
- package/src/core/validator.ts +0 -508
- package/src/helpers/bgp.ts +0 -473
- package/src/helpers/cables.ts +0 -74
- package/src/helpers/checks.ts +0 -201
- package/src/helpers/exchange.ts +0 -123
- package/src/helpers/filters.ts +0 -107
- package/src/helpers/index.ts +0 -25
- package/src/helpers/route-server.ts +0 -212
- package/src/helpers/upstreams.ts +0 -369
- package/src/index.ts +0 -13
- package/src/integrations/librenms.ts +0 -291
- package/src/integrations/netbox.ts +0 -620
- package/src/integrations/peeringdb.ts +0 -173
- package/src/integrations/prometheus.ts +0 -90
- package/src/schema/common.ts +0 -41
- package/src/schema/device.ts +0 -145
- package/src/schema/firewall.ts +0 -140
- package/src/schema/index.ts +0 -8
- package/src/schema/policy.ts +0 -115
- package/src/schema/routing.ts +0 -173
- package/src/schema/system.ts +0 -203
- package/src/studio/public/app.js +0 -186
- package/src/studio/public/compare.js +0 -250
- package/src/studio/public/dom.js +0 -138
- package/src/studio/public/evaluate.js +0 -687
- package/src/studio/public/export.js +0 -130
- package/src/studio/public/graph.js +0 -1541
- package/src/studio/public/index.html +0 -83
- package/src/studio/public/net.js +0 -134
- package/src/studio/public/tables.js +0 -1289
- package/src/studio/public/tools.js +0 -610
- package/templates/colocation/checks.ts +0 -53
- package/templates/colocation/circuit.config.ts +0 -9
- package/templates/colocation/edge-01.ts +0 -133
- package/templates/colocation/routing.ts +0 -13
- package/templates/colocation/tenancy.ts +0 -76
- package/templates/colocation/tenants/acme.ts +0 -19
- package/templates/colocation/tenants/index.ts +0 -6
- package/templates/colocation/tor-01.ts +0 -33
- package/templates/edge-router/checks.ts +0 -20
- package/templates/edge-router/circuit.config.ts +0 -8
- package/templates/edge-router/edge-01.ts +0 -110
- package/templates/edge-router/routing.ts +0 -13
- package/templates/project/AGENTS.md +0 -51
- package/templates/project/CLAUDE.md +0 -1
- package/templates/project/README.md +0 -16
- package/templates/project/gitignore +0 -5
- package/templates/project/tsconfig.json +0 -13
- package/templates/single-site/checks.ts +0 -26
- package/templates/single-site/circuit.config.ts +0 -8
- package/templates/single-site/router.ts +0 -86
- package/templates/single-site/site.ts +0 -18
- package/templates/single-site/switch.ts +0 -34
- /package/src/studio/{public → app/public}/favicon.svg +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
package/dist/helpers/index.js
DELETED
|
@@ -1,881 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Catalog
|
|
3
|
-
} from "../chunks/index-6wb41q67.js";
|
|
4
|
-
import {
|
|
5
|
-
Address2
|
|
6
|
-
} from "../chunks/index-n7w3tg5y.js";
|
|
7
|
-
|
|
8
|
-
// src/helpers/bgp.ts
|
|
9
|
-
class Bgp {
|
|
10
|
-
static PREPENDS = [
|
|
11
|
-
["once", 1],
|
|
12
|
-
["twice", 2],
|
|
13
|
-
["three_times", 3]
|
|
14
|
-
];
|
|
15
|
-
static DEFAULTS = [
|
|
16
|
-
{ family: "ipv4", prefix: "0.0.0.0/0" },
|
|
17
|
-
{ family: "ipv6", prefix: "::/0" }
|
|
18
|
-
];
|
|
19
|
-
static sanity(lengths = {}) {
|
|
20
|
-
const ipv4 = lengths.ipv4 ?? { min: 8, max: 24 };
|
|
21
|
-
const ipv6 = lengths.ipv6 ?? { min: 16, max: 48 };
|
|
22
|
-
return [
|
|
23
|
-
{
|
|
24
|
-
description: `IPv4 shorter than /${ipv4.min}`,
|
|
25
|
-
match: { family: "ipv4", prefix_length: { max: ipv4.min - 1 } },
|
|
26
|
-
action: "reject"
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
description: `IPv4 longer than /${ipv4.max}`,
|
|
30
|
-
match: { family: "ipv4", prefix_length: { min: ipv4.max + 1 } },
|
|
31
|
-
action: "reject"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
description: `IPv6 shorter than /${ipv6.min}`,
|
|
35
|
-
match: { family: "ipv6", prefix_length: { max: ipv6.min - 1 } },
|
|
36
|
-
action: "reject"
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
description: `IPv6 longer than /${ipv6.max}`,
|
|
40
|
-
match: { family: "ipv6", prefix_length: { min: ipv6.max + 1 } },
|
|
41
|
-
action: "reject"
|
|
42
|
-
},
|
|
43
|
-
{ description: "RPKI invalid", match: { rpki: "invalid" }, action: "reject" }
|
|
44
|
-
];
|
|
45
|
-
}
|
|
46
|
-
static announce(prefixes, options = {}) {
|
|
47
|
-
return [
|
|
48
|
-
...prefixes.map(({ prefix, description }) => ({
|
|
49
|
-
...description ? { description } : {},
|
|
50
|
-
match: { prefix },
|
|
51
|
-
action: "accept"
|
|
52
|
-
})),
|
|
53
|
-
...options.reject === false ? [] : [Bgp.NOTHING_ELSE]
|
|
54
|
-
];
|
|
55
|
-
}
|
|
56
|
-
static NOTHING_ELSE = { description: "Nothing else", action: "reject" };
|
|
57
|
-
static learn(options = {}) {
|
|
58
|
-
const { tag } = options;
|
|
59
|
-
return [
|
|
60
|
-
...options.before ?? [],
|
|
61
|
-
...options.sanity ? [{ call: options.sanity }] : Bgp.sanity(),
|
|
62
|
-
...(options.sources ?? []).map((source) => ({ set: { preferred_source: source } })),
|
|
63
|
-
...options.local_pref !== undefined ? [{ set: { local_pref: options.local_pref } }] : [],
|
|
64
|
-
...tag === undefined ? [] : ["standard" in tag ? Bgp.adding(tag.standard) : ("large" in tag) ? Bgp.adding(tag.large) : tag],
|
|
65
|
-
{ action: "accept" }
|
|
66
|
-
];
|
|
67
|
-
}
|
|
68
|
-
static defaultOnly(options = {}) {
|
|
69
|
-
const defaults = Bgp.DEFAULTS.map(({ family, prefix }) => {
|
|
70
|
-
const source = options.sources?.find((address) => Address2.family(address) === family);
|
|
71
|
-
const set = {
|
|
72
|
-
...options.local_pref !== undefined ? { local_pref: options.local_pref } : {},
|
|
73
|
-
...source ? { preferred_source: source } : {}
|
|
74
|
-
};
|
|
75
|
-
return {
|
|
76
|
-
description: options.description ?? `Default route, ${family}`,
|
|
77
|
-
match: { prefix },
|
|
78
|
-
...Object.keys(set).length ? { set } : {},
|
|
79
|
-
action: "accept"
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
return options.reject ? [...defaults, Bgp.NOTHING_ELSE] : defaults;
|
|
83
|
-
}
|
|
84
|
-
static adding(community) {
|
|
85
|
-
const large = community.split(":").length === 3;
|
|
86
|
-
return {
|
|
87
|
-
description: `Tag ${community}`,
|
|
88
|
-
set: large ? { add_large_communities: [community] } : { add_communities: [community] }
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
static neighborPolicies(device) {
|
|
92
|
-
const bgp = device.routing?.bgp;
|
|
93
|
-
return Object.entries(bgp?.neighbors ?? {}).map(([name, neighbor]) => {
|
|
94
|
-
const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
|
|
95
|
-
return {
|
|
96
|
-
name,
|
|
97
|
-
address: neighbor.address,
|
|
98
|
-
role: neighbor.role ?? group?.role,
|
|
99
|
-
import: neighbor.import ?? group?.import,
|
|
100
|
-
export: neighbor.export ?? group?.export
|
|
101
|
-
};
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
static offered(device, policy) {
|
|
105
|
-
const rules = policy ? device.policies?.[policy] ?? [] : [];
|
|
106
|
-
return rules.filter((rule) => rule.action === "accept" && rule.match?.prefix).map((rule) => rule.match.prefix);
|
|
107
|
-
}
|
|
108
|
-
static communityScheme(scheme) {
|
|
109
|
-
const { asn } = scheme;
|
|
110
|
-
const community = (fn, parameter) => `${asn}:${fn}:${parameter}`;
|
|
111
|
-
const anyOf = (fn) => `${asn}:${fn}:*`;
|
|
112
|
-
const written = (fn, parameter) => `${asn}:${fn}:${parameter}`;
|
|
113
|
-
const ours = {
|
|
114
|
-
standard: [`${asn}:*`],
|
|
115
|
-
large: [`${asn}:*:*`]
|
|
116
|
-
};
|
|
117
|
-
const learnedFrom = Object.fromEntries(Object.entries(scheme.learned_from?.classes ?? {}).map(([name, value]) => [
|
|
118
|
-
name,
|
|
119
|
-
community(scheme.learned_from.function, value)
|
|
120
|
-
]));
|
|
121
|
-
const information = [scheme.learned_from?.function, scheme.learned_at?.function, scheme.learned_from_as].filter((fn) => fn !== undefined);
|
|
122
|
-
const tag = (neighborClass, neighborAs, options = {}) => {
|
|
123
|
-
const { site, trusted = false } = options;
|
|
124
|
-
if (scheme.learned_at && site === undefined)
|
|
125
|
-
throw new Error(`Bgp.communityScheme: tag for AS${neighborAs} needs a site, for learned_at`);
|
|
126
|
-
return {
|
|
127
|
-
description: `Learned from AS${neighborAs}`,
|
|
128
|
-
set: {
|
|
129
|
-
remove_communities: ours.standard,
|
|
130
|
-
remove_large_communities: trusted ? information.map(anyOf) : ours.large,
|
|
131
|
-
add_large_communities: [
|
|
132
|
-
...scheme.learned_from ? [community(scheme.learned_from.function, scheme.learned_from.classes[neighborClass])] : [],
|
|
133
|
-
...scheme.learned_at ? [community(scheme.learned_at.function, site)] : [],
|
|
134
|
-
...scheme.learned_from_as !== undefined ? [community(scheme.learned_from_as, neighborAs)] : []
|
|
135
|
-
]
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
};
|
|
139
|
-
const blackhole = (prefixSets) => {
|
|
140
|
-
const fn = scheme.blackhole?.function;
|
|
141
|
-
if (fn === undefined)
|
|
142
|
-
throw new Error("Bgp.communityScheme: blackhole needs a function number in the scheme");
|
|
143
|
-
return prefixSets.map((prefixSet) => ({
|
|
144
|
-
description: "Blackhole, asked by the neighbor",
|
|
145
|
-
match: { large_community: community(fn, 0), prefix_set: prefixSet },
|
|
146
|
-
set: { blackhole: true },
|
|
147
|
-
action: "accept"
|
|
148
|
-
}));
|
|
149
|
-
};
|
|
150
|
-
const asnOf = (neighbor) => typeof neighbor === "number" ? neighbor : neighbor.asn;
|
|
151
|
-
const passOnTo = (neighbor) => typeof neighbor === "number" ? undefined : neighbor.communities?.blackhole;
|
|
152
|
-
const blackholeExport = (neighbor) => {
|
|
153
|
-
if (scheme.blackhole === undefined)
|
|
154
|
-
return [];
|
|
155
|
-
const passOn = passOnTo(neighbor);
|
|
156
|
-
const match = { large_community: community(scheme.blackhole.function, 0) };
|
|
157
|
-
if (!passOn)
|
|
158
|
-
return [{ description: "Blackholed here, never announced", match, action: "reject" }];
|
|
159
|
-
const large = passOn.split(":").length === 3;
|
|
160
|
-
return [
|
|
161
|
-
{
|
|
162
|
-
description: `Blackhole passed on to AS${asnOf(neighbor)}`,
|
|
163
|
-
match,
|
|
164
|
-
set: {
|
|
165
|
-
remove_communities: ours.standard,
|
|
166
|
-
remove_large_communities: ours.large,
|
|
167
|
-
...large ? { add_large_communities: [passOn] } : { add_communities: [passOn] }
|
|
168
|
-
},
|
|
169
|
-
action: "accept"
|
|
170
|
-
}
|
|
171
|
-
];
|
|
172
|
-
};
|
|
173
|
-
const rejectDoNotAnnounce = (neighbor) => {
|
|
174
|
-
const neighborAs = asnOf(neighbor);
|
|
175
|
-
const fn = scheme.do_not_announce;
|
|
176
|
-
if (fn === undefined)
|
|
177
|
-
return [];
|
|
178
|
-
return [
|
|
179
|
-
{ description: "Asked not to be announced anywhere", match: { large_community: community(fn, 0) }, action: "reject" },
|
|
180
|
-
{
|
|
181
|
-
description: `Asked not to be announced to AS${neighborAs}`,
|
|
182
|
-
match: { large_community: community(fn, neighborAs) },
|
|
183
|
-
action: "reject"
|
|
184
|
-
}
|
|
185
|
-
];
|
|
186
|
-
};
|
|
187
|
-
const prepends = (neighbor) => Bgp.PREPENDS.flatMap(([name, times]) => {
|
|
188
|
-
const neighborAs = asnOf(neighbor);
|
|
189
|
-
const fn = scheme.prepend?.[name];
|
|
190
|
-
if (fn === undefined)
|
|
191
|
-
return [];
|
|
192
|
-
return [
|
|
193
|
-
{
|
|
194
|
-
description: `Asked to prepend ${name.replace("_", " ")} to AS${neighborAs}`,
|
|
195
|
-
match: { large_community: community(fn, neighborAs) },
|
|
196
|
-
set: { prepend: times }
|
|
197
|
-
}
|
|
198
|
-
];
|
|
199
|
-
});
|
|
200
|
-
const stripOurs = () => ({
|
|
201
|
-
description: "Our communities stay inside our network",
|
|
202
|
-
set: { remove_communities: ours.standard, remove_large_communities: ours.large }
|
|
203
|
-
});
|
|
204
|
-
const actions = (neighbor) => passOnTo(neighbor) ? [...rejectDoNotAnnounce(neighbor), ...blackholeExport(neighbor), ...prepends(neighbor), stripOurs()] : [...blackholeExport(neighbor), ...rejectDoNotAnnounce(neighbor), ...prepends(neighbor), stripOurs()];
|
|
205
|
-
const catalogue = [
|
|
206
|
-
...Object.entries(scheme.learned_from?.classes ?? {}).map(([name, value]) => ({
|
|
207
|
-
community: written(scheme.learned_from.function, value),
|
|
208
|
-
description: `Learned from ${Bgp.article(name)}`
|
|
209
|
-
})),
|
|
210
|
-
...scheme.learned_at ? [
|
|
211
|
-
...Object.entries(scheme.learned_at.sites ?? {}).map(([site, name]) => ({
|
|
212
|
-
community: written(scheme.learned_at.function, site),
|
|
213
|
-
description: `Learned in ${name}`
|
|
214
|
-
})),
|
|
215
|
-
{ community: written(scheme.learned_at.function, "nnn"), description: "Learned at site $0" }
|
|
216
|
-
] : [],
|
|
217
|
-
...scheme.learned_from_as !== undefined ? [{ community: written(scheme.learned_from_as, "nnn"), description: "Learned from AS$0" }] : [],
|
|
218
|
-
...scheme.do_not_announce !== undefined ? [
|
|
219
|
-
{ community: written(scheme.do_not_announce, 0), description: "Do not announce to anyone" },
|
|
220
|
-
{ community: written(scheme.do_not_announce, "nnn"), description: "Do not announce to AS$0" }
|
|
221
|
-
] : [],
|
|
222
|
-
...Bgp.PREPENDS.flatMap(([name]) => {
|
|
223
|
-
const fn = scheme.prepend?.[name];
|
|
224
|
-
return fn === undefined ? [] : [{ community: written(fn, "nnn"), description: `Prepend ${name.replace("_", " ")} to AS$0` }];
|
|
225
|
-
}),
|
|
226
|
-
...scheme.blackhole !== undefined ? [
|
|
227
|
-
{
|
|
228
|
-
community: written(scheme.blackhole.function, 0),
|
|
229
|
-
description: `Blackhole: dropped in AS${asn}, and by the upstreams that take it`
|
|
230
|
-
}
|
|
231
|
-
] : []
|
|
232
|
-
];
|
|
233
|
-
return {
|
|
234
|
-
community,
|
|
235
|
-
learned_from: learnedFrom,
|
|
236
|
-
tag,
|
|
237
|
-
actions,
|
|
238
|
-
blackholeExport,
|
|
239
|
-
rejectDoNotAnnounce,
|
|
240
|
-
prepends,
|
|
241
|
-
stripOurs,
|
|
242
|
-
blackhole,
|
|
243
|
-
catalogue
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
static article(word) {
|
|
247
|
-
return /^[aeiou]/i.test(word) ? `an ${word}` : `a ${word}`;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
// src/helpers/cables.ts
|
|
251
|
-
class Cables {
|
|
252
|
-
static link(spec) {
|
|
253
|
-
return {
|
|
254
|
-
a: { [spec.a.port]: Cables.end(spec, spec.b, spec.b.port) },
|
|
255
|
-
b: { [spec.b.port]: Cables.end(spec, spec.a, spec.a.port) }
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
static lag(spec) {
|
|
259
|
-
const pairs = Object.entries(spec.cables);
|
|
260
|
-
const taken = new Map;
|
|
261
|
-
for (const [near, far] of pairs) {
|
|
262
|
-
const other = taken.get(far);
|
|
263
|
-
if (other)
|
|
264
|
-
throw new Error(`Cables.lag: ${spec.b.device} ${far} is cabled to both ${other} and ${near} on ${spec.a.device}`);
|
|
265
|
-
taken.set(far, near);
|
|
266
|
-
}
|
|
267
|
-
const a = Object.fromEntries(pairs.map(([near, far]) => [near, { ...Cables.end(spec, spec.b, far), lag: spec.a.lag }]));
|
|
268
|
-
const b = Object.fromEntries(pairs.map(([near, far]) => [far, { ...Cables.end(spec, spec.a, near), lag: spec.b.lag }]));
|
|
269
|
-
return {
|
|
270
|
-
a,
|
|
271
|
-
b
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
static end(shared, far, port) {
|
|
275
|
-
const label = far.platform && far.model ? Catalog.spell(far.platform, far.model, port) : undefined;
|
|
276
|
-
return {
|
|
277
|
-
link: { device: far.device, port, ...shared.through ? { through: shared.through } : {} },
|
|
278
|
-
...shared.describe ? { description: shared.describe({ device: far.device, port, ...label ? { label } : {} }) } : {}
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
// src/helpers/checks.ts
|
|
283
|
-
class Checks {
|
|
284
|
-
static exportsEndInReject = (devices) => {
|
|
285
|
-
const findings = [];
|
|
286
|
-
for (const device of devices) {
|
|
287
|
-
for (const neighbor of Bgp.neighborPolicies(device)) {
|
|
288
|
-
if (neighbor.role === "customer")
|
|
289
|
-
continue;
|
|
290
|
-
const last = neighbor.export ? device.policies?.[neighbor.export]?.at(-1) : undefined;
|
|
291
|
-
if (neighbor.export && (last?.action !== "reject" || last.match)) {
|
|
292
|
-
findings.push({
|
|
293
|
-
level: "error",
|
|
294
|
-
device: device.name,
|
|
295
|
-
message: `${neighbor.name}: export ${neighbor.export} does not end in an unconditional reject`
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return findings;
|
|
301
|
-
};
|
|
302
|
-
static importsEndInAcceptOrReject = (devices) => {
|
|
303
|
-
const findings = [];
|
|
304
|
-
for (const device of devices) {
|
|
305
|
-
for (const neighbor of Bgp.neighborPolicies(device)) {
|
|
306
|
-
const last = neighbor.import ? device.policies?.[neighbor.import]?.at(-1) : undefined;
|
|
307
|
-
if (neighbor.import && (!last?.action || last.match)) {
|
|
308
|
-
findings.push({
|
|
309
|
-
level: "error",
|
|
310
|
-
device: device.name,
|
|
311
|
-
message: `${neighbor.name}: import ${neighbor.import} does not end in an unconditional accept or reject`
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
return findings;
|
|
317
|
-
};
|
|
318
|
-
static tunnelsOutsideOffered = (devices) => {
|
|
319
|
-
const findings = [];
|
|
320
|
-
for (const device of devices) {
|
|
321
|
-
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
322
|
-
if (iface.type !== "gre" && iface.type !== "vxlan")
|
|
323
|
-
continue;
|
|
324
|
-
const subnets = (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
|
|
325
|
-
for (const neighbor of Bgp.neighborPolicies(device)) {
|
|
326
|
-
if (!subnets.some((subnet) => Address2.contains(subnet, neighbor.address)))
|
|
327
|
-
continue;
|
|
328
|
-
for (const prefix of Bgp.offered(device, neighbor.export)) {
|
|
329
|
-
if (Address2.contains(prefix, iface.local)) {
|
|
330
|
-
findings.push({
|
|
331
|
-
level: "error",
|
|
332
|
-
device: device.name,
|
|
333
|
-
message: `${name} is sourced from ${iface.local}, inside ${prefix}, which ${neighbor.name} is offered`
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
return findings;
|
|
341
|
-
};
|
|
342
|
-
static nativeVlansAreDeadEnds = (devices) => {
|
|
343
|
-
const findings = [];
|
|
344
|
-
for (const device of devices) {
|
|
345
|
-
const carriers = [
|
|
346
|
-
...Object.entries(device.ports ?? {}).flatMap(([name, port]) => port ? [{ what: `port ${name}`, settings: port }] : []),
|
|
347
|
-
...Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) => iface.type === "lag" ? [{ what: `LAG ${name}`, settings: iface }] : [])
|
|
348
|
-
];
|
|
349
|
-
const nativeOn = new Map;
|
|
350
|
-
for (const { what, settings } of carriers) {
|
|
351
|
-
if (settings.native_vlan)
|
|
352
|
-
nativeOn.set(settings.native_vlan, [...nativeOn.get(settings.native_vlan) ?? [], what]);
|
|
353
|
-
}
|
|
354
|
-
for (const [vlan, trunks] of nativeOn) {
|
|
355
|
-
const report = (message) => findings.push({ level: "error", device: device.name, message: `native VLAN ${vlan} ${message}` });
|
|
356
|
-
if (trunks.length > 1)
|
|
357
|
-
report(`is native on ${trunks.join(", ")}: give each trunk its own`);
|
|
358
|
-
const members = carriers.filter(({ settings }) => Checks.vlansOf(settings).includes(vlan));
|
|
359
|
-
for (const { what } of members)
|
|
360
|
-
report(`is also an access or tagged VLAN on ${what}`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return findings;
|
|
364
|
-
};
|
|
365
|
-
static vlansOf(port) {
|
|
366
|
-
return [port.access_vlan, ...port.trunk_vlans ?? []].filter((vlan) => vlan !== undefined);
|
|
367
|
-
}
|
|
368
|
-
static trustBoundary(boundary) {
|
|
369
|
-
return (devices) => {
|
|
370
|
-
const untrusted = new Set(boundary.untrusted);
|
|
371
|
-
const routers = new Set(boundary.routers);
|
|
372
|
-
const extended = (a, b) => (boundary.extended ?? []).some(([x, y]) => x === a && y === b || x === b && y === a);
|
|
373
|
-
const findings = [];
|
|
374
|
-
for (const device of devices) {
|
|
375
|
-
const isRouter = routers.has(device.name);
|
|
376
|
-
const ports = Object.entries(device.ports ?? {}).flatMap(([name, port]) => port ? [{ name, port }] : []);
|
|
377
|
-
const carriers = [
|
|
378
|
-
...ports.filter(({ port }) => !port.lag).map(({ name, port }) => ({
|
|
379
|
-
what: `port ${name}`,
|
|
380
|
-
carried: Checks.vlansOf(port),
|
|
381
|
-
faces: port.link ? [port.link.device] : []
|
|
382
|
-
})),
|
|
383
|
-
...Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) => iface.type === "lag" ? [
|
|
384
|
-
{
|
|
385
|
-
what: `LAG ${name}`,
|
|
386
|
-
carried: Checks.vlansOf(iface),
|
|
387
|
-
faces: ports.filter(({ port }) => port.lag === name && port.link).map(({ port }) => port.link.device)
|
|
388
|
-
}
|
|
389
|
-
] : [])
|
|
390
|
-
];
|
|
391
|
-
for (const { what, carried, faces } of carriers) {
|
|
392
|
-
const mixes = carried.some((vlan) => untrusted.has(vlan)) && carried.some((vlan) => !untrusted.has(vlan));
|
|
393
|
-
const facesRouter = isRouter || faces.some((name) => routers.has(name));
|
|
394
|
-
const isExtension = faces.length > 0 && faces.every((name) => extended(device.name, name));
|
|
395
|
-
if (mixes && !facesRouter && !isExtension) {
|
|
396
|
-
findings.push({
|
|
397
|
-
level: "error",
|
|
398
|
-
device: device.name,
|
|
399
|
-
message: `${what} carries an untrusted VLAN beside a trusted one and does not face a router`
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (isRouter)
|
|
404
|
-
continue;
|
|
405
|
-
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
406
|
-
if (iface.type === "vlan" && untrusted.has(iface.vlan)) {
|
|
407
|
-
findings.push({
|
|
408
|
-
level: "error",
|
|
409
|
-
device: device.name,
|
|
410
|
-
message: `interface ${name} puts an address on untrusted VLAN ${iface.vlan}; only a router may`
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
return findings;
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
// src/helpers/route-server.ts
|
|
420
|
-
class RouteServerConfig {
|
|
421
|
-
static LENGTHS = {
|
|
422
|
-
ipv4: { shortest: 8, longest: 24 },
|
|
423
|
-
ipv6: { shortest: 12, longest: 48 }
|
|
424
|
-
};
|
|
425
|
-
static RULES = [
|
|
426
|
-
"# 1. the first AS in its path is the member's own,",
|
|
427
|
-
"# 2. its next hop is the member's own address on the peering LAN,",
|
|
428
|
-
"# 3. it is not inside the peering LAN,",
|
|
429
|
-
`# 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.`
|
|
430
|
-
];
|
|
431
|
-
static bird(server, lan, members) {
|
|
432
|
-
const families = RouteServerConfig.families(server, lan);
|
|
433
|
-
const sessions = [];
|
|
434
|
-
const names = new Map;
|
|
435
|
-
for (const member of members) {
|
|
436
|
-
for (const family of families) {
|
|
437
|
-
const neighbor = member[family];
|
|
438
|
-
if (!neighbor)
|
|
439
|
-
continue;
|
|
440
|
-
const name = RouteServerConfig.protocolName(member.name, family);
|
|
441
|
-
const holder = names.get(name);
|
|
442
|
-
if (holder)
|
|
443
|
-
throw new Error(`members ${holder} and ${member.name} would both have session ${name}`);
|
|
444
|
-
names.set(name, member.name);
|
|
445
|
-
sessions.push(RouteServerConfig.birdSession(name, server, member, family, neighbor, lan[family]));
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const header = [
|
|
449
|
-
`# ${server.name}, a route server of the exchange, for BIRD 2.`,
|
|
450
|
-
"# Circuit wrote it from the member list: change the list, not this file.",
|
|
451
|
-
"",
|
|
452
|
-
`router id ${server.router_id};`,
|
|
453
|
-
"",
|
|
454
|
-
"# Learns the server's interfaces and addresses. It sends no route anywhere.",
|
|
455
|
-
"protocol device {",
|
|
456
|
-
"}",
|
|
457
|
-
"",
|
|
458
|
-
"# One session per member and family. A route a member sends is kept only when:",
|
|
459
|
-
...RouteServerConfig.RULES,
|
|
460
|
-
"# Every member gets every route the others sent, as they sent it.",
|
|
461
|
-
"# A route that is not kept stays visible: birdc show route filtered."
|
|
462
|
-
];
|
|
463
|
-
return [...header, ...sessions.flatMap((session) => ["", session])].join(`
|
|
464
|
-
`) + `
|
|
465
|
-
`;
|
|
466
|
-
}
|
|
467
|
-
static openbgpd(server, lan, members) {
|
|
468
|
-
const families = RouteServerConfig.families(server, lan).filter((family) => members.some((member) => member[family]));
|
|
469
|
-
const lines = [
|
|
470
|
-
`# ${server.name}, a route server of the exchange, for OpenBGPD.`,
|
|
471
|
-
"# Circuit wrote it from the member list: change the list, not this file.",
|
|
472
|
-
"",
|
|
473
|
-
`AS ${server.asn}`,
|
|
474
|
-
`router-id ${server.router_id}`,
|
|
475
|
-
"",
|
|
476
|
-
"# The server passes routes on and carries no traffic, so it keeps them out of its own routing table.",
|
|
477
|
-
"fib-update no",
|
|
478
|
-
"",
|
|
479
|
-
"# Members see each other's paths as they were sent: the server never adds its own AS, and passes MED and communities on untouched.",
|
|
480
|
-
"transparent-as yes",
|
|
481
|
-
"",
|
|
482
|
-
"# A route that is not kept stays visible: bgpctl show rib filtered.",
|
|
483
|
-
"rde rib Loc-RIB include filtered"
|
|
484
|
-
];
|
|
485
|
-
for (const family of families) {
|
|
486
|
-
lines.push("", ...RouteServerConfig.openbgpdGroup(server, family, members));
|
|
487
|
-
}
|
|
488
|
-
lines.push("", "# A route a member sends is kept only when:", ...RouteServerConfig.RULES);
|
|
489
|
-
lines.push("# The last rule a route matches decides, and a route no rule allows is dropped.");
|
|
490
|
-
for (const family of families) {
|
|
491
|
-
const { shortest, longest } = RouteServerConfig.LENGTHS[family];
|
|
492
|
-
const any = family === "ipv4" ? "0.0.0.0/0" : "::/0";
|
|
493
|
-
lines.push(`allow from group "${family} members" peer-as neighbor-as nexthop neighbor prefix ${any} prefixlen ${shortest} - ${longest}`);
|
|
494
|
-
}
|
|
495
|
-
for (const family of families) {
|
|
496
|
-
lines.push(`deny from any prefix ${Address2.prefixOf(lan[family])} or-longer`);
|
|
497
|
-
}
|
|
498
|
-
lines.push("", "# Every member gets every route the others sent, as they sent it.", "allow to any");
|
|
499
|
-
return lines.join(`
|
|
500
|
-
`) + `
|
|
501
|
-
`;
|
|
502
|
-
}
|
|
503
|
-
static families(server, lan) {
|
|
504
|
-
const families = ["ipv4", "ipv6"].filter((family) => server[family]);
|
|
505
|
-
if (!families.length)
|
|
506
|
-
throw new Error(`route server ${server.name} has no address on the peering LAN`);
|
|
507
|
-
for (const family of families) {
|
|
508
|
-
const prefix = lan[family];
|
|
509
|
-
const address = server[family];
|
|
510
|
-
if (!prefix || !Address2.contains(prefix, address))
|
|
511
|
-
throw new Error(`${address} of route server ${server.name} is outside the peering LAN`);
|
|
512
|
-
}
|
|
513
|
-
return families;
|
|
514
|
-
}
|
|
515
|
-
static protocolName(member, family) {
|
|
516
|
-
const name = `${member.toLowerCase().replace(/[^a-z0-9_]/g, "_")}_${family}`;
|
|
517
|
-
return /^[a-z]/.test(name) ? name : `m_${name}`;
|
|
518
|
-
}
|
|
519
|
-
static description(member) {
|
|
520
|
-
return `"${member.name.replace(/["\\]/g, "")}, AS${member.asn}"`;
|
|
521
|
-
}
|
|
522
|
-
static birdSession(name, server, member, family, neighbor, lan) {
|
|
523
|
-
const { shortest, longest } = RouteServerConfig.LENGTHS[family];
|
|
524
|
-
return [
|
|
525
|
-
`protocol bgp ${name} {`,
|
|
526
|
-
` description ${RouteServerConfig.description(member)};`,
|
|
527
|
-
` local ${server[family]} as ${server.asn};`,
|
|
528
|
-
` neighbor ${neighbor} as ${member.asn};`,
|
|
529
|
-
" rs client;",
|
|
530
|
-
` ${family} {`,
|
|
531
|
-
" import keep filtered on;",
|
|
532
|
-
" import filter {",
|
|
533
|
-
` if bgp_path.first != ${member.asn} then reject;`,
|
|
534
|
-
` if bgp_next_hop != ${neighbor} then reject;`,
|
|
535
|
-
` if net ~ [ ${Address2.prefixOf(lan)}+ ] then reject;`,
|
|
536
|
-
` if net.len < ${shortest} || net.len > ${longest} then reject;`,
|
|
537
|
-
" accept;",
|
|
538
|
-
" };",
|
|
539
|
-
" export all;",
|
|
540
|
-
" };",
|
|
541
|
-
"}"
|
|
542
|
-
].join(`
|
|
543
|
-
`);
|
|
544
|
-
}
|
|
545
|
-
static openbgpdGroup(server, family, members) {
|
|
546
|
-
const lines = [
|
|
547
|
-
`# One session per member with an ${family === "ipv4" ? "IPv4" : "IPv6"} address.`,
|
|
548
|
-
`group "${family} members" {`,
|
|
549
|
-
` local-address ${server[family]}`,
|
|
550
|
-
" # A route whose first AS is not the member's own is dropped by the rules below. The default would close the whole session.",
|
|
551
|
-
" enforce neighbor-as no"
|
|
552
|
-
];
|
|
553
|
-
for (const member of members) {
|
|
554
|
-
const neighbor = member[family];
|
|
555
|
-
if (!neighbor)
|
|
556
|
-
continue;
|
|
557
|
-
lines.push(` neighbor ${neighbor} {`, ` remote-as ${member.asn}`, ` descr ${RouteServerConfig.description(member)}`, " }");
|
|
558
|
-
}
|
|
559
|
-
lines.push("}");
|
|
560
|
-
return lines;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// src/helpers/exchange.ts
|
|
565
|
-
class Exchange {
|
|
566
|
-
static build(exchange, members) {
|
|
567
|
-
const percent = exchange.storm_percent ?? 1;
|
|
568
|
-
const ports = (device) => {
|
|
569
|
-
const result = {};
|
|
570
|
-
for (const member of members) {
|
|
571
|
-
const port = member.ports[device];
|
|
572
|
-
if (!port)
|
|
573
|
-
continue;
|
|
574
|
-
result[port] = {
|
|
575
|
-
description: `${member.name}, AS${member.asn}`,
|
|
576
|
-
access_vlan: exchange.vlan,
|
|
577
|
-
stp: exchange.stp ?? false,
|
|
578
|
-
lldp: exchange.lldp ?? false,
|
|
579
|
-
storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
|
|
580
|
-
...exchange.mac_limit !== undefined ? { mac_limit: exchange.mac_limit } : {},
|
|
581
|
-
...exchange.ethertypes ? { ethertypes: exchange.ethertypes } : {}
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
return result;
|
|
585
|
-
};
|
|
586
|
-
const check = (devices) => {
|
|
587
|
-
const findings = [];
|
|
588
|
-
const error = (message, device) => findings.push({ level: "error", ...device ? { device } : {}, message });
|
|
589
|
-
const taken = new Map((exchange.reserved ?? []).map((address) => [address, "reserved"]));
|
|
590
|
-
const portHolders = new Map;
|
|
591
|
-
members.forEach((member, index) => {
|
|
592
|
-
for (const other of members.slice(index + 1)) {
|
|
593
|
-
if (other.name === member.name)
|
|
594
|
-
error(`member ${member.name} is declared twice`);
|
|
595
|
-
if (other.asn === member.asn)
|
|
596
|
-
error(`members ${member.name} and ${other.name} share AS${member.asn}`);
|
|
597
|
-
}
|
|
598
|
-
for (const [address, lan] of [
|
|
599
|
-
[member.ipv4, exchange.lan.ipv4],
|
|
600
|
-
[member.ipv6, exchange.lan.ipv6]
|
|
601
|
-
]) {
|
|
602
|
-
if (!address)
|
|
603
|
-
continue;
|
|
604
|
-
if (!lan || !Address2.contains(lan, address))
|
|
605
|
-
error(`${address} of ${member.name} is outside the peering LAN`);
|
|
606
|
-
const holder = taken.get(address);
|
|
607
|
-
if (holder)
|
|
608
|
-
error(`${address} of ${member.name} is already ${holder === "reserved" ? "reserved" : `${holder}'s`}`);
|
|
609
|
-
taken.set(address, member.name);
|
|
610
|
-
}
|
|
611
|
-
for (const [deviceName, portName] of Object.entries(member.ports)) {
|
|
612
|
-
const holder = portHolders.get(`${deviceName} ${portName}`);
|
|
613
|
-
if (holder)
|
|
614
|
-
error(`port ${portName} is given to both ${holder} and ${member.name}`, deviceName);
|
|
615
|
-
portHolders.set(`${deviceName} ${portName}`, member.name);
|
|
616
|
-
const device = devices.find((candidate) => candidate.name === deviceName);
|
|
617
|
-
const port = device?.ports?.[portName];
|
|
618
|
-
const carried = port ? Checks.vlansOf(port) : [];
|
|
619
|
-
if (carried.length !== 1 || carried[0] !== exchange.vlan) {
|
|
620
|
-
error(`port ${portName} of ${member.name} does not carry the peering VLAN alone`, deviceName);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
return findings;
|
|
625
|
-
};
|
|
626
|
-
const bird = (server) => RouteServerConfig.bird(server, exchange.lan, members);
|
|
627
|
-
const openbgpd = (server) => RouteServerConfig.openbgpd(server, exchange.lan, members);
|
|
628
|
-
return { ports, check, bird, openbgpd };
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
// src/helpers/filters.ts
|
|
632
|
-
class Filters {
|
|
633
|
-
static MARTIANS = ["127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/8"];
|
|
634
|
-
static PRIVATE_RANGES = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"];
|
|
635
|
-
static AMPLIFIERS = {
|
|
636
|
-
qotd: 17,
|
|
637
|
-
chargen: 19,
|
|
638
|
-
dns: 53,
|
|
639
|
-
portmap: 111,
|
|
640
|
-
ntp: 123,
|
|
641
|
-
netbios: 137,
|
|
642
|
-
snmp: 161,
|
|
643
|
-
cldap: 389,
|
|
644
|
-
slp: 427,
|
|
645
|
-
rip: 520,
|
|
646
|
-
"ms-sql": 1434,
|
|
647
|
-
ssdp: 1900,
|
|
648
|
-
ard: 3283,
|
|
649
|
-
"ws-discovery": 3702,
|
|
650
|
-
mdns: 5353,
|
|
651
|
-
coap: 5683,
|
|
652
|
-
ubiquiti: 10001,
|
|
653
|
-
memcached: 11211,
|
|
654
|
-
dvr: 37810
|
|
655
|
-
};
|
|
656
|
-
static ASKED = new Set([Filters.AMPLIFIERS.dns, Filters.AMPLIFIERS.ntp, Filters.AMPLIFIERS.snmp]);
|
|
657
|
-
static BAD_TCP_FLAGS = [
|
|
658
|
-
["FIN+SYN", ["fin", "syn"], []],
|
|
659
|
-
["SYN+RST", ["syn", "rst"], []],
|
|
660
|
-
["FIN+RST", ["fin", "rst"], []],
|
|
661
|
-
["FIN without ACK", ["fin"], ["ack"]],
|
|
662
|
-
["FIN+URG", ["fin", "urg"], []],
|
|
663
|
-
["Xmas scan", ["fin", "syn", "psh", "urg"], []]
|
|
664
|
-
];
|
|
665
|
-
static antiSpoofing(vlan, sources) {
|
|
666
|
-
return sources.map((src) => ({
|
|
667
|
-
description: `${vlan}: drop source ${src}`,
|
|
668
|
-
match: { vlan, src },
|
|
669
|
-
action: "drop"
|
|
670
|
-
}));
|
|
671
|
-
}
|
|
672
|
-
static antiAmplification(destinations, options = {}) {
|
|
673
|
-
return destinations.flatMap((dst) => [
|
|
674
|
-
...(options.keep_fragments ?? []).filter((prefix) => Address2.contains(dst, prefix)).map((prefix) => ({
|
|
675
|
-
description: `UDP fragments to ${prefix} kept`,
|
|
676
|
-
match: { protocol: "udp", dst: prefix, src_port: 0 },
|
|
677
|
-
action: "accept"
|
|
678
|
-
})),
|
|
679
|
-
{ description: `UDP fragments to ${dst}`, match: { protocol: "udp", dst, src_port: 0 }, action: "drop" },
|
|
680
|
-
...Object.entries(Filters.AMPLIFIERS).flatMap(([name, port]) => Filters.ASKED.has(port) ? [
|
|
681
|
-
{
|
|
682
|
-
description: `UDP to ${dst} from ${name}, between two servers`,
|
|
683
|
-
match: { protocol: "udp", dst, src_port: port, dst_port: port },
|
|
684
|
-
action: "accept"
|
|
685
|
-
},
|
|
686
|
-
{
|
|
687
|
-
description: `UDP to ${dst} from ${name}, to a port below 1024`,
|
|
688
|
-
match: { protocol: "udp", dst, src_port: port, dst_port: { min: 0, max: 1023 } },
|
|
689
|
-
action: "drop"
|
|
690
|
-
}
|
|
691
|
-
] : [{ description: `UDP to ${dst} from ${name}`, match: { protocol: "udp", dst, src_port: port }, action: "drop" }])
|
|
692
|
-
]);
|
|
693
|
-
}
|
|
694
|
-
static badTcpFlags() {
|
|
695
|
-
return Filters.BAD_TCP_FLAGS.map(([name, set, unset]) => ({
|
|
696
|
-
description: `Bad flags ${name}`,
|
|
697
|
-
match: { protocol: "tcp", tcp_flags: { set, unset } },
|
|
698
|
-
action: "drop"
|
|
699
|
-
}));
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
// src/helpers/upstreams.ts
|
|
703
|
-
class Upstreams {
|
|
704
|
-
static catalog(catalog) {
|
|
705
|
-
return catalog;
|
|
706
|
-
}
|
|
707
|
-
static doNotAnnounceTo(upstream, asns, description) {
|
|
708
|
-
if (!asns.length)
|
|
709
|
-
return [];
|
|
710
|
-
const community = upstream.communities?.do_not_announce_to;
|
|
711
|
-
if (!community)
|
|
712
|
-
throw new Error(`Upstreams: ${upstream.name ?? `AS${upstream.asn}`} has no communities.do_not_announce_to, so it can't be asked to keep our routes from AS${asns.join(", AS")}`);
|
|
713
|
-
const communities = asns.map(community);
|
|
714
|
-
const large = communities.filter((value) => value.split(":").length === 3);
|
|
715
|
-
const standard = communities.filter((value) => value.split(":").length === 2);
|
|
716
|
-
return [
|
|
717
|
-
{
|
|
718
|
-
description,
|
|
719
|
-
set: {
|
|
720
|
-
...standard.length ? { add_communities: standard } : {},
|
|
721
|
-
...large.length ? { add_large_communities: large } : {}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
];
|
|
725
|
-
}
|
|
726
|
-
static define(catalog, options = {}) {
|
|
727
|
-
const all = Object.entries(catalog);
|
|
728
|
-
const nameOf = (key, upstream) => upstream.name ?? key;
|
|
729
|
-
const unique = (what, names) => {
|
|
730
|
-
const taken = new Map;
|
|
731
|
-
for (const [key, name] of names) {
|
|
732
|
-
if (name === undefined)
|
|
733
|
-
continue;
|
|
734
|
-
const other = taken.get(name);
|
|
735
|
-
if (other)
|
|
736
|
-
throw new Error(`Upstreams: ${other} and ${key} both have the ${what} ${name}`);
|
|
737
|
-
taken.set(name, key);
|
|
738
|
-
}
|
|
739
|
-
};
|
|
740
|
-
unique("VLAN", [
|
|
741
|
-
...all.map(([key, upstream]) => [key, upstream.handoff?.vlan.name]),
|
|
742
|
-
...all.map(([key, upstream]) => [key, upstream.handoff?.native?.name])
|
|
743
|
-
]);
|
|
744
|
-
unique("interface", all.map(([key, upstream]) => [key, upstream.handoff?.interface ?? upstream.tunnel?.interface]));
|
|
745
|
-
unique("policy", [
|
|
746
|
-
...all.map(([key, upstream]) => [key, upstream.import?.name]),
|
|
747
|
-
...all.map(([key, upstream]) => [key, upstream.export.name])
|
|
748
|
-
]);
|
|
749
|
-
unique("BGP group", all.map(([key, upstream]) => [key, upstream.group]));
|
|
750
|
-
const vlans = (device) => {
|
|
751
|
-
const result = {};
|
|
752
|
-
for (const [key, upstream] of all) {
|
|
753
|
-
const handoff = upstream.handoff;
|
|
754
|
-
if (!handoff || device !== undefined && handoff.port?.device !== device)
|
|
755
|
-
continue;
|
|
756
|
-
result[handoff.vlan.name] = { id: handoff.vlan.id, description: nameOf(key, upstream) };
|
|
757
|
-
if (handoff.native) {
|
|
758
|
-
result[handoff.native.name] = {
|
|
759
|
-
id: handoff.native.id,
|
|
760
|
-
description: `Native toward ${nameOf(key, upstream)}, nothing else in it`
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
return result;
|
|
765
|
-
};
|
|
766
|
-
const vlansOn = (device) => all.flatMap(([, upstream]) => upstream.handoff?.port?.device === device ? [upstream.handoff.vlan.name] : []);
|
|
767
|
-
const ports = (device) => {
|
|
768
|
-
const result = {};
|
|
769
|
-
for (const [key, upstream] of all) {
|
|
770
|
-
const handoff = upstream.handoff;
|
|
771
|
-
if (!handoff?.port || handoff.port.device !== device)
|
|
772
|
-
continue;
|
|
773
|
-
result[handoff.port.port] = {
|
|
774
|
-
description: nameOf(key, upstream),
|
|
775
|
-
...handoff.native ? { trunk_vlans: [handoff.vlan.name], native_vlan: handoff.native.name } : { access_vlan: handoff.vlan.name },
|
|
776
|
-
lldp: handoff.port.lldp ?? false,
|
|
777
|
-
stp: handoff.port.stp ?? false,
|
|
778
|
-
...handoff.port.storm_control ? { storm_control: handoff.port.storm_control } : {}
|
|
779
|
-
};
|
|
780
|
-
}
|
|
781
|
-
return result;
|
|
782
|
-
};
|
|
783
|
-
const interfaces = () => {
|
|
784
|
-
const result = {};
|
|
785
|
-
for (const [key, upstream] of all) {
|
|
786
|
-
const description = nameOf(key, upstream);
|
|
787
|
-
if (upstream.tunnel) {
|
|
788
|
-
const { interface: name, local, remote, addresses, mtu } = upstream.tunnel;
|
|
789
|
-
result[name] = { type: "gre", description, local, remote, addresses, ...mtu ? { mtu } : {} };
|
|
790
|
-
} else if (upstream.handoff) {
|
|
791
|
-
const { interface: name, vlan, addresses, exchange } = upstream.handoff;
|
|
792
|
-
result[name] = { type: "vlan", vlan: vlan.name, description, addresses, ...exchange ? { exchange } : {} };
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
return result;
|
|
796
|
-
};
|
|
797
|
-
const uplinks = () => all.flatMap(([, upstream]) => upstream.handoff ? [upstream.handoff.interface] : []);
|
|
798
|
-
const settings = (key, upstream) => {
|
|
799
|
-
const multihop = upstream.multihop;
|
|
800
|
-
return {
|
|
801
|
-
remote_as: upstream.asn,
|
|
802
|
-
description: nameOf(key, upstream),
|
|
803
|
-
role: upstream.role,
|
|
804
|
-
...upstream.import ? { import: upstream.import.name } : {},
|
|
805
|
-
export: upstream.export.name,
|
|
806
|
-
...upstream.table ? { table: upstream.table } : {},
|
|
807
|
-
...multihop ? {
|
|
808
|
-
local_address: multihop.local_address,
|
|
809
|
-
multihop: true,
|
|
810
|
-
...multihop.families ? { families: multihop.families } : {},
|
|
811
|
-
...multihop.password ? { password: multihop.password } : {}
|
|
812
|
-
} : {}
|
|
813
|
-
};
|
|
814
|
-
};
|
|
815
|
-
const groups = () => Object.fromEntries(all.flatMap(([key, upstream]) => upstream.group ? [[upstream.group, settings(key, upstream)]] : []));
|
|
816
|
-
const neighbors = () => Object.fromEntries(all.flatMap(([key, upstream]) => Object.entries(upstream.sessions).map(([name, session]) => {
|
|
817
|
-
const limit = upstream.limits?.[Address2.family(session.address)];
|
|
818
|
-
return [
|
|
819
|
-
name,
|
|
820
|
-
{
|
|
821
|
-
...upstream.group ? { group: upstream.group } : settings(key, upstream),
|
|
822
|
-
...session,
|
|
823
|
-
...limit ? { max_prefixes: limit } : {}
|
|
824
|
-
}
|
|
825
|
-
];
|
|
826
|
-
})));
|
|
827
|
-
const policies = () => {
|
|
828
|
-
const result = {};
|
|
829
|
-
for (const [key, upstream] of all) {
|
|
830
|
-
if (upstream.import) {
|
|
831
|
-
const rules = upstream.import.rules;
|
|
832
|
-
result[upstream.import.name] = options.mapImports ? options.mapImports(rules, key, upstream) : rules;
|
|
833
|
-
}
|
|
834
|
-
const rules = upstream.export.rules;
|
|
835
|
-
result[upstream.export.name] = options.mapExports ? options.mapExports(rules, key, upstream) : rules;
|
|
836
|
-
}
|
|
837
|
-
return result;
|
|
838
|
-
};
|
|
839
|
-
const untrusted = () => [
|
|
840
|
-
...all.flatMap(([, upstream]) => upstream.handoff ? [upstream.handoff.vlan.name] : []),
|
|
841
|
-
...natives()
|
|
842
|
-
];
|
|
843
|
-
const natives = () => all.flatMap(([, upstream]) => upstream.handoff?.native ? [upstream.handoff.native.name] : []);
|
|
844
|
-
const privateDrops = () => all.flatMap(([, upstream]) => upstream.handoff && upstream.drop_private ? [upstream.handoff.vlan.name] : []);
|
|
845
|
-
const tunnelRules = () => all.flatMap(([key, upstream]) => upstream.tunnel ? [
|
|
846
|
-
{
|
|
847
|
-
description: `GRE from ${nameOf(key, upstream)}`,
|
|
848
|
-
match: {
|
|
849
|
-
protocol: "gre",
|
|
850
|
-
src: `${upstream.tunnel.remote}/${Address2.bits(Address2.family(upstream.tunnel.remote))}`
|
|
851
|
-
},
|
|
852
|
-
action: "accept"
|
|
853
|
-
}
|
|
854
|
-
] : []);
|
|
855
|
-
const addresses = () => all.flatMap(([, upstream]) => Object.values(upstream.sessions).map((session) => session.address));
|
|
856
|
-
return {
|
|
857
|
-
vlans,
|
|
858
|
-
vlansOn,
|
|
859
|
-
ports,
|
|
860
|
-
interfaces,
|
|
861
|
-
uplinks,
|
|
862
|
-
groups,
|
|
863
|
-
neighbors,
|
|
864
|
-
policies,
|
|
865
|
-
untrusted,
|
|
866
|
-
natives,
|
|
867
|
-
privateDrops,
|
|
868
|
-
tunnelRules,
|
|
869
|
-
addresses
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
export {
|
|
874
|
-
Bgp,
|
|
875
|
-
Cables,
|
|
876
|
-
Checks,
|
|
877
|
-
Exchange,
|
|
878
|
-
Filters,
|
|
879
|
-
RouteServerConfig,
|
|
880
|
-
Upstreams
|
|
881
|
-
};
|