@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Neighbor } from "../core/cabling";
|
|
2
|
-
import type { Finding } from "
|
|
3
|
-
import type { Device, Family } from "../
|
|
2
|
+
import type { Finding } from "./finding";
|
|
3
|
+
import type { Device, Family } from "../model";
|
|
4
4
|
import type { ConsoleDialect } from "../transport/console";
|
|
5
5
|
import type { WaitLimits } from "./bgp-wait";
|
|
6
6
|
import type { PlatformName } from "./catalog";
|
|
@@ -23,7 +23,7 @@ export type ApplyOptions = {
|
|
|
23
23
|
/** How a device is read. */
|
|
24
24
|
export type ReadOptions = Pick<ApplyOptions, "console">;
|
|
25
25
|
|
|
26
|
-
/** A login on a device for commands that only read, as `circuit
|
|
26
|
+
/** A login on a device for commands that only read, as `circuit exec` runs them. */
|
|
27
27
|
export type ReadSession = {
|
|
28
28
|
/**
|
|
29
29
|
* Run one command and return what it printed, without its echo or the prompt.
|
|
@@ -95,12 +95,14 @@ export abstract class Platform {
|
|
|
95
95
|
/** The name a device's `platform` field takes. */
|
|
96
96
|
abstract readonly name: PlatformName;
|
|
97
97
|
abstract readonly os: OsSupport;
|
|
98
|
-
/** File extension of
|
|
98
|
+
/** File extension of the device's config, for syntax highlighting. */
|
|
99
99
|
abstract readonly extension: string;
|
|
100
|
-
/** How the platform's serial console asks for a login and shows its prompt. Present when the platform can be read and applied through a console, `--console`, and opened with `circuit console`. */
|
|
100
|
+
/** How the platform's serial console asks for a login and shows its prompt. Present when the platform can be read and applied through a console, `--console`, and opened with `circuit access-console`. */
|
|
101
101
|
readonly console: ConsoleDialect | undefined = undefined;
|
|
102
102
|
/** Fields this platform cannot express. Validation fails on any of them, so nothing is skipped silently. */
|
|
103
103
|
abstract unsupported(device: Device): string[];
|
|
104
|
+
/** Fields this platform renders but has not proved on a device yet. Validation refuses them, unless `--experimental` asks for them, which makes each a warning. Absent on a platform with none. */
|
|
105
|
+
experimental?(device: Device): string[];
|
|
104
106
|
/** What the platform does with a config that it may not do elsewhere, each as `validate` reports it: an error stops `diff` and `apply`, a warning is reported only. Absent on a platform with none. */
|
|
105
107
|
checks?(device: Device): Finding[];
|
|
106
108
|
/** The switch rules the render makes from the device's hardware ACLs, so `validate` can hold them to the model's limits. Absent on a platform whose rules are not counted. */
|
|
@@ -109,7 +111,7 @@ export abstract class Platform {
|
|
|
109
111
|
abstract render(device: Device): string;
|
|
110
112
|
/** What the device runs now, secrets removed, in the form `render` produces where the platform allows. */
|
|
111
113
|
abstract read(device: Device, network: readonly Device[], options?: ReadOptions): Promise<string>;
|
|
112
|
-
/** The LLDP neighbors the device sees, by Circuit's port names, for `
|
|
114
|
+
/** The LLDP neighbors the device sees, by Circuit's port names, for `diff` and `apply` to compare with each port's `link`. Absent on a platform whose LLDP command and its output are not proved on a device yet. */
|
|
113
115
|
neighbors?(device: Device, network: readonly Device[], options?: ReadOptions): Promise<Neighbor[]>;
|
|
114
116
|
/** What it takes to go from `current` to `desired`. */
|
|
115
117
|
abstract plan(desired: string, current: string, options: ApplyOptions): Plan;
|
|
@@ -117,7 +119,7 @@ export abstract class Platform {
|
|
|
117
119
|
fill?(device: Device, network: readonly Device[], set: string, family: Family, prefixes: string[]): Promise<void>;
|
|
118
120
|
/** Send a plan, keeping the device reachable or restoring it. */
|
|
119
121
|
abstract apply(device: Device, network: readonly Device[], plan: Plan, options: ApplyOptions): Promise<void>;
|
|
120
|
-
/** Whether a command only reads on this platform, so `circuit
|
|
122
|
+
/** Whether a command only reads on this platform, so `circuit exec` may send it. Anything not known to only read is refused. */
|
|
121
123
|
abstract readOnly(command: string): boolean;
|
|
122
124
|
/** Log in for commands that only read, over SSH or on the console, the way the platform's read logs in. */
|
|
123
125
|
abstract session(device: Device, options?: ReadOptions): Promise<ReadSession>;
|
|
@@ -128,7 +130,7 @@ export abstract class Platform {
|
|
|
128
130
|
/** Open again these sessions, each closed on its prefix limit, read by `bgpSessions`. Only a session still closed on its limit is touched. */
|
|
129
131
|
bgpClearLimit?(session: ReadSession, sessions: readonly BgpSession[]): Promise<void>;
|
|
130
132
|
/**
|
|
131
|
-
* Checks that the device is healthy after an apply, not only that it took the commands. Called before the plan is sent, to read what the checks compare against; the CLI runs them after the apply
|
|
133
|
+
* Checks that the device is healthy after an apply, not only that it took the commands. Called before the plan is sent, to read what the checks compare against; the CLI runs them after the apply. Absent on a platform with no checks yet.
|
|
132
134
|
*/
|
|
133
135
|
health?(device: Device, plan: Plan, options?: HealthOptions): Promise<HealthCheck>;
|
|
134
136
|
}
|
|
@@ -35,6 +35,6 @@ Behaviour that looks like something else. Each one has been hit on a RAX721 runn
|
|
|
35
35
|
- A MAC access-list a port filters with cannot change: `ACL 3000 is in use, and it can not be modified or destroyed`. Each port has its own list, 3001 and up by its place in the catalog, and the plan unbinds the port, changes the list and binds it again.
|
|
36
36
|
- `no portswitch` asks for `y` like `portswitch`; a port the plan switched stays switched when it goes back to unused, which the read cannot see.
|
|
37
37
|
- OSPF BFD is `ip ospf bfd` on the VLAN interface, printed as sent and undone with `no ip ospf bfd`.
|
|
38
|
-
- A static route is watched by a BFD session: `bfd N bind peer-ip <next hop> interface vlan <V>` opens the session's view, where `session enable` starts it. It prints under `!command in bfd_template_mode`, with `session enable` indented, and goes with `no bfd N`. The route takes it as `ip route <prefix> <mask> <next hop> track bfd N` and prints `track bfd-session N`; `no ip route <prefix> <mask> <next hop>` removes the route whatever it tracks. A route takes a session number that does not exist yet. Only one session binds to a peer: a second one's `session enable` answers `Error input`, so the old session goes before the new one is made. Proved on
|
|
38
|
+
- A static route is watched by a BFD session: `bfd N bind peer-ip <next hop> interface vlan <V>` opens the session's view, where `session enable` starts it. It prints under `!command in bfd_template_mode`, with `session enable` indented, and goes with `no bfd N`. The route takes it as `ip route <prefix> <mask> <next hop> track bfd N` and prints `track bfd-session N`; `no ip route <prefix> <mask> <next hop>` removes the route whatever it tracks. A route takes a session number that does not exist yet. Only one session binds to a peer: a second one's `session enable` answers `Error input`, so the old session goes before the new one is made. Proved on switch-01, 2026-10-24.
|
|
39
39
|
- BFD timers are global, `bfd detect-multiplier N`, `bfd send-interval N` and `bfd receive-interval N`, but `send-interval` took 10 and refused 300 and 99999, so its unit and range are not known and Circuit refuses `routing.bfd` here.
|
|
40
40
|
- The caret under `Error input` marks how far the CLI took a line, so sending a whole line with a made-up ending shows whether a keyword exists, without the `?` or Tab that run the line.
|
|
@@ -6,7 +6,7 @@ import type { ClientChannel } from "ssh2";
|
|
|
6
6
|
import { Address } from "../../core/address";
|
|
7
7
|
import type { Neighbor } from "../../core/cabling";
|
|
8
8
|
import { Secrets } from "../../core/secrets";
|
|
9
|
-
import type { Device } from "../../
|
|
9
|
+
import type { Device } from "../../model";
|
|
10
10
|
import { ConsoleShell, SerialConsole, type ConsoleDialect } from "../../transport/console";
|
|
11
11
|
import { Ssh } from "../../transport/ssh";
|
|
12
12
|
import { Catalog } from "../catalog";
|
|
@@ -139,60 +139,67 @@ export class RaisecomRos extends Platform {
|
|
|
139
139
|
|
|
140
140
|
if (device.management?.interfaces) found.push("management.interfaces, which only FortiOS builds for now");
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
const interfaces = Derive.interfaces(device);
|
|
143
|
+
|
|
144
|
+
for (const { name, kind, settings } of interfaces) {
|
|
145
|
+
if (kind === "bridge") found.push(`interfaces.bridge.${name}, which only FortiOS builds for now`);
|
|
146
|
+
if (kind === "vlan_interface" && "mac" in settings && settings.mac)
|
|
147
|
+
found.push(`interfaces.vlan_interface.${name}.mac, which only RouterOS sets for now`);
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
for (const [name, port] of
|
|
148
|
-
|
|
150
|
+
for (const [name, port] of Derive.ports(device)) {
|
|
151
|
+
const where = `interfaces.ethernet.${name}`;
|
|
149
152
|
|
|
150
|
-
for (const field of ["speed", "
|
|
151
|
-
if (port[field] !== undefined) found.push(
|
|
153
|
+
for (const field of ["speed", "ingress_acl", "mtu", "addresses", "vrrp", "vrf", "ipv6_ra", "hardware_offload"] as const) {
|
|
154
|
+
if (port[field] !== undefined) found.push(`${where}.${field}`);
|
|
152
155
|
}
|
|
153
156
|
|
|
154
|
-
if (port.lldp === true) found.push(
|
|
155
|
-
if (port.
|
|
156
|
-
found.push(
|
|
157
|
+
if (port.lldp === true) found.push(`${where}.lldp true, LLDP is turned on per device`);
|
|
158
|
+
if (port.in_lag && (port.mac_limit !== undefined || port.ethertypes))
|
|
159
|
+
found.push(`${where}.mac_limit and ethertypes on a LAG member, proved on a port alone`);
|
|
157
160
|
if (port.mac_limit !== undefined && (port.mac_limit < 1 || port.mac_limit > RaisecomRosRender.MAC_LIMIT_MAX))
|
|
158
|
-
found.push(
|
|
159
|
-
if (typeof port.stp === "object" || port.stp === true) found.push(
|
|
161
|
+
found.push(`${where}.mac_limit ${port.mac_limit}, port security takes 1 to ${RaisecomRosRender.MAC_LIMIT_MAX}`);
|
|
162
|
+
if (typeof port.stp === "object" || port.stp === true) found.push(`${where}.stp other than false`);
|
|
160
163
|
const storm = port.storm_control;
|
|
161
164
|
if (storm && (storm.multicast || storm.unknown_unicast || (storm.broadcast && "percent" in storm.broadcast))) {
|
|
162
|
-
found.push(
|
|
165
|
+
found.push(`${where}.storm_control other than broadcast in pps`);
|
|
163
166
|
}
|
|
164
167
|
}
|
|
165
168
|
|
|
166
|
-
for (const [name,
|
|
167
|
-
if (
|
|
168
|
-
for (const field of ["addresses", "vrrp", "vrf", "mtu", "storm_control", "stp"] as const) {
|
|
169
|
-
if (iface[field] !== undefined) found.push(`interfaces.${name}.${field}, a LAG here only switches`);
|
|
170
|
-
}
|
|
171
|
-
for (const field of ["mac_limit", "ethertypes"] as const) {
|
|
172
|
-
if (iface[field] !== undefined) found.push(`interfaces.${name}.${field}, proved on a port alone`);
|
|
173
|
-
}
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
169
|
+
for (const [name, lag] of Object.entries(device.interfaces?.lag ?? {})) {
|
|
170
|
+
if (!lag) continue;
|
|
176
171
|
|
|
177
|
-
|
|
178
|
-
found.push(`interfaces.${name}, a
|
|
179
|
-
|
|
172
|
+
for (const field of ["addresses", "vrrp", "vrf", "mtu", "storm_control", "stp", "ipv6_ra", "hardware_offload"] as const) {
|
|
173
|
+
if (lag[field] !== undefined) found.push(`interfaces.lag.${name}.${field}, a LAG here only switches`);
|
|
174
|
+
}
|
|
175
|
+
for (const field of ["mac_limit", "ethertypes"] as const) {
|
|
176
|
+
if (lag[field] !== undefined) found.push(`interfaces.lag.${name}.${field}, proved on a port alone`);
|
|
180
177
|
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const [name, iface] of Object.entries(device.interfaces?.vlan_interface ?? {})) {
|
|
181
|
+
if (!iface) continue;
|
|
182
|
+
const where = `interfaces.vlan_interface.${name}`;
|
|
181
183
|
|
|
182
|
-
for (const field of ["vrf", "description", "mtu", "ipv6_ra"] as const) {
|
|
183
|
-
if (iface[field] !== undefined) found.push(
|
|
184
|
+
for (const field of ["vrf", "description", "mtu", "ipv6_ra", "hardware_offload"] as const) {
|
|
185
|
+
if (iface[field] !== undefined) found.push(`${where}.${field}`);
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
for (const entry of iface.addresses ?? []) {
|
|
187
189
|
const address = typeof entry === "string" ? entry : entry.address;
|
|
188
|
-
if (Address.family(address) === "ipv6") found.push(
|
|
190
|
+
if (Address.family(address) === "ipv6") found.push(`${where}.addresses ${address}, only IPv4 here`);
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
for (const group of iface.vrrp ?? []) {
|
|
192
|
-
if (Address.family(group.address) === "ipv6") found.push(
|
|
194
|
+
if (Address.family(group.address) === "ipv6") found.push(`${where}.vrrp for IPv6`);
|
|
193
195
|
}
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
// Only LAGs and VLAN interfaces are built here.
|
|
199
|
+
for (const { name, kind } of interfaces) {
|
|
200
|
+
if (kind !== "lag" && kind !== "vlan_interface") found.push(`interfaces.${kind}.${name}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
196
203
|
const routing = device.routing;
|
|
197
204
|
|
|
198
205
|
for (const field of ["bgp", "rpki"] as const) {
|
|
@@ -200,73 +207,85 @@ export class RaisecomRos extends Platform {
|
|
|
200
207
|
}
|
|
201
208
|
|
|
202
209
|
const ospf = routing?.ospf;
|
|
203
|
-
if (ospf
|
|
210
|
+
if (routing?.router_id && !ospf) found.push("routing.router_id without routing.ospf, only OSPF uses it here");
|
|
211
|
+
if (ospf?.address_families?.includes("ipv6")) found.push("routing.ospf.address_families ipv6, OSPF runs for IPv4 here");
|
|
204
212
|
|
|
205
213
|
for (const [areaId, area] of Object.entries(ospf?.areas ?? {})) {
|
|
206
|
-
for (const name of Object.
|
|
207
|
-
if (device.interfaces?.[name]
|
|
208
|
-
found.push(`routing.ospf.areas.${areaId}.${name}, OSPF runs on VLAN interfaces here`);
|
|
214
|
+
for (const [name, settings] of Object.entries(area.interfaces)) {
|
|
215
|
+
if (settings && !device.interfaces?.vlan_interface?.[name])
|
|
216
|
+
found.push(`routing.ospf.areas.${areaId}.interfaces.${name}, OSPF runs on VLAN interfaces here`);
|
|
209
217
|
}
|
|
210
218
|
}
|
|
211
219
|
|
|
212
|
-
for (const route of routing?.
|
|
213
|
-
|
|
214
|
-
|
|
220
|
+
for (const route of routing?.static_routes ?? []) {
|
|
221
|
+
const where = `routing.static_routes ${route.prefix}`;
|
|
222
|
+
|
|
223
|
+
if (!route.next_hop || route.vrf || Address.family(route.prefix) === "ipv6") {
|
|
224
|
+
found.push(`${where}, only an IPv4 next_hop in the default table`);
|
|
215
225
|
}
|
|
216
226
|
|
|
217
|
-
if (route.description || route.distance) found.push(
|
|
227
|
+
if (route.description || route.distance) found.push(`${where} description and distance`);
|
|
218
228
|
|
|
219
|
-
if (route.bfd && route.
|
|
220
|
-
found.push(
|
|
229
|
+
if (route.bfd && route.next_hop && !RaisecomRosRender.bfdSessions(device).has(route.next_hop)) {
|
|
230
|
+
found.push(`${where} bfd, the next hop must be on a VLAN interface's network`);
|
|
221
231
|
}
|
|
222
232
|
}
|
|
223
233
|
|
|
224
|
-
for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
|
|
225
|
-
if (vlan
|
|
234
|
+
for (const [name, vlan] of Object.entries(device.switching?.vlans ?? {})) {
|
|
235
|
+
if (vlan?.description) found.push(`switching.vlans.${name}.description`);
|
|
226
236
|
}
|
|
227
237
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
"
|
|
233
|
-
"
|
|
234
|
-
"
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
238
|
+
// Address sets alone build nothing: `management.allow` may name one, and the project has put its addresses there.
|
|
239
|
+
const { address_sets: _addressSets, ...firewall } = device.firewall ?? {};
|
|
240
|
+
|
|
241
|
+
const UNSUPPORTED_GROUPS: [string, unknown][] = [
|
|
242
|
+
["firewall", Object.keys(firewall).length ? firewall : undefined],
|
|
243
|
+
["services.dhcp_server", device.services?.dhcp_server],
|
|
244
|
+
["services.dhcp_relay", device.services?.dhcp_relay],
|
|
245
|
+
["switching.acls", device.switching?.acls],
|
|
246
|
+
["routing.policies", routing?.policies],
|
|
247
|
+
["routing.prefix_sets", routing?.prefix_sets],
|
|
248
|
+
["system.hardware_offload", device.system?.hardware_offload],
|
|
249
|
+
["system.certificates", device.system?.certificates],
|
|
250
|
+
["routing.vrfs", routing?.vrfs],
|
|
251
|
+
["monitoring.flow_export", device.monitoring?.flow_export],
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
for (const [path, value] of UNSUPPORTED_GROUPS) {
|
|
255
|
+
if (value !== undefined) found.push(path);
|
|
243
256
|
}
|
|
244
257
|
|
|
245
258
|
const management = device.management ?? {};
|
|
246
259
|
|
|
247
|
-
for (const field of ["http", "https", "api", "api_tls", "ftp", "
|
|
260
|
+
for (const field of ["http", "https", "api", "api_tls", "ftp", "native_protocol", "console"] as const) {
|
|
248
261
|
if (management[field]) found.push(`management.${field}`);
|
|
249
262
|
}
|
|
250
263
|
|
|
251
|
-
|
|
264
|
+
const snmp = device.monitoring?.snmp;
|
|
265
|
+
|
|
266
|
+
for (const [name, user] of Object.entries(snmp?.users ?? {})) {
|
|
252
267
|
if (!RaisecomRosRender.SNMP_AUTH[user.auth])
|
|
253
|
-
found.push(`
|
|
268
|
+
found.push(`monitoring.snmp.users.${name}.auth ${user.auth}, the platform authenticates with SHA1 or MD5`);
|
|
254
269
|
}
|
|
255
270
|
|
|
256
|
-
if (
|
|
271
|
+
if (snmp?.contact || snmp?.location) found.push("monitoring.snmp.contact and location");
|
|
257
272
|
if (management.ssh?.weak_crypto) found.push("management.ssh.weak_crypto");
|
|
258
273
|
|
|
259
|
-
|
|
260
|
-
if (
|
|
274
|
+
const switching = device.switching;
|
|
275
|
+
if (switching?.lldp && switching.lldp !== true) found.push("switching.lldp.interfaces, LLDP is per device");
|
|
276
|
+
if (switching?.stp?.mode && switching.stp.mode !== "mstp") found.push("switching.stp.mode other than mstp");
|
|
261
277
|
|
|
262
|
-
for (const [name, user] of Object.entries(device.users ?? {})) {
|
|
263
|
-
if (user.role !== "admin") found.push(`users.${name}.role other than admin`);
|
|
278
|
+
for (const [name, user] of Object.entries(device.system?.users ?? {})) {
|
|
279
|
+
if (user && user.role !== "admin") found.push(`system.users.${name}.role other than admin`);
|
|
264
280
|
}
|
|
265
281
|
|
|
266
282
|
const system = device.system ?? {};
|
|
283
|
+
const services = device.services ?? {};
|
|
267
284
|
|
|
268
|
-
if (system.dns || system.logging?.local || system.release_channel || system.banner ||
|
|
269
|
-
found.push(
|
|
285
|
+
if (system.dns || system.logging?.local || system.release_channel || system.banner || services.dns_server || services.ntp_server) {
|
|
286
|
+
found.push(
|
|
287
|
+
"system.dns, system.logging.local, system.release_channel, system.banner, services.dns_server and services.ntp_server"
|
|
288
|
+
);
|
|
270
289
|
}
|
|
271
290
|
|
|
272
291
|
// SNMP takes its own access-list; SSH and telnet share the management one.
|
|
@@ -682,7 +701,7 @@ export class RaisecomRos extends Platform {
|
|
|
682
701
|
const opened = await SerialConsole.open(console);
|
|
683
702
|
|
|
684
703
|
try {
|
|
685
|
-
await SerialConsole.login(opened.channel, RaisecomRos.CONSOLE, user, password, console.
|
|
704
|
+
await SerialConsole.login(opened.channel, RaisecomRos.CONSOLE, user, password, console.password_only);
|
|
686
705
|
} catch (error) {
|
|
687
706
|
opened.close();
|
|
688
707
|
throw error;
|
|
@@ -695,7 +714,7 @@ export class RaisecomRos extends Platform {
|
|
|
695
714
|
opened.close();
|
|
696
715
|
},
|
|
697
716
|
fresh: async () => {
|
|
698
|
-
await SerialConsole.login(opened.channel, RaisecomRos.CONSOLE, user, password, console.
|
|
717
|
+
await SerialConsole.login(opened.channel, RaisecomRos.CONSOLE, user, password, console.password_only);
|
|
699
718
|
await Ssh.send(opened.channel, "terminal page-break disable", 15_000, 1000);
|
|
700
719
|
return session;
|
|
701
720
|
},
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { Address } from "../../core/address";
|
|
2
2
|
import { Secrets } from "../../core/secrets";
|
|
3
3
|
import { Utils } from "../../core/utils";
|
|
4
|
-
import type { Device, Ethertype, Port, Prefix, Severity, SnmpUser, Switched, Vrrp } from "../../
|
|
4
|
+
import type { Device, Ethertype, Port, Prefix, Severity, SnmpUser, Switched, Vrrp } from "../../model";
|
|
5
5
|
import { Catalog } from "../catalog";
|
|
6
6
|
import { Derive } from "../derive";
|
|
7
7
|
|
|
8
|
-
type AnyPort = Port<string, string, string, string>;
|
|
9
|
-
|
|
10
8
|
/** The configuration a Raisecom ROS device should run. */
|
|
11
9
|
export class RaisecomRosRender {
|
|
12
10
|
/** The access-list that guards every management service. */
|
|
@@ -63,18 +61,18 @@ export class RaisecomRosRender {
|
|
|
63
61
|
/** The whole configuration of a device. */
|
|
64
62
|
static of(device: Device): string {
|
|
65
63
|
const lines: string[] = [];
|
|
66
|
-
const vlans = device.vlans ?? {};
|
|
64
|
+
const vlans = device.switching?.vlans ?? {};
|
|
67
65
|
const vlanId = (name: string) => vlans[name]!.id;
|
|
68
66
|
const management = device.management ?? {};
|
|
69
|
-
const allow =
|
|
67
|
+
const allow = RaisecomRosRender.managementAllow(device);
|
|
70
68
|
|
|
71
|
-
const ids = Object.values(vlans).
|
|
69
|
+
const ids = Object.values(vlans).flatMap((vlan) => (vlan ? [vlan.id] : []));
|
|
72
70
|
if (ids.length) lines.push(`create vlan ${RaisecomRosRender.vlanList(ids)} active`, "!");
|
|
73
71
|
|
|
74
72
|
const lagNumbers = Derive.lagIds(device);
|
|
75
73
|
|
|
76
|
-
for (const [name, lag] of Object.entries(device.interfaces ?? {})) {
|
|
77
|
-
if (lag
|
|
74
|
+
for (const [name, lag] of Object.entries(device.interfaces?.lag ?? {})) {
|
|
75
|
+
if (!lag) continue;
|
|
78
76
|
|
|
79
77
|
lines.push(`interface port-channel ${lagNumbers.get(name)}`);
|
|
80
78
|
if (lag.description) lines.push(`description ${lag.description}`);
|
|
@@ -83,14 +81,15 @@ export class RaisecomRosRender {
|
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
if (allow.length) lines.push(...RaisecomRosRender.accessList(RaisecomRosRender.MANAGEMENT_ACL, "management", allow));
|
|
86
|
-
const
|
|
84
|
+
const snmp = device.monitoring?.snmp;
|
|
85
|
+
const snmpSources = snmp?.allow;
|
|
87
86
|
if (snmpSources) lines.push(...RaisecomRosRender.accessList(RaisecomRosRender.SNMP_ACL, "snmp", snmpSources));
|
|
88
87
|
|
|
89
88
|
const ports = Catalog.ports(device.platform, device.model);
|
|
90
89
|
|
|
91
90
|
// Each port that takes only some kinds of frame has a MAC access-list of its own, so changing one releases only that port.
|
|
92
91
|
ports.forEach((port, index) => {
|
|
93
|
-
const ethertypes = device.
|
|
92
|
+
const ethertypes = device.interfaces?.ethernet?.[port]?.ethertypes;
|
|
94
93
|
if (!ethertypes) return;
|
|
95
94
|
|
|
96
95
|
lines.push(`access-list ${RaisecomRosRender.ethertypeAcl(index)}`);
|
|
@@ -100,8 +99,8 @@ export class RaisecomRosRender {
|
|
|
100
99
|
lines.push(" rule 100 deny any any", "!");
|
|
101
100
|
});
|
|
102
101
|
|
|
103
|
-
for (const [name, user] of Object.entries(device.users ?? {}))
|
|
104
|
-
lines.push(`user name ${name} password ${Secrets.marker(user.password)}`);
|
|
102
|
+
for (const [name, user] of Object.entries(device.system?.users ?? {}))
|
|
103
|
+
if (user) lines.push(`user name ${name} password ${Secrets.marker(user.password)}`);
|
|
105
104
|
if (management.privilege_password) lines.push(`enable password ${Secrets.marker(management.privilege_password)}`);
|
|
106
105
|
lines.push(`hostname ${device.name}`);
|
|
107
106
|
|
|
@@ -115,7 +114,7 @@ export class RaisecomRosRender {
|
|
|
115
114
|
lines.push("!");
|
|
116
115
|
|
|
117
116
|
ports.forEach((port, index) => {
|
|
118
|
-
const settings = device.
|
|
117
|
+
const settings = device.interfaces?.ethernet?.[port];
|
|
119
118
|
lines.push(
|
|
120
119
|
`interface ${Catalog.spell(device.platform, device.model, port)}`,
|
|
121
120
|
...RaisecomRosRender.physicalPort(settings, vlanId, lagNumbers),
|
|
@@ -126,8 +125,8 @@ export class RaisecomRosRender {
|
|
|
126
125
|
|
|
127
126
|
const ospf = device.routing?.ospf;
|
|
128
127
|
|
|
129
|
-
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
130
|
-
if (iface
|
|
128
|
+
for (const [name, iface] of Object.entries(device.interfaces?.vlan_interface ?? {})) {
|
|
129
|
+
if (!iface) continue;
|
|
131
130
|
|
|
132
131
|
lines.push(`interface vlan ${vlanId(iface.vlan)}`);
|
|
133
132
|
|
|
@@ -153,7 +152,9 @@ export class RaisecomRosRender {
|
|
|
153
152
|
lines.push(`router ospf ${RaisecomRosRender.OSPF_PROCESS}${routerId ? ` router-id ${routerId}` : ""}`);
|
|
154
153
|
|
|
155
154
|
for (const [areaId, area] of Object.entries(ospf.areas)) {
|
|
156
|
-
for (const iface of Object.
|
|
155
|
+
for (const [iface, settings] of Object.entries(area.interfaces)) {
|
|
156
|
+
if (!settings) continue;
|
|
157
|
+
|
|
157
158
|
for (const prefix of Derive.ipv4Networks(device, iface)) {
|
|
158
159
|
lines.push(`network ${Address.host(prefix)} ${Address.wildcard(Address.split(prefix).len)} area ${areaId}`);
|
|
159
160
|
}
|
|
@@ -165,18 +166,18 @@ export class RaisecomRosRender {
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
// Storm control acts only once detection is on.
|
|
168
|
-
if (
|
|
169
|
-
if (device.
|
|
169
|
+
if (Derive.ports(device).some(([, port]) => port.storm_control)) lines.push("storm-control detection enable");
|
|
170
|
+
if (device.monitoring?.transceivers) lines.push("transceiver ddm enable");
|
|
170
171
|
// LLDP and spanning tree are off by default and turned on by the config.
|
|
171
|
-
|
|
172
|
-
if (
|
|
172
|
+
const switching = device.switching;
|
|
173
|
+
if (switching?.lldp) lines.push("lldp enable");
|
|
174
|
+
if (switching?.stp) {
|
|
173
175
|
lines.push("spanning-tree enable");
|
|
174
|
-
if (
|
|
176
|
+
if (switching.stp.priority !== undefined) lines.push(`spanning-tree priority ${switching.stp.priority}`);
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
// The factory communities survive a reset, and one of them can write, so they are always removed.
|
|
178
180
|
lines.push("no snmp-server community public", "no snmp-server community private");
|
|
179
|
-
const snmp = management.snmp;
|
|
180
181
|
if (snmp?.community) lines.push(`snmp-server community ${Secrets.marker(snmp.community)} ro`);
|
|
181
182
|
|
|
182
183
|
const snmpUsers = Object.entries(snmp?.users ?? {});
|
|
@@ -201,12 +202,12 @@ export class RaisecomRosRender {
|
|
|
201
202
|
lines.push(`bfd ${session.id} bind peer-ip ${via} interface vlan ${vlanId(session.vlan)}`, "session enable", "!");
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
for (const route of device.routing?.
|
|
205
|
+
for (const route of device.routing?.static_routes ?? []) {
|
|
205
206
|
const { addr, len } = Address.split(route.prefix);
|
|
206
|
-
if (Address.family(addr) !== "ipv4" || !route.
|
|
207
|
+
if (Address.family(addr) !== "ipv4" || !route.next_hop) continue;
|
|
207
208
|
|
|
208
|
-
const session = route.bfd ? sessions.get(route.
|
|
209
|
-
lines.push(`ip route ${addr} ${Address.netmask(len)} ${route.
|
|
209
|
+
const session = route.bfd ? sessions.get(route.next_hop) : undefined;
|
|
210
|
+
lines.push(`ip route ${addr} ${Address.netmask(len)} ${route.next_hop}${session ? ` track bfd ${session.id}` : ""}`);
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
if (management.telnet) {
|
|
@@ -222,7 +223,7 @@ export class RaisecomRosRender {
|
|
|
222
223
|
const ntp = device.system?.ntp;
|
|
223
224
|
if (ntp?.servers.length) lines.push("ntp enable", ...ntp.servers.map((server) => `ntp server ${server}`));
|
|
224
225
|
|
|
225
|
-
for (const host of device.
|
|
226
|
+
for (const host of device.monitoring?.syslog ?? []) {
|
|
226
227
|
const level = RaisecomRosRender.LOG_LEVELS[host.level];
|
|
227
228
|
const port = host.port ?? RaisecomRosRender.LOG_DEFAULTS.port;
|
|
228
229
|
let line = `logging host ${host.address}`;
|
|
@@ -234,6 +235,14 @@ export class RaisecomRosRender {
|
|
|
234
235
|
return lines.join("\n") + "\n";
|
|
235
236
|
}
|
|
236
237
|
|
|
238
|
+
/** The sources every management service takes. The project has already turned an address set's name here into the set's addresses. */
|
|
239
|
+
private static managementAllow(device: Device): readonly Prefix[] {
|
|
240
|
+
const allow = device.management?.allow ?? [];
|
|
241
|
+
if (typeof allow === "string")
|
|
242
|
+
throw new Error(`${device.name}: management.allow is still the address set ${allow}, not its addresses`);
|
|
243
|
+
return allow;
|
|
244
|
+
}
|
|
245
|
+
|
|
237
246
|
/** An access-list permitting `sources`, then denying the rest. Its description takes one word. */
|
|
238
247
|
private static accessList(number: number, description: string, sources: readonly Prefix[]): string[] {
|
|
239
248
|
const rules = sources.map(
|
|
@@ -245,7 +254,7 @@ export class RaisecomRosRender {
|
|
|
245
254
|
}
|
|
246
255
|
|
|
247
256
|
/** Access or trunk membership, for a port or a LAG. */
|
|
248
|
-
private static switching(settings: Switched
|
|
257
|
+
private static switching(settings: Switched, vlanId: (name: string) => number): string[] {
|
|
249
258
|
const lines: string[] = [];
|
|
250
259
|
if (settings.access_vlan) lines.push(`switchport access vlan ${vlanId(settings.access_vlan)}`);
|
|
251
260
|
|
|
@@ -271,16 +280,12 @@ export class RaisecomRosRender {
|
|
|
271
280
|
}
|
|
272
281
|
|
|
273
282
|
/** A port the config does not declare is shut down. A LAG member holds its membership and description; the LAG holds the rest. */
|
|
274
|
-
private static physicalPort(
|
|
275
|
-
settings: AnyPort | undefined,
|
|
276
|
-
vlanId: (name: string) => number,
|
|
277
|
-
lagNumbers: Map<string, number>
|
|
278
|
-
): string[] {
|
|
283
|
+
private static physicalPort(settings: Port | undefined, vlanId: (name: string) => number, lagNumbers: Map<string, number>): string[] {
|
|
279
284
|
if (!settings) return ["shutdown"];
|
|
280
285
|
|
|
281
286
|
const lines: string[] = [];
|
|
282
287
|
if (settings.description) lines.push(`description ${settings.description}`);
|
|
283
|
-
if (settings.
|
|
288
|
+
if (settings.in_lag) return [...lines, `port-channel ${lagNumbers.get(settings.in_lag)}`];
|
|
284
289
|
|
|
285
290
|
lines.push(...RaisecomRosRender.switching(settings, vlanId));
|
|
286
291
|
|
|
@@ -299,29 +304,28 @@ export class RaisecomRosRender {
|
|
|
299
304
|
return lines;
|
|
300
305
|
}
|
|
301
306
|
|
|
302
|
-
/** The MAC access-list of the port at `index` in the catalog, for its `ethertypes`. MAC access-lists are 3000 and up. */
|
|
303
307
|
/** The BFD session for each next hop a static route watches, numbered from 1 in the order the routes come, with the VLAN its interface is on. A next hop on no VLAN interface's network has none. */
|
|
304
308
|
static bfdSessions(device: Device): Map<string, { id: number; vlan: string }> {
|
|
305
309
|
const sessions = new Map<string, { id: number; vlan: string }>();
|
|
306
310
|
|
|
307
|
-
for (const route of device.routing?.
|
|
308
|
-
|
|
311
|
+
for (const route of device.routing?.static_routes ?? []) {
|
|
312
|
+
const nextHop = route.next_hop;
|
|
313
|
+
if (!route.bfd || !nextHop || sessions.has(nextHop)) continue;
|
|
309
314
|
|
|
310
|
-
const holder = Object.values(device.interfaces ?? {}).find(
|
|
311
|
-
(iface) =>
|
|
312
|
-
|
|
313
|
-
(
|
|
314
|
-
|
|
315
|
-
return Address.family(address) === "ipv4" && Address.contains(Address.prefixOf(address), route.via!);
|
|
316
|
-
})
|
|
315
|
+
const holder = Object.values(device.interfaces?.vlan_interface ?? {}).find((iface) =>
|
|
316
|
+
(iface?.addresses ?? []).some((entry) => {
|
|
317
|
+
const address = typeof entry === "string" ? entry : entry.address;
|
|
318
|
+
return Address.family(address) === "ipv4" && Address.contains(Address.prefixOf(address), nextHop);
|
|
319
|
+
})
|
|
317
320
|
);
|
|
318
321
|
|
|
319
|
-
if (holder
|
|
322
|
+
if (holder) sessions.set(nextHop, { id: sessions.size + 1, vlan: holder.vlan });
|
|
320
323
|
}
|
|
321
324
|
|
|
322
325
|
return sessions;
|
|
323
326
|
}
|
|
324
327
|
|
|
328
|
+
/** The MAC access-list of the port at `index` in the catalog, for its `ethertypes`. MAC access-lists are 3000 and up. */
|
|
325
329
|
static ethertypeAcl(index: number): number {
|
|
326
330
|
return 3001 + index;
|
|
327
331
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Behaviour that looks like something else. Each one has been hit on a real device.
|
|
4
4
|
|
|
5
5
|
- Every command takes effect as it arrives; there is no candidate configuration. `apply` saves a backup, arms a scheduler that restores it, and restores at once if a command is refused.
|
|
6
|
-
- A `/32` address gives no connected route, so a session over it never opens. Give the far end: `{ address,
|
|
6
|
+
- A `/32` address gives no connected route, so a session over it never opens. Give the far end: `{ address, peer_address }` renders `network=<peer_address>`.
|
|
7
7
|
- `[find address=X/Y]` unquoted matches nothing, silently. Every selector the plan builds is quoted.
|
|
8
8
|
- `~` in `find` has no anchors. Match exactly.
|
|
9
9
|
- A property of the same group is printed as a bare suffix, `output.filter-chain=X .network=Y`, which only reads back inside that command. The parser expands it before building a `set`.
|
|
@@ -64,8 +64,8 @@ Behaviour that looks like something else. Each one has been hit on a real device
|
|
|
64
64
|
- No MAC learning limit on 7.23.5: `/interface bridge port` has `learn` but no limit, and `/interface ethernet switch port` has none (CCR2216). `mac_limit` stays refused.
|
|
65
65
|
- A switch rule matches a kind of frame with `mac-protocol` (`ip`, `ipv6`, `arp` among 23 kinds), but cannot match "not" one. An ethertype filter is a rule that lets each kind through and a last one with an empty `new-dst-ports` for the rest, after the port's ACL rules, which match only `ip` or `ipv6`. Four such rules on a free port took the router to 70 switch rules, none invalid (CCR2216, 7.23.5).
|
|
66
66
|
- A routing table holds no routes the router forwards by unless it has `fib`: `/routing table add fib name=...`. The export prints the flag between the properties, `add comment=... fib name=...`.
|
|
67
|
-
- A BGP connection's `routing-table` is where the session installs what it learns and also where it announces from: an `output.network` prefix is announced only while that table has a route for it, and a route in main alone is not enough. On a CCR2216 on 7.23.5, a fresh session with `routing-table=<table>` installed its routes there, active, with the next hop resolved through main, and announced nothing until the table had the prefix too; then it did. Circuit's validate refuses a session in a table that holds none of `
|
|
68
|
-
- A running session takes a new `routing-table` only when it restarts, but its announcements stop at once: set from main to a table, it withdrew everything within seconds, kept its learned routes in main, and announced nothing even once the table had the prefix; set back to main, it still announced nothing until it restarted. `/routing bgp session resend afi=ipv6` did not help either, on the router where this was found (#157). Disabling and enabling the connection restarted it in the new table. The plan does exactly that, written on the plan's line, and after an apply Circuit checks that every session still announces what it announced of `
|
|
67
|
+
- A BGP connection's `routing-table` is where the session installs what it learns and also where it announces from: an `output.network` prefix is announced only while that table has a route for it, and a route in main alone is not enough. On a CCR2216 on 7.23.5, a fresh session with `routing-table=<table>` installed its routes there, active, with the next hop resolved through main, and announced nothing until the table had the prefix too; then it did. Circuit's validate refuses a session in a table that holds none of `originated_prefixes`, and warns for each it lacks. Routing filters cannot move a route to a table: `set routing-table` and `set table` are `invalid argument`.
|
|
68
|
+
- A running session takes a new `routing-table` only when it restarts, but its announcements stop at once: set from main to a table, it withdrew everything within seconds, kept its learned routes in main, and announced nothing even once the table had the prefix; set back to main, it still announced nothing until it restarted. `/routing bgp session resend afi=ipv6` did not help either, on the router where this was found (#157). Disabling and enabling the connection restarted it in the new table. The plan does exactly that, written on the plan's line, and after an apply Circuit checks that every session still announces what it announced of `originated_prefixes` before (CCR2216, 7.23.5).
|
|
69
69
|
- A passive connection without `local.address` listens only on its default local address, `local.default-address`, which may be a public one: a peer connecting to another address of the router gets a TCP reset (CCR2216, 7.23.5).
|
|
70
70
|
- `/routing rule` is an ordered list without names, like a filter chain, and the plan rewrites it whole in the config's order. `lookup-only-in-table` drops a packet its table has no route for (a ping printed `no route`); `lookup` routes it by main instead (the ping left). An interface to match is `interface=`. A `drop` or `unreachable` rule is printed with `!realm`, which Circuit does not write (CCR2216, 7.23.5).
|
|
71
71
|
- An eBGP connection to an address on no connected network, without `multihop=yes`, takes the TCP connection, logs `connection <local> from <remote>`, and closes it with no OPEN and nothing else in the log, even with `bgp,debug` logging on (CCR2216, 7.23.5).
|