@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/src/helpers/checks.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { Address } from "../core/address";
|
|
2
|
-
import type { Check, Finding } from "../core/define";
|
|
3
|
-
import { Bgp } from "./bgp";
|
|
4
|
-
|
|
5
|
-
/** Where traffic from outside, before anything filters it, may go. Every VLAN not listed as untrusted is trusted, so a VLAN added later is protected without being listed. */
|
|
6
|
-
export type TrustBoundary = {
|
|
7
|
-
/** VLANs carrying traffic straight from outside: transits, exchanges, an ISP handoff. */
|
|
8
|
-
untrusted: readonly string[];
|
|
9
|
-
/** The devices that filter: the only ones that may give an untrusted VLAN an address, and the only ones a port carrying both kinds may face. */
|
|
10
|
-
routers: readonly string[];
|
|
11
|
-
/**
|
|
12
|
-
* Links between two switches that may carry both kinds, each named by its two devices: a site's switch extended to another building, with an exchange's VLAN and its own management on one cable. Neither end gives an untrusted VLAN an address.
|
|
13
|
-
*/
|
|
14
|
-
extended?: readonly (readonly [string, string])[];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
/** Checks for `checks` in `defineNetwork`, and helpers for checks of your own. */
|
|
18
|
-
export class Checks {
|
|
19
|
-
/**
|
|
20
|
-
* Every BGP export ends in a reject that matches everything, so nothing leaves by accident. An export to a neighbor whose `role` is `customer` is left out: sending a customer everything you know is what it pays for, and cannot leak a route to the rest of the internet.
|
|
21
|
-
*/
|
|
22
|
-
static readonly exportsEndInReject: Check = (devices) => {
|
|
23
|
-
const findings: Finding[] = [];
|
|
24
|
-
|
|
25
|
-
for (const device of devices) {
|
|
26
|
-
for (const neighbor of Bgp.neighborPolicies(device)) {
|
|
27
|
-
if (neighbor.role === "customer") continue;
|
|
28
|
-
|
|
29
|
-
const last = neighbor.export ? device.policies?.[neighbor.export]?.at(-1) : undefined;
|
|
30
|
-
|
|
31
|
-
if (neighbor.export && (last?.action !== "reject" || last.match)) {
|
|
32
|
-
findings.push({
|
|
33
|
-
level: "error",
|
|
34
|
-
device: device.name,
|
|
35
|
-
message: `${neighbor.name}: export ${neighbor.export} does not end in an unconditional reject`,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return findings;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Every BGP import ends in a rule that decides for every route, an accept or a reject without a match, so what the import takes is written in it and not left to the platform's default.
|
|
46
|
-
*/
|
|
47
|
-
static readonly importsEndInAcceptOrReject: Check = (devices) => {
|
|
48
|
-
const findings: Finding[] = [];
|
|
49
|
-
|
|
50
|
-
for (const device of devices) {
|
|
51
|
-
for (const neighbor of Bgp.neighborPolicies(device)) {
|
|
52
|
-
const last = neighbor.import ? device.policies?.[neighbor.import]?.at(-1) : undefined;
|
|
53
|
-
|
|
54
|
-
if (neighbor.import && (!last?.action || last.match)) {
|
|
55
|
-
findings.push({
|
|
56
|
-
level: "error",
|
|
57
|
-
device: device.name,
|
|
58
|
-
message: `${neighbor.name}: import ${neighbor.import} does not end in an unconditional accept or reject`,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return findings;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
/** 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. */
|
|
68
|
-
static readonly tunnelsOutsideOffered: Check = (devices) => {
|
|
69
|
-
const findings: Finding[] = [];
|
|
70
|
-
|
|
71
|
-
for (const device of devices) {
|
|
72
|
-
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
73
|
-
if (iface.type !== "gre" && iface.type !== "vxlan") continue;
|
|
74
|
-
|
|
75
|
-
const subnets = (iface.addresses ?? []).map((entry) => (typeof entry === "string" ? entry : entry.address));
|
|
76
|
-
|
|
77
|
-
for (const neighbor of Bgp.neighborPolicies(device)) {
|
|
78
|
-
if (!subnets.some((subnet) => Address.contains(subnet, neighbor.address))) continue;
|
|
79
|
-
|
|
80
|
-
for (const prefix of Bgp.offered(device, neighbor.export)) {
|
|
81
|
-
if (Address.contains(prefix, iface.local)) {
|
|
82
|
-
findings.push({
|
|
83
|
-
level: "error",
|
|
84
|
-
device: device.name,
|
|
85
|
-
message: `${name} is sourced from ${iface.local}, inside ${prefix}, which ${neighbor.name} is offered`,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return findings;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* A native VLAN is a dead end: native on one trunk (a port or a LAG) of a device at most, and never an access VLAN or a tagged VLAN anywhere on that device. On a switch, a native VLAN is an untagged member of every trunk it is native on, so one native VLAN shared by two trunks joins them: every untagged frame from one leaves on the other.
|
|
98
|
-
*/
|
|
99
|
-
static readonly nativeVlansAreDeadEnds: Check = (devices) => {
|
|
100
|
-
const findings: Finding[] = [];
|
|
101
|
-
|
|
102
|
-
for (const device of devices) {
|
|
103
|
-
const carriers = [
|
|
104
|
-
...Object.entries(device.ports ?? {}).flatMap(([name, port]) => (port ? [{ what: `port ${name}`, settings: port }] : [])),
|
|
105
|
-
...Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
|
|
106
|
-
iface.type === "lag" ? [{ what: `LAG ${name}`, settings: iface }] : []
|
|
107
|
-
),
|
|
108
|
-
];
|
|
109
|
-
|
|
110
|
-
const nativeOn = new Map<string, string[]>();
|
|
111
|
-
for (const { what, settings } of carriers) {
|
|
112
|
-
if (settings.native_vlan) nativeOn.set(settings.native_vlan, [...(nativeOn.get(settings.native_vlan) ?? []), what]);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
for (const [vlan, trunks] of nativeOn) {
|
|
116
|
-
const report = (message: string) =>
|
|
117
|
-
findings.push({ level: "error", device: device.name, message: `native VLAN ${vlan} ${message}` });
|
|
118
|
-
|
|
119
|
-
if (trunks.length > 1) report(`is native on ${trunks.join(", ")}: give each trunk its own`);
|
|
120
|
-
|
|
121
|
-
const members = carriers.filter(({ settings }) => Checks.vlansOf(settings).includes(vlan));
|
|
122
|
-
for (const { what } of members) report(`is also an access or tagged VLAN on ${what}`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return findings;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/** The VLANs a port carries, untagged or tagged. */
|
|
130
|
-
static vlansOf(port: { access_vlan?: string; trunk_vlans?: readonly string[] }): string[] {
|
|
131
|
-
return [port.access_vlan, ...(port.trunk_vlans ?? [])].filter((vlan): vlan is string => vlan !== undefined);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** 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. */
|
|
135
|
-
static trustBoundary(boundary: TrustBoundary): Check {
|
|
136
|
-
return (devices) => {
|
|
137
|
-
const untrusted = new Set(boundary.untrusted);
|
|
138
|
-
const routers = new Set(boundary.routers);
|
|
139
|
-
const extended = (a: string, b: string) =>
|
|
140
|
-
(boundary.extended ?? []).some(([x, y]) => (x === a && y === b) || (x === b && y === a));
|
|
141
|
-
const findings: Finding[] = [];
|
|
142
|
-
|
|
143
|
-
for (const device of devices) {
|
|
144
|
-
const isRouter = routers.has(device.name);
|
|
145
|
-
const ports = Object.entries(device.ports ?? {}).flatMap(([name, port]) => (port ? [{ name, port }] : []));
|
|
146
|
-
|
|
147
|
-
// What carries VLANs: a port on its own, or a LAG, which faces whatever its members link to.
|
|
148
|
-
const carriers = [
|
|
149
|
-
...ports
|
|
150
|
-
.filter(({ port }) => !port.lag)
|
|
151
|
-
.map(({ name, port }) => ({
|
|
152
|
-
what: `port ${name}`,
|
|
153
|
-
carried: Checks.vlansOf(port),
|
|
154
|
-
faces: port.link ? [port.link.device] : [],
|
|
155
|
-
})),
|
|
156
|
-
...Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) =>
|
|
157
|
-
iface.type === "lag"
|
|
158
|
-
? [
|
|
159
|
-
{
|
|
160
|
-
what: `LAG ${name}`,
|
|
161
|
-
carried: Checks.vlansOf(iface),
|
|
162
|
-
faces: ports
|
|
163
|
-
.filter(({ port }) => port.lag === name && port.link)
|
|
164
|
-
.map(({ port }) => port.link!.device),
|
|
165
|
-
},
|
|
166
|
-
]
|
|
167
|
-
: []
|
|
168
|
-
),
|
|
169
|
-
];
|
|
170
|
-
|
|
171
|
-
for (const { what, carried, faces } of carriers) {
|
|
172
|
-
const mixes = carried.some((vlan) => untrusted.has(vlan)) && carried.some((vlan) => !untrusted.has(vlan));
|
|
173
|
-
const facesRouter = isRouter || faces.some((name) => routers.has(name));
|
|
174
|
-
const isExtension = faces.length > 0 && faces.every((name) => extended(device.name, name));
|
|
175
|
-
|
|
176
|
-
if (mixes && !facesRouter && !isExtension) {
|
|
177
|
-
findings.push({
|
|
178
|
-
level: "error",
|
|
179
|
-
device: device.name,
|
|
180
|
-
message: `${what} carries an untrusted VLAN beside a trusted one and does not face a router`,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (isRouter) continue;
|
|
186
|
-
|
|
187
|
-
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
188
|
-
if (iface.type === "vlan" && untrusted.has(iface.vlan)) {
|
|
189
|
-
findings.push({
|
|
190
|
-
level: "error",
|
|
191
|
-
device: device.name,
|
|
192
|
-
message: `interface ${name} puts an address on untrusted VLAN ${iface.vlan}; only a router may`,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return findings;
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
}
|
package/src/helpers/exchange.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { Address } from "../core/address";
|
|
2
|
-
import type { Check, Finding } from "../core/define";
|
|
3
|
-
import type { Ethertype, IP, Port, Prefix } from "../schema";
|
|
4
|
-
import { Checks } from "./checks";
|
|
5
|
-
import { RouteServerConfig, type RouteServer } from "./route-server";
|
|
6
|
-
|
|
7
|
-
/** A member of an internet exchange: a network with a port on the fabric and an address on the peering LAN. */
|
|
8
|
-
export type ExchangeMember = {
|
|
9
|
-
/** Lowercase, and never reused. */
|
|
10
|
-
name: string;
|
|
11
|
-
asn: number;
|
|
12
|
-
/** The member's port on each fabric switch, by switch name. */
|
|
13
|
-
ports: Readonly<Record<string, string>>;
|
|
14
|
-
/** Its addresses on the peering LAN. The route servers peer with these. */
|
|
15
|
-
ipv4?: IP;
|
|
16
|
-
ipv6?: IP;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/** An internet exchange's peering LAN, as its fabric carries it. */
|
|
20
|
-
export type InternetExchange<V extends string> = {
|
|
21
|
-
/** The peering VLAN, by the name the fabric switches give it. */
|
|
22
|
-
vlan: V;
|
|
23
|
-
/** The peering LAN's subnets. */
|
|
24
|
-
lan: { ipv4?: Prefix; ipv6?: Prefix };
|
|
25
|
-
/** Addresses on the peering LAN no member may take, such as the route servers'. */
|
|
26
|
-
reserved?: readonly IP[];
|
|
27
|
-
/** How much broadcast, multicast and unknown unicast a member port lets through, in percent of its speed. 1 when absent. */
|
|
28
|
-
storm_percent?: number;
|
|
29
|
-
/** The most MAC addresses a member port learns, usually 1. No limit when absent. No platform renders it yet, so `validate` refuses it. */
|
|
30
|
-
mac_limit?: number;
|
|
31
|
-
/** The only kinds of frame a member port takes in, usually `ipv4`, `ipv6` and `arp`. Every kind when absent. No platform renders it yet, so `validate` refuses it. */
|
|
32
|
-
ethertypes?: readonly Ethertype[];
|
|
33
|
-
/** LLDP on member ports. Off when absent: a member's router has no business learning the fabric. */
|
|
34
|
-
lldp?: boolean;
|
|
35
|
-
/** Spanning tree on member ports. Off when absent: a member's port faces a router, which must not join the fabric's spanning tree. */
|
|
36
|
-
stp?: boolean;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
/** An internet exchange's fabric: its member ports, a check of its members, and its route servers' config. */
|
|
40
|
-
export class Exchange {
|
|
41
|
-
/**
|
|
42
|
-
* Member ports and a check for an internet exchange's fabric.
|
|
43
|
-
*
|
|
44
|
-
* - `ports(device)`: every member's port on one switch, each written as
|
|
45
|
-
* `{ description: "<name>, AS<asn>", access_vlan: vlan, stp: false, lldp: false, storm_control: { broadcast, multicast, unknown_unicast: { percent: storm_percent } } }`,
|
|
46
|
-
* plus `mac_limit` and `ethertypes` when the exchange sets them. `stp` and `lldp` are the exchange's own when it sets them, and storm control is 1 % when `storm_percent` is absent.
|
|
47
|
-
* - `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.
|
|
48
|
-
* - `bird(server)`: one route server's BIRD 2 config, with a session to every member. Write it to a file for the server; Circuit never logs in to a route server.
|
|
49
|
-
* - `openbgpd(server)`: the same route server's config for OpenBGPD, its `bgpd.conf`, with the same sessions and the same rules.
|
|
50
|
-
*/
|
|
51
|
-
static build<const V extends string>(exchange: InternetExchange<V>, members: readonly ExchangeMember[]) {
|
|
52
|
-
const percent = exchange.storm_percent ?? 1;
|
|
53
|
-
|
|
54
|
-
const ports = (device: string) => {
|
|
55
|
-
const result: Record<string, Port<V, string, string, string>> = {};
|
|
56
|
-
|
|
57
|
-
for (const member of members) {
|
|
58
|
-
const port = member.ports[device];
|
|
59
|
-
if (!port) continue;
|
|
60
|
-
|
|
61
|
-
result[port] = {
|
|
62
|
-
description: `${member.name}, AS${member.asn}`,
|
|
63
|
-
access_vlan: exchange.vlan,
|
|
64
|
-
stp: exchange.stp ?? false,
|
|
65
|
-
lldp: exchange.lldp ?? false,
|
|
66
|
-
storm_control: { broadcast: { percent }, multicast: { percent }, unknown_unicast: { percent } },
|
|
67
|
-
...(exchange.mac_limit !== undefined ? { mac_limit: exchange.mac_limit } : {}),
|
|
68
|
-
...(exchange.ethertypes ? { ethertypes: exchange.ethertypes } : {}),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return result;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const check: Check = (devices) => {
|
|
76
|
-
const findings: Finding[] = [];
|
|
77
|
-
const error = (message: string, device?: string) => findings.push({ level: "error", ...(device ? { device } : {}), message });
|
|
78
|
-
const taken = new Map<string, string>((exchange.reserved ?? []).map((address) => [address, "reserved"]));
|
|
79
|
-
// `ports()` writes one port per name, so a second member on a port would silently replace the first.
|
|
80
|
-
const portHolders = new Map<string, string>();
|
|
81
|
-
|
|
82
|
-
members.forEach((member, index) => {
|
|
83
|
-
for (const other of members.slice(index + 1)) {
|
|
84
|
-
if (other.name === member.name) error(`member ${member.name} is declared twice`);
|
|
85
|
-
if (other.asn === member.asn) error(`members ${member.name} and ${other.name} share AS${member.asn}`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
for (const [address, lan] of [
|
|
89
|
-
[member.ipv4, exchange.lan.ipv4],
|
|
90
|
-
[member.ipv6, exchange.lan.ipv6],
|
|
91
|
-
] as const) {
|
|
92
|
-
if (!address) continue;
|
|
93
|
-
if (!lan || !Address.contains(lan, address)) error(`${address} of ${member.name} is outside the peering LAN`);
|
|
94
|
-
|
|
95
|
-
const holder = taken.get(address);
|
|
96
|
-
if (holder) error(`${address} of ${member.name} is already ${holder === "reserved" ? "reserved" : `${holder}'s`}`);
|
|
97
|
-
taken.set(address, member.name);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
for (const [deviceName, portName] of Object.entries(member.ports)) {
|
|
101
|
-
const holder = portHolders.get(`${deviceName} ${portName}`);
|
|
102
|
-
if (holder) error(`port ${portName} is given to both ${holder} and ${member.name}`, deviceName);
|
|
103
|
-
portHolders.set(`${deviceName} ${portName}`, member.name);
|
|
104
|
-
|
|
105
|
-
const device = devices.find((candidate) => candidate.name === deviceName);
|
|
106
|
-
const port = device?.ports?.[portName as keyof typeof device.ports];
|
|
107
|
-
const carried = port ? Checks.vlansOf(port) : [];
|
|
108
|
-
|
|
109
|
-
if (carried.length !== 1 || carried[0] !== exchange.vlan) {
|
|
110
|
-
error(`port ${portName} of ${member.name} does not carry the peering VLAN alone`, deviceName);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
return findings;
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const bird = (server: RouteServer) => RouteServerConfig.bird(server, exchange.lan, members);
|
|
119
|
-
const openbgpd = (server: RouteServer) => RouteServerConfig.openbgpd(server, exchange.lan, members);
|
|
120
|
-
|
|
121
|
-
return { ports, check, bird, openbgpd };
|
|
122
|
-
}
|
|
123
|
-
}
|
package/src/helpers/filters.ts
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { Address } from "../core/address";
|
|
2
|
-
import type { AclRule, FilterRule, Prefix, TcpFlag } from "../schema";
|
|
3
|
-
|
|
4
|
-
/** What `Filters.antiAmplification` leaves alone. */
|
|
5
|
-
export type AntiAmplificationOptions = {
|
|
6
|
-
/** 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. */
|
|
7
|
-
keep_fragments?: readonly Prefix[];
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
/** Address lists and edge filters: hardware ACL rules and firewall rules for traffic from outside. */
|
|
11
|
-
export class Filters {
|
|
12
|
-
/** IPv4 sources that never arrive from the internet: loopback, link-local and "this network", after RFC 6890. */
|
|
13
|
-
static readonly MARTIANS = ["127.0.0.0/8", "169.254.0.0/16", "0.0.0.0/8"] as const satisfies readonly Prefix[];
|
|
14
|
-
|
|
15
|
-
/** IPv4 private space, RFC 1918. */
|
|
16
|
-
static readonly PRIVATE_RANGES = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"] as const satisfies readonly Prefix[];
|
|
17
|
-
|
|
18
|
-
/** 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. */
|
|
19
|
-
static readonly AMPLIFIERS = {
|
|
20
|
-
qotd: 17,
|
|
21
|
-
chargen: 19,
|
|
22
|
-
dns: 53,
|
|
23
|
-
portmap: 111,
|
|
24
|
-
ntp: 123,
|
|
25
|
-
netbios: 137,
|
|
26
|
-
snmp: 161,
|
|
27
|
-
cldap: 389,
|
|
28
|
-
slp: 427,
|
|
29
|
-
rip: 520,
|
|
30
|
-
"ms-sql": 1434,
|
|
31
|
-
ssdp: 1900,
|
|
32
|
-
ard: 3283,
|
|
33
|
-
"ws-discovery": 3702,
|
|
34
|
-
mdns: 5353,
|
|
35
|
-
coap: 5683,
|
|
36
|
-
ubiquiti: 10001,
|
|
37
|
-
memcached: 11211,
|
|
38
|
-
dvr: 37810,
|
|
39
|
-
} as const satisfies Record<string, number>;
|
|
40
|
-
|
|
41
|
-
/** 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. */
|
|
42
|
-
private static readonly ASKED: ReadonlySet<number> = new Set([Filters.AMPLIFIERS.dns, Filters.AMPLIFIERS.ntp, Filters.AMPLIFIERS.snmp]);
|
|
43
|
-
|
|
44
|
-
/** TCP flag combinations no legitimate packet carries: name, flags set, flags unset. */
|
|
45
|
-
private static readonly BAD_TCP_FLAGS = [
|
|
46
|
-
["FIN+SYN", ["fin", "syn"], []],
|
|
47
|
-
["SYN+RST", ["syn", "rst"], []],
|
|
48
|
-
["FIN+RST", ["fin", "rst"], []],
|
|
49
|
-
["FIN without ACK", ["fin"], ["ack"]],
|
|
50
|
-
["FIN+URG", ["fin", "urg"], []],
|
|
51
|
-
["Xmas scan", ["fin", "syn", "psh", "urg"], []],
|
|
52
|
-
] as const satisfies readonly (readonly [string, readonly TcpFlag[], readonly TcpFlag[]])[];
|
|
53
|
-
|
|
54
|
-
/** 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. */
|
|
55
|
-
static antiSpoofing<V extends string>(vlan: V, sources: readonly Prefix[]): AclRule<V>[] {
|
|
56
|
-
return sources.map((src) => ({
|
|
57
|
-
description: `${vlan}: drop source ${src}`,
|
|
58
|
-
match: { vlan, src },
|
|
59
|
-
action: "drop",
|
|
60
|
-
}));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Hardware ACL rules dropping reflected UDP floods toward `destinations`, in the switch chip, without limiting anything else.
|
|
65
|
-
*
|
|
66
|
-
* - Replies from an amplifier no host asks across the internet are dropped.
|
|
67
|
-
* - 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.
|
|
68
|
-
* - The fragments after the first carry no port, and a switch chip reads them as port 0. They are dropped, but toward `keep_fragments`.
|
|
69
|
-
*/
|
|
70
|
-
static antiAmplification(destinations: readonly Prefix[], options: AntiAmplificationOptions = {}): AclRule<never>[] {
|
|
71
|
-
return destinations.flatMap((dst) => [
|
|
72
|
-
...(options.keep_fragments ?? [])
|
|
73
|
-
.filter((prefix) => Address.contains(dst, prefix))
|
|
74
|
-
.map((prefix): AclRule<never> => ({
|
|
75
|
-
description: `UDP fragments to ${prefix} kept`,
|
|
76
|
-
match: { protocol: "udp", dst: prefix, src_port: 0 },
|
|
77
|
-
action: "accept",
|
|
78
|
-
})),
|
|
79
|
-
{ description: `UDP fragments to ${dst}`, match: { protocol: "udp", dst, src_port: 0 }, action: "drop" },
|
|
80
|
-
...Object.entries(Filters.AMPLIFIERS).flatMap(([name, port]): AclRule<never>[] =>
|
|
81
|
-
Filters.ASKED.has(port)
|
|
82
|
-
? [
|
|
83
|
-
{
|
|
84
|
-
description: `UDP to ${dst} from ${name}, between two servers`,
|
|
85
|
-
match: { protocol: "udp", dst, src_port: port, dst_port: port },
|
|
86
|
-
action: "accept",
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
description: `UDP to ${dst} from ${name}, to a port below 1024`,
|
|
90
|
-
match: { protocol: "udp", dst, src_port: port, dst_port: { min: 0, max: 1023 } },
|
|
91
|
-
action: "drop",
|
|
92
|
-
},
|
|
93
|
-
]
|
|
94
|
-
: [{ description: `UDP to ${dst} from ${name}`, match: { protocol: "udp", dst, src_port: port }, action: "drop" }]
|
|
95
|
-
),
|
|
96
|
-
]);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Firewall rules dropping packets with a TCP flag combination no legitimate packet carries, for a forward chain. */
|
|
100
|
-
static badTcpFlags(): FilterRule<never, never>[] {
|
|
101
|
-
return Filters.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
|
-
}
|
package/src/helpers/index.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Building blocks most networks write the same way: address lists, cables written once for both ends, edge filters, a BGP import sanity policy, an exchange's member ports and route servers, upstreams written once, checks, and helpers for checks of your own. Each returns plain config to place where it belongs, so nothing here applies itself.
|
|
2
|
-
|
|
3
|
-
export { Bgp } from "./bgp";
|
|
4
|
-
export type {
|
|
5
|
-
AnnounceOptions,
|
|
6
|
-
Announced,
|
|
7
|
-
CommunityScheme,
|
|
8
|
-
DefaultOnlyOptions,
|
|
9
|
-
ExportNeighbor,
|
|
10
|
-
LearnOptions,
|
|
11
|
-
NeighborCommunities,
|
|
12
|
-
SanityLengths,
|
|
13
|
-
} from "./bgp";
|
|
14
|
-
export { Cables } from "./cables";
|
|
15
|
-
export type { FarEnd } from "./cables";
|
|
16
|
-
export { Checks } from "./checks";
|
|
17
|
-
export type { TrustBoundary } from "./checks";
|
|
18
|
-
export { Exchange } from "./exchange";
|
|
19
|
-
export type { ExchangeMember, InternetExchange } from "./exchange";
|
|
20
|
-
export { Filters } from "./filters";
|
|
21
|
-
export { RouteServerConfig } from "./route-server";
|
|
22
|
-
export { Upstreams } from "./upstreams";
|
|
23
|
-
export type { NamedVlan, Upstream, UpstreamCatalog, UpstreamOptions, UpstreamPolicy } from "./upstreams";
|
|
24
|
-
export type { RouteServer } from "./route-server";
|
|
25
|
-
export type { AntiAmplificationOptions } from "./filters";
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { Address } from "../core/address";
|
|
2
|
-
import type { Family, IPv4, IPv6, Prefix } from "../schema";
|
|
3
|
-
import type { ExchangeMember } from "./exchange";
|
|
4
|
-
|
|
5
|
-
/** A route server on an exchange's peering LAN: a server running BIRD 2 or OpenBGPD that passes every member's routes to every other member. */
|
|
6
|
-
export type RouteServer = {
|
|
7
|
-
/** Its name, such as `rs1`, written at the top of its config. */
|
|
8
|
-
name: string;
|
|
9
|
-
/** Its own AS. Members peer with it, but it never adds itself to a path, so members see each other's paths as they were sent. */
|
|
10
|
-
asn: number;
|
|
11
|
-
/** Its BGP router ID, an IPv4 address of the server. Its address on the peering LAN is the usual pick. */
|
|
12
|
-
router_id: IPv4;
|
|
13
|
-
/** Its IPv4 address on the peering LAN. Every member with an IPv4 address gets an IPv4 session from it. */
|
|
14
|
-
ipv4?: IPv4;
|
|
15
|
-
/** Its IPv6 address on the peering LAN. Every member with an IPv6 address gets an IPv6 session from it. */
|
|
16
|
-
ipv6?: IPv6;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/** A route server's config, written from an exchange's member list. Circuit writes the file; it never logs in to the server. */
|
|
20
|
-
export class RouteServerConfig {
|
|
21
|
-
/** The shortest and longest prefix a member may send, by family. */
|
|
22
|
-
static readonly LENGTHS: Readonly<Record<Family, { shortest: number; longest: number }>> = {
|
|
23
|
-
ipv4: { shortest: 8, longest: 24 },
|
|
24
|
-
ipv6: { shortest: 12, longest: 48 },
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/** The rules a route must pass, the same in every config. */
|
|
28
|
-
private static readonly RULES = [
|
|
29
|
-
"# 1. the first AS in its path is the member's own,",
|
|
30
|
-
"# 2. its next hop is the member's own address on the peering LAN,",
|
|
31
|
-
"# 3. it is not inside the peering LAN,",
|
|
32
|
-
`# 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.`,
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The BIRD 2 config of one route server: one session per member and family, each passing on only the routes the member may send.
|
|
37
|
-
*
|
|
38
|
-
* Throws when the server has no address on the peering LAN, has one outside it, or two members would get sessions of one name.
|
|
39
|
-
*/
|
|
40
|
-
static bird(server: RouteServer, lan: { ipv4?: Prefix; ipv6?: Prefix }, members: readonly ExchangeMember[]): string {
|
|
41
|
-
const families = RouteServerConfig.families(server, lan);
|
|
42
|
-
const sessions: string[] = [];
|
|
43
|
-
const names = new Map<string, string>();
|
|
44
|
-
|
|
45
|
-
for (const member of members) {
|
|
46
|
-
for (const family of families) {
|
|
47
|
-
const neighbor = member[family];
|
|
48
|
-
if (!neighbor) continue;
|
|
49
|
-
|
|
50
|
-
const name = RouteServerConfig.protocolName(member.name, family);
|
|
51
|
-
const holder = names.get(name);
|
|
52
|
-
if (holder) throw new Error(`members ${holder} and ${member.name} would both have session ${name}`);
|
|
53
|
-
names.set(name, member.name);
|
|
54
|
-
|
|
55
|
-
sessions.push(RouteServerConfig.birdSession(name, server, member, family, neighbor, lan[family]!));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const header = [
|
|
60
|
-
`# ${server.name}, a route server of the exchange, for BIRD 2.`,
|
|
61
|
-
"# Circuit wrote it from the member list: change the list, not this file.",
|
|
62
|
-
"",
|
|
63
|
-
`router id ${server.router_id};`,
|
|
64
|
-
"",
|
|
65
|
-
"# Learns the server's interfaces and addresses. It sends no route anywhere.",
|
|
66
|
-
"protocol device {",
|
|
67
|
-
"}",
|
|
68
|
-
"",
|
|
69
|
-
"# One session per member and family. A route a member sends is kept only when:",
|
|
70
|
-
...RouteServerConfig.RULES,
|
|
71
|
-
"# Every member gets every route the others sent, as they sent it.",
|
|
72
|
-
"# A route that is not kept stays visible: birdc show route filtered.",
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
return [...header, ...sessions.flatMap((session) => ["", session])].join("\n") + "\n";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The OpenBGPD config (`bgpd.conf`) of one route server: one session per member and family, in one group per family, and filter rules that pass on only the routes each member may send.
|
|
80
|
-
*
|
|
81
|
-
* Throws when the server has no address on the peering LAN, or has one outside it.
|
|
82
|
-
*/
|
|
83
|
-
static openbgpd(server: RouteServer, lan: { ipv4?: Prefix; ipv6?: Prefix }, members: readonly ExchangeMember[]): string {
|
|
84
|
-
const families = RouteServerConfig.families(server, lan).filter((family) => members.some((member) => member[family]));
|
|
85
|
-
|
|
86
|
-
const lines = [
|
|
87
|
-
`# ${server.name}, a route server of the exchange, for OpenBGPD.`,
|
|
88
|
-
"# Circuit wrote it from the member list: change the list, not this file.",
|
|
89
|
-
"",
|
|
90
|
-
`AS ${server.asn}`,
|
|
91
|
-
`router-id ${server.router_id}`,
|
|
92
|
-
"",
|
|
93
|
-
"# The server passes routes on and carries no traffic, so it keeps them out of its own routing table.",
|
|
94
|
-
"fib-update no",
|
|
95
|
-
"",
|
|
96
|
-
"# Members see each other's paths as they were sent: the server never adds its own AS, and passes MED and communities on untouched.",
|
|
97
|
-
"transparent-as yes",
|
|
98
|
-
"",
|
|
99
|
-
"# A route that is not kept stays visible: bgpctl show rib filtered.",
|
|
100
|
-
"rde rib Loc-RIB include filtered",
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
for (const family of families) {
|
|
104
|
-
lines.push("", ...RouteServerConfig.openbgpdGroup(server, family, members));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
lines.push("", "# A route a member sends is kept only when:", ...RouteServerConfig.RULES);
|
|
108
|
-
lines.push("# The last rule a route matches decides, and a route no rule allows is dropped.");
|
|
109
|
-
|
|
110
|
-
for (const family of families) {
|
|
111
|
-
const { shortest, longest } = RouteServerConfig.LENGTHS[family];
|
|
112
|
-
const any = family === "ipv4" ? "0.0.0.0/0" : "::/0";
|
|
113
|
-
|
|
114
|
-
// `prefixlen 8-24` without spaces is a syntax error in bgpd 9.1, though its manual shows it.
|
|
115
|
-
lines.push(
|
|
116
|
-
`allow from group "${family} members" peer-as neighbor-as nexthop neighbor prefix ${any} prefixlen ${shortest} - ${longest}`
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
for (const family of families) {
|
|
121
|
-
lines.push(`deny from any prefix ${Address.prefixOf(lan[family]!)} or-longer`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
lines.push("", "# Every member gets every route the others sent, as they sent it.", "allow to any");
|
|
125
|
-
|
|
126
|
-
return lines.join("\n") + "\n";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** The families the server has an address in, each checked to be inside the peering LAN. */
|
|
130
|
-
private static families(server: RouteServer, lan: { ipv4?: Prefix; ipv6?: Prefix }): Family[] {
|
|
131
|
-
const families = (["ipv4", "ipv6"] as const).filter((family) => server[family]);
|
|
132
|
-
if (!families.length) throw new Error(`route server ${server.name} has no address on the peering LAN`);
|
|
133
|
-
|
|
134
|
-
for (const family of families) {
|
|
135
|
-
const prefix = lan[family];
|
|
136
|
-
const address = server[family]!;
|
|
137
|
-
if (!prefix || !Address.contains(prefix, address))
|
|
138
|
-
throw new Error(`${address} of route server ${server.name} is outside the peering LAN`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return families;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** A BIRD symbol: letters, digits and underscores, starting with a letter. */
|
|
145
|
-
private static protocolName(member: string, family: Family): string {
|
|
146
|
-
const name = `${member.toLowerCase().replace(/[^a-z0-9_]/g, "_")}_${family}`;
|
|
147
|
-
return /^[a-z]/.test(name) ? name : `m_${name}`;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** A member's name and AS, safe inside double quotes. */
|
|
151
|
-
private static description(member: ExchangeMember): string {
|
|
152
|
-
return `"${member.name.replace(/["\\]/g, "")}, AS${member.asn}"`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
private static birdSession(
|
|
156
|
-
name: string,
|
|
157
|
-
server: RouteServer,
|
|
158
|
-
member: ExchangeMember,
|
|
159
|
-
family: Family,
|
|
160
|
-
neighbor: string,
|
|
161
|
-
lan: Prefix
|
|
162
|
-
): string {
|
|
163
|
-
const { shortest, longest } = RouteServerConfig.LENGTHS[family];
|
|
164
|
-
|
|
165
|
-
return [
|
|
166
|
-
`protocol bgp ${name} {`,
|
|
167
|
-
` description ${RouteServerConfig.description(member)};`,
|
|
168
|
-
` local ${server[family]} as ${server.asn};`,
|
|
169
|
-
` neighbor ${neighbor} as ${member.asn};`,
|
|
170
|
-
" rs client;",
|
|
171
|
-
` ${family} {`,
|
|
172
|
-
" import keep filtered on;",
|
|
173
|
-
" import filter {",
|
|
174
|
-
` if bgp_path.first != ${member.asn} then reject;`,
|
|
175
|
-
` if bgp_next_hop != ${neighbor} then reject;`,
|
|
176
|
-
` if net ~ [ ${Address.prefixOf(lan)}+ ] then reject;`,
|
|
177
|
-
` if net.len < ${shortest} || net.len > ${longest} then reject;`,
|
|
178
|
-
" accept;",
|
|
179
|
-
" };",
|
|
180
|
-
" export all;",
|
|
181
|
-
" };",
|
|
182
|
-
"}",
|
|
183
|
-
].join("\n");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** One family's sessions: a group holding every member with an address in that family. */
|
|
187
|
-
private static openbgpdGroup(server: RouteServer, family: Family, members: readonly ExchangeMember[]): string[] {
|
|
188
|
-
const lines = [
|
|
189
|
-
`# One session per member with an ${family === "ipv4" ? "IPv4" : "IPv6"} address.`,
|
|
190
|
-
`group "${family} members" {`,
|
|
191
|
-
` local-address ${server[family]}`,
|
|
192
|
-
" # A route whose first AS is not the member's own is dropped by the rules below. The default would close the whole session.",
|
|
193
|
-
" enforce neighbor-as no",
|
|
194
|
-
];
|
|
195
|
-
|
|
196
|
-
for (const member of members) {
|
|
197
|
-
const neighbor = member[family];
|
|
198
|
-
if (!neighbor) continue;
|
|
199
|
-
|
|
200
|
-
lines.push(
|
|
201
|
-
` neighbor ${neighbor} {`,
|
|
202
|
-
` remote-as ${member.asn}`,
|
|
203
|
-
` descr ${RouteServerConfig.description(member)}`,
|
|
204
|
-
" }"
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
lines.push("}");
|
|
209
|
-
|
|
210
|
-
return lines;
|
|
211
|
-
}
|
|
212
|
-
}
|