@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
|
@@ -10,30 +10,29 @@ import type {
|
|
|
10
10
|
Ethertype,
|
|
11
11
|
Family,
|
|
12
12
|
FilterMatch,
|
|
13
|
-
|
|
13
|
+
Lag,
|
|
14
14
|
NatRule,
|
|
15
15
|
Neighbor,
|
|
16
16
|
NeighborSettings,
|
|
17
17
|
PolicyRule,
|
|
18
18
|
Port,
|
|
19
19
|
Ports,
|
|
20
|
+
Prefix,
|
|
21
|
+
RouteRule,
|
|
20
22
|
Service,
|
|
21
23
|
Severity,
|
|
22
|
-
|
|
24
|
+
VlanInterface,
|
|
25
|
+
} from "../../model";
|
|
23
26
|
import { Catalog } from "../catalog";
|
|
27
|
+
import { Derive, type NamedInterface } from "../derive";
|
|
24
28
|
|
|
25
29
|
/** A property value as a command takes it. `undefined` and `false` leave the property out. */
|
|
26
30
|
export type Value = string | number | undefined | false;
|
|
27
31
|
|
|
28
|
-
export type AnyPort = Port<string, string, string, string>;
|
|
29
|
-
|
|
30
32
|
/** One switch rule: the port it is on, the ACL rule it comes from, and the one source and destination port or range it takes. */
|
|
31
|
-
export type SwitchRule = { port: string; rule: AclRule
|
|
32
|
-
export type AnyInterface = Interface<string, string>;
|
|
33
|
+
export type SwitchRule = { port: string; rule: AclRule; srcPort?: string; dstPort?: string };
|
|
33
34
|
|
|
34
|
-
type Speed = NonNullable<
|
|
35
|
-
type AnyRule = PolicyRule<string, string>;
|
|
36
|
-
type AnyNeighbor = Neighbor<string, string, string>;
|
|
35
|
+
type Speed = NonNullable<Port["speed"]>;
|
|
37
36
|
|
|
38
37
|
/** One device's render: the device, and the commands collected under their menu. */
|
|
39
38
|
export class RouterOsRender {
|
|
@@ -85,7 +84,7 @@ export class RouterOsRender {
|
|
|
85
84
|
"rs-client": "ebgp-rs",
|
|
86
85
|
} as const;
|
|
87
86
|
|
|
88
|
-
/** Neutral service name to the platform's. `
|
|
87
|
+
/** Neutral service name to the platform's. `native_protocol` is the platform's own desktop client. */
|
|
89
88
|
private static readonly SERVICES = {
|
|
90
89
|
ftp: "ftp",
|
|
91
90
|
ssh: "ssh",
|
|
@@ -94,12 +93,11 @@ export class RouterOsRender {
|
|
|
94
93
|
https: "www-ssl",
|
|
95
94
|
api: "api",
|
|
96
95
|
api_tls: "api-ssl",
|
|
97
|
-
|
|
96
|
+
native_protocol: "winbox",
|
|
98
97
|
} as const;
|
|
99
98
|
|
|
100
99
|
private static readonly USER_GROUPS = { admin: "full", operator: "write", "read-only": "read" } as const;
|
|
101
100
|
|
|
102
|
-
private static readonly SNMP_AUTH = { sha1: "SHA1", sha256: "SHA1" } as const;
|
|
103
101
|
private static readonly SNMP_PRIVACY = { aes128: "AES", des: "DES" } as const;
|
|
104
102
|
|
|
105
103
|
/** Severity threshold to the topics the platform logs under, most severe first. */
|
|
@@ -112,17 +110,41 @@ export class RouterOsRender {
|
|
|
112
110
|
];
|
|
113
111
|
private static readonly SEVERITY_ORDER: Severity[] = ["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"];
|
|
114
112
|
|
|
115
|
-
private readonly ports: Record<string,
|
|
116
|
-
|
|
113
|
+
private readonly ports: Record<string, Port>;
|
|
114
|
+
|
|
115
|
+
/** Every interface that is not a port, kind by kind. */
|
|
116
|
+
private readonly interfaces: NamedInterface[];
|
|
117
117
|
|
|
118
118
|
/** Commands under their menu. */
|
|
119
119
|
private readonly menus = new Map<string, string[]>();
|
|
120
120
|
|
|
121
121
|
private constructor(private readonly device: Device) {
|
|
122
|
-
this.ports = Object.fromEntries(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
this.ports = Object.fromEntries(Derive.ports(device));
|
|
123
|
+
this.interfaces = Derive.interfaces(device);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The entries of a map keyed by name, without the names whose value is left out. */
|
|
127
|
+
static entries<V>(map: { readonly [name: string]: V | undefined } | undefined): [string, V][] {
|
|
128
|
+
return Object.entries(map ?? {}).flatMap(([name, value]) => (value === undefined ? [] : [[name, value] as [string, V]]));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A value of a match: the one written, or the only one of a list. `unsupported()` refuses a list of more. */
|
|
132
|
+
static one<T>(value: T | readonly T[] | undefined): T | undefined {
|
|
133
|
+
return Array.isArray(value) ? (value as readonly T[])[0] : (value as T | undefined);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A route policy rule on routes: not a call, and not a `{ communities }` rule, which `unsupported()` refuses. */
|
|
137
|
+
static onRoutes(rule: PolicyRule): rule is RouteRule {
|
|
138
|
+
return rule.call === undefined && rule.communities === undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A prefix set's prefixes, written as a list or as a map of named prefixes. Undefined for a set fetched from a registry. */
|
|
142
|
+
static prefixesOf(device: Device, set: string): readonly Prefix[] | undefined {
|
|
143
|
+
const declared = device.routing?.prefix_sets?.[set];
|
|
144
|
+
if (!declared || !("prefixes" in declared)) return undefined;
|
|
145
|
+
|
|
146
|
+
const prefixes = declared.prefixes;
|
|
147
|
+
return Array.isArray(prefixes) ? prefixes : Object.values(prefixes);
|
|
126
148
|
}
|
|
127
149
|
|
|
128
150
|
/** The whole configuration the device should run. */
|
|
@@ -261,8 +283,8 @@ export class RouterOsRender {
|
|
|
261
283
|
}
|
|
262
284
|
|
|
263
285
|
/** The conditions of a rule, as the platform's filter language writes them. */
|
|
264
|
-
private static conditions(rule:
|
|
265
|
-
const match = rule.
|
|
286
|
+
private static conditions(rule: RouteRule): string[] {
|
|
287
|
+
const match = rule.if ?? {};
|
|
266
288
|
const tests: string[] = [];
|
|
267
289
|
if (match.prefix) tests.push(`dst==${match.prefix}`);
|
|
268
290
|
if (match.prefix_set) tests.push(`dst in ${match.prefix_set}`);
|
|
@@ -271,25 +293,25 @@ export class RouterOsRender {
|
|
|
271
293
|
if (match.as_path_length?.min !== undefined) tests.push(`bgp-path-len>${match.as_path_length.min - 1}`);
|
|
272
294
|
if (match.as_path_length?.max !== undefined) tests.push(`bgp-path-len<${match.as_path_length.max + 1}`);
|
|
273
295
|
// The platform's AS path regexp matches each AS as a whole number, so a bare number is that AS anywhere in the path.
|
|
274
|
-
if (match.
|
|
296
|
+
if (match.as_path_contains !== undefined) tests.push(`bgp-as-path ${match.as_path_contains}`);
|
|
275
297
|
if (match.rpki) tests.push(`rpki ${match.rpki}`);
|
|
276
|
-
if (match.
|
|
298
|
+
if (match.standard_community) tests.push(`bgp-communities includes ${match.standard_community}`);
|
|
277
299
|
if (match.large_community) tests.push(`bgp-large-communities includes ${match.large_community}`);
|
|
278
300
|
return tests;
|
|
279
301
|
}
|
|
280
302
|
|
|
281
303
|
/** What a rule does to a matching route, then its decision. */
|
|
282
|
-
private static actions(rule:
|
|
304
|
+
private static actions(rule: RouteRule, family: Family): string[] {
|
|
283
305
|
const set = rule.set ?? {};
|
|
284
306
|
const steps: string[] = [];
|
|
285
307
|
if (set.local_pref !== undefined) steps.push(`set bgp-local-pref ${set.local_pref}`);
|
|
286
308
|
if (set.med !== undefined) steps.push(`set bgp-med ${set.med}`);
|
|
287
309
|
if (set.prepend !== undefined) steps.push(`set bgp-path-prepend ${set.prepend}`);
|
|
288
|
-
for (const pattern of set.
|
|
310
|
+
for (const pattern of set.remove_standard_communities ?? []) {
|
|
289
311
|
steps.push(`delete bgp-communities regexp "${RouterOsRender.communityPattern(pattern)}"`);
|
|
290
312
|
}
|
|
291
|
-
if (set.
|
|
292
|
-
if (set.
|
|
313
|
+
if (set.standard_communities) steps.push(`set bgp-communities ${set.standard_communities.join(",")}`);
|
|
314
|
+
if (set.add_standard_communities) steps.push(`append bgp-communities ${set.add_standard_communities.join(",")}`);
|
|
293
315
|
|
|
294
316
|
for (const pattern of set.remove_large_communities ?? []) {
|
|
295
317
|
steps.push(`delete bgp-large-communities regexp "${RouterOsRender.communityPattern(pattern)}"`);
|
|
@@ -299,18 +321,21 @@ export class RouterOsRender {
|
|
|
299
321
|
if (set.add_large_communities) steps.push(`append bgp-large-communities ${set.add_large_communities.join(",")}`);
|
|
300
322
|
// An address only means something to routes of its own family.
|
|
301
323
|
if (set.next_hop && Address.family(set.next_hop) === family) steps.push(`set gw ${set.next_hop}`);
|
|
302
|
-
|
|
324
|
+
const preferredSource = set.preferred_source && Address.host(set.preferred_source);
|
|
325
|
+
if (preferredSource && Address.family(preferredSource) === family) steps.push(`set pref-src ${preferredSource}`);
|
|
303
326
|
if (set.blackhole) steps.push("set blackhole yes");
|
|
304
|
-
if (rule.
|
|
327
|
+
if (rule.then) steps.push(rule.then);
|
|
305
328
|
return steps;
|
|
306
329
|
}
|
|
307
330
|
|
|
308
331
|
/** One rule as the platform writes it: hand off, ask the validator, test, change, decide. */
|
|
309
|
-
private static ruleText(rule:
|
|
332
|
+
private static ruleText(rule: PolicyRule, family: Family): string {
|
|
333
|
+
if (rule.call) return `jump ${RouterOsRender.chainName(rule.call, family)}`;
|
|
334
|
+
if (!RouterOsRender.onRoutes(rule)) return "";
|
|
335
|
+
|
|
310
336
|
const parts: string[] = [];
|
|
311
|
-
if (rule.call) parts.push(`jump ${RouterOsRender.chainName(rule.call, family)}`);
|
|
312
337
|
// An RPKI state is only known once the validator has been asked, per route, so the question goes on the same rule.
|
|
313
|
-
if (rule.
|
|
338
|
+
if (rule.if?.rpki) parts.push(`rpki-verify ${RouterOsRender.RPKI_GROUP}`);
|
|
314
339
|
|
|
315
340
|
const tests = RouterOsRender.conditions(rule);
|
|
316
341
|
const steps = RouterOsRender.actions(rule, family);
|
|
@@ -353,49 +378,34 @@ export class RouterOsRender {
|
|
|
353
378
|
|
|
354
379
|
/** What the platform calls a port or an interface. A port is spelled by the model; an interface keeps its config name. */
|
|
355
380
|
private name(portOrInterface: string): string {
|
|
356
|
-
if (
|
|
381
|
+
if (this.interfaces.some((entry) => entry.name === portOrInterface)) return portOrInterface;
|
|
357
382
|
|
|
358
383
|
return Catalog.spell(this.device.platform, this.device.model, portOrInterface);
|
|
359
384
|
}
|
|
360
385
|
|
|
361
386
|
/** The number of a VLAN, by its config name. */
|
|
362
387
|
private vlanId(vlan: string): number {
|
|
363
|
-
const declared = this.device.vlans?.[vlan];
|
|
388
|
+
const declared = this.device.switching?.vlans?.[vlan];
|
|
364
389
|
if (!declared) throw new Error(`${this.device.name}: VLAN ${vlan} is not declared`);
|
|
365
390
|
return declared.id;
|
|
366
391
|
}
|
|
367
392
|
|
|
368
393
|
/** Ports and LAGs that are layer 2 members of a VLAN, with their settings. A port in a LAG is represented by the LAG. */
|
|
369
|
-
private switched(): [string,
|
|
370
|
-
const ports = Object.entries(this.ports).filter(([, port]) => !port.
|
|
371
|
-
const lags =
|
|
372
|
-
const lag = entry[1];
|
|
373
|
-
return lag.type === "lag" && Boolean(lag.access_vlan || lag.trunk_vlans);
|
|
374
|
-
});
|
|
394
|
+
private switched(): [string, Port | Lag][] {
|
|
395
|
+
const ports = Object.entries(this.ports).filter(([, port]) => !port.in_lag && (port.access_vlan || port.trunk_vlans));
|
|
396
|
+
const lags = RouterOsRender.entries(this.device.interfaces?.lag).filter(([, lag]) => Boolean(lag.access_vlan || lag.trunk_vlans));
|
|
375
397
|
return [...ports, ...lags];
|
|
376
398
|
}
|
|
377
399
|
|
|
378
400
|
/** VLAN interfaces, with their settings. */
|
|
379
|
-
private declaredVlanInterfaces(): [string,
|
|
380
|
-
return
|
|
381
|
-
(
|
|
382
|
-
entry
|
|
383
|
-
): entry is [
|
|
384
|
-
string,
|
|
385
|
-
Extract<
|
|
386
|
-
AnyInterface,
|
|
387
|
-
{
|
|
388
|
-
type: "vlan";
|
|
389
|
-
}
|
|
390
|
-
>,
|
|
391
|
-
] => entry[1].type === "vlan"
|
|
392
|
-
);
|
|
401
|
+
private declaredVlanInterfaces(): [string, VlanInterface][] {
|
|
402
|
+
return RouterOsRender.entries(this.device.interfaces?.vlan_interface);
|
|
393
403
|
}
|
|
394
404
|
|
|
395
405
|
/** Every port and interface that holds addresses or other layer 3 settings. Ports are named as the platform spells them. */
|
|
396
|
-
private routed(): { name: string; settings:
|
|
406
|
+
private routed(): { name: string; settings: Port | NamedInterface["settings"] }[] {
|
|
397
407
|
const ports = Object.entries(this.ports).map(([port, settings]) => ({ name: this.name(port), settings }));
|
|
398
|
-
const interfaces =
|
|
408
|
+
const interfaces = this.interfaces.map(({ name, settings }) => ({ name, settings }));
|
|
399
409
|
return [...ports, ...interfaces];
|
|
400
410
|
}
|
|
401
411
|
|
|
@@ -422,7 +432,7 @@ export class RouterOsRender {
|
|
|
422
432
|
* Restarting the offload after a failure, and offloading IPv6 too, follow the same switch.
|
|
423
433
|
*/
|
|
424
434
|
private hardwareOffload(): void {
|
|
425
|
-
const on = RouterOsRender.yesNo(this.device.hardware_offload);
|
|
435
|
+
const on = RouterOsRender.yesNo(this.device.system?.hardware_offload);
|
|
426
436
|
this.add("/interface ethernet switch", "set switch1", { "l3-hw-offloading": on });
|
|
427
437
|
this.add("/interface ethernet switch l3hw-settings", "set", { autorestart: on, "ipv6-hw": on });
|
|
428
438
|
}
|
|
@@ -433,7 +443,7 @@ export class RouterOsRender {
|
|
|
433
443
|
const needsBridge = this.switched().length > 0 || this.declaredVlanInterfaces().length > 0;
|
|
434
444
|
|
|
435
445
|
if (needsBridge) {
|
|
436
|
-
const stp = device.stp;
|
|
446
|
+
const stp = device.switching?.stp;
|
|
437
447
|
const protocolMode = stp ? (stp.mode ?? "rstp") : "none";
|
|
438
448
|
const priority = stp?.priority === undefined ? undefined : `0x${stp.priority.toString(16)}`;
|
|
439
449
|
this.add("/interface bridge", "add", {
|
|
@@ -444,10 +454,8 @@ export class RouterOsRender {
|
|
|
444
454
|
});
|
|
445
455
|
}
|
|
446
456
|
|
|
447
|
-
for (const [name,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
this.add("/interface bridge", "add", { comment: iface.description, name });
|
|
457
|
+
for (const [name, loopback] of RouterOsRender.entries(device.interfaces?.loopback)) {
|
|
458
|
+
this.add("/interface bridge", "add", { comment: loopback.description, name });
|
|
451
459
|
}
|
|
452
460
|
}
|
|
453
461
|
|
|
@@ -499,11 +507,9 @@ export class RouterOsRender {
|
|
|
499
507
|
|
|
500
508
|
/** A LAG is a bonding interface. Its members are named as the platform spells them. */
|
|
501
509
|
private lags(): void {
|
|
502
|
-
for (const [name, lag] of
|
|
503
|
-
if (lag.type !== "lag") continue;
|
|
504
|
-
|
|
510
|
+
for (const [name, lag] of RouterOsRender.entries(this.device.interfaces?.lag)) {
|
|
505
511
|
const members = Object.entries(this.ports)
|
|
506
|
-
.filter(([, port]) => port.
|
|
512
|
+
.filter(([, port]) => port.in_lag === name)
|
|
507
513
|
.map(([port]) => this.name(port));
|
|
508
514
|
const mode = lag.mode === "static" ? "balance-xor" : "802.3ad";
|
|
509
515
|
this.add("/interface bonding", "add", {
|
|
@@ -524,7 +530,7 @@ export class RouterOsRender {
|
|
|
524
530
|
*/
|
|
525
531
|
private vlanInterfaces(): void {
|
|
526
532
|
for (const [name, iface] of this.declaredVlanInterfaces()) {
|
|
527
|
-
const softwareOnly = this.device.hardware_offload && iface.hardware_offload === false;
|
|
533
|
+
const softwareOnly = this.device.system?.hardware_offload && iface.hardware_offload === false;
|
|
528
534
|
const vlanName = iface.mac ? RouterOsRender.underMacvlan(name) : name;
|
|
529
535
|
|
|
530
536
|
this.add("/interface vlan", "add", {
|
|
@@ -549,38 +555,34 @@ export class RouterOsRender {
|
|
|
549
555
|
}
|
|
550
556
|
|
|
551
557
|
private tunnels(): void {
|
|
552
|
-
for (const [name,
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
}
|
|
558
|
+
for (const [name, gre] of RouterOsRender.entries(this.device.interfaces?.gre)) {
|
|
559
|
+
this.add("/interface gre", "add", {
|
|
560
|
+
comment: gre.description,
|
|
561
|
+
"local-address": Address.host(gre.local_address),
|
|
562
|
+
mtu: gre.mtu,
|
|
563
|
+
name,
|
|
564
|
+
"remote-address": gre.remote_address,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
562
567
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
}
|
|
568
|
+
for (const [name, vxlan] of RouterOsRender.entries(this.device.interfaces?.vxlan)) {
|
|
569
|
+
// 4789 is the platform's default and is not written.
|
|
570
|
+
const port = vxlan.port && vxlan.port !== 4789 ? vxlan.port : undefined;
|
|
571
|
+
this.add("/interface vxlan", "add", {
|
|
572
|
+
comment: vxlan.description,
|
|
573
|
+
"local-address": Address.host(vxlan.local_address),
|
|
574
|
+
"mac-address": vxlan.mac,
|
|
575
|
+
mtu: vxlan.mtu,
|
|
576
|
+
name,
|
|
577
|
+
port,
|
|
578
|
+
vni: vxlan.vni,
|
|
579
|
+
});
|
|
580
|
+
this.add("/interface vxlan vteps", "add", { interface: name, "remote-ip": vxlan.remote_address });
|
|
577
581
|
}
|
|
578
582
|
}
|
|
579
583
|
|
|
580
584
|
private wireguard(): void {
|
|
581
|
-
for (const [name, iface] of
|
|
582
|
-
if (iface.type !== "wireguard") continue;
|
|
583
|
-
|
|
585
|
+
for (const [name, iface] of RouterOsRender.entries(this.device.interfaces?.wireguard)) {
|
|
584
586
|
this.add("/interface wireguard", "add", {
|
|
585
587
|
comment: iface.description,
|
|
586
588
|
"listen-port": iface.listen_port,
|
|
@@ -652,7 +654,7 @@ export class RouterOsRender {
|
|
|
652
654
|
|
|
653
655
|
/** A VLAN is tagged on every trunk carrying it, and on the bridge itself when the router has an interface in it. */
|
|
654
656
|
private bridgeVlans(): void {
|
|
655
|
-
for (const [vlanName, vlan] of
|
|
657
|
+
for (const [vlanName, vlan] of RouterOsRender.entries(this.device.switching?.vlans)) {
|
|
656
658
|
const routedHere = this.declaredVlanInterfaces().some(([, iface]) => iface.vlan === vlanName);
|
|
657
659
|
const trunks = this.switched()
|
|
658
660
|
.filter(([, settings]) => settings.trunk_vlans?.includes(vlanName))
|
|
@@ -702,7 +704,7 @@ export class RouterOsRender {
|
|
|
702
704
|
const lldp = this.lldpMembers();
|
|
703
705
|
if (lldp) lists.set("lldp", lldp);
|
|
704
706
|
|
|
705
|
-
const native = this.device.management?.
|
|
707
|
+
const native = this.device.management?.native_protocol?.interfaces;
|
|
706
708
|
if (native)
|
|
707
709
|
lists.set(
|
|
708
710
|
"native",
|
|
@@ -722,7 +724,7 @@ export class RouterOsRender {
|
|
|
722
724
|
* Where LLDP runs, when it is limited to some ports. Undefined when it runs everywhere or nowhere, which the discovery settings say directly.
|
|
723
725
|
*/
|
|
724
726
|
private lldpMembers(): string[] | undefined {
|
|
725
|
-
const lldp = this.device.lldp;
|
|
727
|
+
const lldp = this.device.switching?.lldp;
|
|
726
728
|
if (!lldp) return undefined;
|
|
727
729
|
|
|
728
730
|
const overridden = Object.entries(this.ports).filter(([, port]) => port.lldp !== undefined);
|
|
@@ -741,7 +743,7 @@ export class RouterOsRender {
|
|
|
741
743
|
|
|
742
744
|
/** A VRF lists its interfaces. */
|
|
743
745
|
private vrfs(): void {
|
|
744
|
-
for (const [vrf, settings] of
|
|
746
|
+
for (const [vrf, settings] of RouterOsRender.entries(this.device.routing?.vrfs)) {
|
|
745
747
|
const members = this.routed()
|
|
746
748
|
.filter(({ settings: iface }) => iface.vrf === vrf)
|
|
747
749
|
.map(({ name }) => name);
|
|
@@ -762,7 +764,7 @@ export class RouterOsRender {
|
|
|
762
764
|
for (const entry of settings.addresses ?? []) {
|
|
763
765
|
const address = typeof entry === "string" ? entry : entry.address;
|
|
764
766
|
// The far end of a point-to-point address, which the platform calls its network.
|
|
765
|
-
const peer = typeof entry === "string" ? undefined : entry.
|
|
767
|
+
const peer = typeof entry === "string" ? undefined : entry.peer_address;
|
|
766
768
|
|
|
767
769
|
if (Address.family(address) === "ipv4") {
|
|
768
770
|
this.add("/ip address", "add", { address, comment: settings.description, interface: name, network: peer });
|
|
@@ -795,7 +797,7 @@ export class RouterOsRender {
|
|
|
795
797
|
}
|
|
796
798
|
|
|
797
799
|
private dhcp(): void {
|
|
798
|
-
for (const [name, server] of Object.entries(this.device.
|
|
800
|
+
for (const [name, server] of Object.entries(this.device.services?.dhcp_server ?? {})) {
|
|
799
801
|
const [first, last] = server.pool;
|
|
800
802
|
// The pool shares the server's name.
|
|
801
803
|
this.add("/ip pool", "add", { name, ranges: `${first}-${last}` });
|
|
@@ -821,14 +823,17 @@ export class RouterOsRender {
|
|
|
821
823
|
}
|
|
822
824
|
}
|
|
823
825
|
|
|
824
|
-
for (const [name, relay] of Object.entries(this.device.dhcp_relay ?? {})) {
|
|
826
|
+
for (const [name, relay] of Object.entries(this.device.services?.dhcp_relay ?? {})) {
|
|
825
827
|
this.add("/ip dhcp-relay", "add", { "dhcp-server": relay.servers.join(","), interface: this.name(relay.interface), name });
|
|
826
828
|
}
|
|
827
829
|
}
|
|
828
830
|
|
|
829
831
|
private dns(): void {
|
|
830
|
-
const
|
|
831
|
-
this.add("/ip dns", "set", {
|
|
832
|
+
const servers = this.device.system?.dns?.servers;
|
|
833
|
+
this.add("/ip dns", "set", {
|
|
834
|
+
"allow-remote-requests": RouterOsRender.yesNo(this.device.services?.dns_server),
|
|
835
|
+
servers: servers?.join(",") ?? "",
|
|
836
|
+
});
|
|
832
837
|
}
|
|
833
838
|
|
|
834
839
|
/**
|
|
@@ -840,16 +845,17 @@ export class RouterOsRender {
|
|
|
840
845
|
const { device } = this;
|
|
841
846
|
const entries: { list: string; address: string }[] = [];
|
|
842
847
|
|
|
843
|
-
for (const [list, members] of
|
|
848
|
+
for (const [list, members] of RouterOsRender.entries(device.firewall?.address_sets)) {
|
|
844
849
|
for (const address of members) entries.push({ list, address });
|
|
845
850
|
}
|
|
846
851
|
|
|
847
|
-
for (const [list
|
|
848
|
-
|
|
852
|
+
for (const [list] of RouterOsRender.entries(device.routing?.prefix_sets)) {
|
|
853
|
+
const prefixes = RouterOsRender.prefixesOf(device, list);
|
|
854
|
+
if (prefixes) for (const address of prefixes) entries.push({ list, address });
|
|
849
855
|
else this.addLine("/ip firewall address-list", `# generated ${list}`);
|
|
850
856
|
}
|
|
851
857
|
|
|
852
|
-
for (const prefix of device.routing?.bgp?.
|
|
858
|
+
for (const prefix of device.routing?.bgp?.originated_prefixes ?? []) {
|
|
853
859
|
entries.push({ list: RouterOsRender.NETWORKS[Address.family(Address.host(prefix))], address: prefix });
|
|
854
860
|
}
|
|
855
861
|
|
|
@@ -918,12 +924,13 @@ export class RouterOsRender {
|
|
|
918
924
|
/** Every address NAT translates to or from: each translation's target, and the address a destination translation matches. Once each, in rule order. */
|
|
919
925
|
static natAddresses(device: Device): string[] {
|
|
920
926
|
const nat = device.firewall?.nat;
|
|
927
|
+
const translatedTo = (rule: NatRule) => (rule.then === "masquerade" ? [] : [Address.host(rule.to_address)]);
|
|
921
928
|
const addresses = [
|
|
922
|
-
...(nat?.source ?? []).flatMap(
|
|
923
|
-
...(nat?.destination ?? []).flatMap((rule) =>
|
|
924
|
-
|
|
925
|
-
...(
|
|
926
|
-
|
|
929
|
+
...(nat?.source ?? []).flatMap(translatedTo),
|
|
930
|
+
...(nat?.destination ?? []).flatMap((rule) => {
|
|
931
|
+
const matched = RouterOsRender.one(rule.if?.dst);
|
|
932
|
+
return [...(matched ? [matched] : []), ...translatedTo(rule)];
|
|
933
|
+
}),
|
|
927
934
|
];
|
|
928
935
|
|
|
929
936
|
return [...new Set(addresses)];
|
|
@@ -934,9 +941,9 @@ export class RouterOsRender {
|
|
|
934
941
|
*
|
|
935
942
|
* A rule naming an address set applies only to the families the set has members in.
|
|
936
943
|
*/
|
|
937
|
-
private families(match: FilterMatch
|
|
944
|
+
private families(match: FilterMatch | undefined): Family[] {
|
|
938
945
|
const sets = this.device.firewall?.address_sets ?? {};
|
|
939
|
-
const address = match?.src ?? match?.dst;
|
|
946
|
+
const address = RouterOsRender.one(match?.src) ?? RouterOsRender.one(match?.dst);
|
|
940
947
|
|
|
941
948
|
let candidates: Family[] = ["ipv4", "ipv6"];
|
|
942
949
|
if (match?.family) candidates = [match.family];
|
|
@@ -944,25 +951,27 @@ export class RouterOsRender {
|
|
|
944
951
|
|
|
945
952
|
const setHas = (set: string | undefined, family: Family) =>
|
|
946
953
|
!set || (sets[set] ?? []).some((member) => Address.family(member) === family);
|
|
947
|
-
return candidates.filter((family) => setHas(match?.
|
|
954
|
+
return candidates.filter((family) => setHas(match?.src_address_set, family) && setHas(match?.dst_address_set, family));
|
|
948
955
|
}
|
|
949
956
|
|
|
950
957
|
/** The match properties of a rule, as the platform names them. */
|
|
951
|
-
private matchProperties(match: FilterMatch
|
|
958
|
+
private matchProperties(match: FilterMatch | undefined, family: Family) {
|
|
952
959
|
const protocol = match?.protocol === "icmp" && family === "ipv6" ? "icmpv6" : match?.protocol;
|
|
953
960
|
const setFlags = match?.tcp_flags?.set ?? [];
|
|
954
961
|
const unsetFlags = (match?.tcp_flags?.unset ?? []).map((flag) => `!${flag}`);
|
|
955
962
|
const flags = match?.tcp_flags ? [...setFlags, ...unsetFlags].join(",") : undefined;
|
|
963
|
+
const inInterface = RouterOsRender.one(match?.in_interface);
|
|
964
|
+
const outInterface = RouterOsRender.one(match?.out_interface);
|
|
956
965
|
return {
|
|
957
|
-
"connection-state": match?.
|
|
958
|
-
"dst-address": match?.dst,
|
|
959
|
-
"dst-address-list": match?.
|
|
966
|
+
"connection-state": match?.connection_state?.join(","),
|
|
967
|
+
"dst-address": RouterOsRender.one(match?.dst),
|
|
968
|
+
"dst-address-list": match?.dst_address_set,
|
|
960
969
|
"dst-port": RouterOsRender.portList(match?.dst_port),
|
|
961
|
-
"in-interface":
|
|
962
|
-
"out-interface":
|
|
970
|
+
"in-interface": inInterface && this.name(inInterface),
|
|
971
|
+
"out-interface": outInterface && this.name(outInterface),
|
|
963
972
|
protocol,
|
|
964
|
-
"src-address": match?.src,
|
|
965
|
-
"src-address-list": match?.
|
|
973
|
+
"src-address": RouterOsRender.one(match?.src),
|
|
974
|
+
"src-address-list": match?.src_address_set,
|
|
966
975
|
"src-port": RouterOsRender.portList(match?.src_port),
|
|
967
976
|
"tcp-flags": flags,
|
|
968
977
|
};
|
|
@@ -976,47 +985,49 @@ export class RouterOsRender {
|
|
|
976
985
|
if (!settings) continue;
|
|
977
986
|
|
|
978
987
|
for (const rule of settings.rules ?? []) {
|
|
979
|
-
if (!this.families(rule.
|
|
988
|
+
if (!this.families(rule.if).includes(family)) continue;
|
|
980
989
|
|
|
981
|
-
const match = this.matchProperties(rule.
|
|
990
|
+
const match = this.matchProperties(rule.if, family);
|
|
982
991
|
// The platform's fast path takes a flow only on IPv4. The accept below still applies on both.
|
|
983
|
-
if (rule.
|
|
992
|
+
if (rule.fast_path && family === "ipv4") {
|
|
984
993
|
this.add(menu, "add", { action: "fasttrack-connection", chain, comment: rule.description, ...match });
|
|
985
994
|
}
|
|
986
995
|
|
|
987
|
-
this.add(menu, "add", { action: rule.
|
|
996
|
+
this.add(menu, "add", { action: rule.then, chain, comment: rule.description, ...match });
|
|
988
997
|
}
|
|
989
998
|
|
|
990
|
-
if (settings.
|
|
999
|
+
if (settings.default_action === "drop") this.add(menu, "add", { action: "drop", chain, comment: "Default" });
|
|
991
1000
|
}
|
|
992
1001
|
}
|
|
993
1002
|
|
|
994
1003
|
private nat(family: Family): void {
|
|
995
1004
|
const menu = family === "ipv4" ? "/ip firewall nat" : "/ipv6 firewall nat";
|
|
996
|
-
const chains: [string, readonly NatRule
|
|
1005
|
+
const chains: [string, readonly NatRule[] | undefined][] = [
|
|
997
1006
|
["srcnat", this.device.firewall?.nat?.source],
|
|
998
1007
|
["dstnat", this.device.firewall?.nat?.destination],
|
|
999
1008
|
];
|
|
1000
1009
|
|
|
1001
1010
|
for (const [chain, rules] of chains) {
|
|
1002
1011
|
for (const rule of rules ?? []) {
|
|
1003
|
-
if (!this.families(rule.
|
|
1012
|
+
if (!this.families(rule.if).includes(family)) continue;
|
|
1004
1013
|
// A translation to an address belongs to that address's family.
|
|
1005
|
-
if (rule.
|
|
1014
|
+
if (rule.then !== "masquerade" && Address.family(rule.to_address) !== family) continue;
|
|
1006
1015
|
|
|
1007
1016
|
const action =
|
|
1008
|
-
rule.
|
|
1009
|
-
? {
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1017
|
+
rule.then === "masquerade"
|
|
1018
|
+
? { action: "masquerade" }
|
|
1019
|
+
: {
|
|
1020
|
+
action: rule.then === "snat" ? "src-nat" : "dst-nat",
|
|
1021
|
+
"to-addresses": Address.host(rule.to_address),
|
|
1022
|
+
"to-ports": rule.to_port,
|
|
1023
|
+
};
|
|
1024
|
+
this.add(menu, "add", { ...action, chain, comment: rule.description, ...this.matchProperties(rule.if, family) });
|
|
1014
1025
|
}
|
|
1015
1026
|
}
|
|
1016
1027
|
}
|
|
1017
1028
|
|
|
1018
1029
|
private helpers(): void {
|
|
1019
|
-
const enabled = this.device.firewall?.
|
|
1030
|
+
const enabled = this.device.firewall?.conntrack_helpers ?? [];
|
|
1020
1031
|
|
|
1021
1032
|
for (const helper of RouterOsRender.HELPERS) {
|
|
1022
1033
|
this.add("/ip firewall service-port", `set ${helper}`, { disabled: RouterOsRender.yesNo(!enabled.includes(helper)) });
|
|
@@ -1028,23 +1039,26 @@ export class RouterOsRender {
|
|
|
1028
1039
|
*/
|
|
1029
1040
|
private switchAcls(): void {
|
|
1030
1041
|
for (const { port, rule, srcPort, dstPort } of RouterOsRender.switchRules(this.device)) {
|
|
1031
|
-
const match = rule.
|
|
1032
|
-
const
|
|
1042
|
+
const match = rule.if ?? {};
|
|
1043
|
+
const src = RouterOsRender.one(match.src);
|
|
1044
|
+
const dst = RouterOsRender.one(match.dst);
|
|
1045
|
+
const vlan = RouterOsRender.one(match.vlan);
|
|
1046
|
+
const ipv6 = match.family === "ipv6" || [src, dst].some((address) => address && Address.family(address) === "ipv6");
|
|
1033
1047
|
const address = (prefix: string | undefined) => (prefix && !ipv6 ? RouterOsRender.dottedMask(prefix) : prefix);
|
|
1034
1048
|
|
|
1035
1049
|
this.add("/interface ethernet switch rule", "add", {
|
|
1036
1050
|
comment: rule.description,
|
|
1037
|
-
[ipv6 ? "dst-address6" : "dst-address"]: address(
|
|
1051
|
+
[ipv6 ? "dst-address6" : "dst-address"]: address(dst),
|
|
1038
1052
|
"dst-port": dstPort,
|
|
1039
1053
|
"mac-protocol": ipv6 ? "ipv6" : "ip",
|
|
1040
|
-
"new-dst-ports": rule.
|
|
1054
|
+
"new-dst-ports": rule.then === "drop" ? "" : undefined,
|
|
1041
1055
|
ports: this.name(port),
|
|
1042
1056
|
protocol: match.protocol,
|
|
1043
1057
|
rate: rule.rate && RouterOsRender.bitrate(rule.rate),
|
|
1044
|
-
[ipv6 ? "src-address6" : "src-address"]: address(
|
|
1058
|
+
[ipv6 ? "src-address6" : "src-address"]: address(src),
|
|
1045
1059
|
"src-port": srcPort,
|
|
1046
1060
|
switch: "switch1",
|
|
1047
|
-
"vlan-id":
|
|
1061
|
+
"vlan-id": vlan && this.vlanId(vlan),
|
|
1048
1062
|
});
|
|
1049
1063
|
}
|
|
1050
1064
|
|
|
@@ -1078,9 +1092,7 @@ export class RouterOsRender {
|
|
|
1078
1092
|
|
|
1079
1093
|
/** The ports with an ethertype filter, with the kinds each takes. Each makes one switch rule per kind and one more. */
|
|
1080
1094
|
static ethertypeFilters(device: Device): { port: string; ethertypes: readonly Ethertype[] }[] {
|
|
1081
|
-
return
|
|
1082
|
-
settings?.ethertypes ? [{ port, ethertypes: settings.ethertypes }] : []
|
|
1083
|
-
);
|
|
1095
|
+
return Derive.ports(device).flatMap(([port, settings]) => (settings.ethertypes ? [{ port, ethertypes: settings.ethertypes }] : []));
|
|
1084
1096
|
}
|
|
1085
1097
|
|
|
1086
1098
|
/** Every switch rule the device's hardware ACLs make, in order: one for each port with an ACL, each rule of it, and each source and destination port or range of the rule. */
|
|
@@ -1088,12 +1100,12 @@ export class RouterOsRender {
|
|
|
1088
1100
|
const each = (ports: Ports | undefined) => (ports === undefined ? [undefined] : RouterOsRender.eachPort(ports));
|
|
1089
1101
|
const rules: SwitchRule[] = [];
|
|
1090
1102
|
|
|
1091
|
-
for (const [port, settings] of
|
|
1092
|
-
if (!settings
|
|
1103
|
+
for (const [port, settings] of Derive.ports(device)) {
|
|
1104
|
+
if (!settings.ingress_acl) continue;
|
|
1093
1105
|
|
|
1094
|
-
for (const rule of device.acls?.[settings.
|
|
1095
|
-
for (const srcPort of each(rule.
|
|
1096
|
-
for (const dstPort of each(rule.
|
|
1106
|
+
for (const rule of device.switching?.acls?.[settings.ingress_acl] ?? []) {
|
|
1107
|
+
for (const srcPort of each(rule.if?.src_port)) {
|
|
1108
|
+
for (const dstPort of each(rule.if?.dst_port)) rules.push({ port, rule, srcPort, dstPort });
|
|
1097
1109
|
}
|
|
1098
1110
|
}
|
|
1099
1111
|
}
|
|
@@ -1118,37 +1130,38 @@ export class RouterOsRender {
|
|
|
1118
1130
|
|
|
1119
1131
|
/** A table beside the main one. `fib` puts its routes in the forwarding table, so traffic can be routed by them. */
|
|
1120
1132
|
private routingTables(): void {
|
|
1121
|
-
for (const [name, table] of
|
|
1133
|
+
for (const [name, table] of RouterOsRender.entries(this.device.routing?.tables)) {
|
|
1122
1134
|
this.add("/routing table", "add fib", { comment: table.description, name });
|
|
1123
1135
|
}
|
|
1124
1136
|
}
|
|
1125
1137
|
|
|
1126
1138
|
/** Rules in the order written. `lookup-only-in-table` drops what the table has no route for, and `lookup` falls back to the main table. */
|
|
1127
1139
|
private routingRules(): void {
|
|
1128
|
-
for (const rule of this.device.routing?.
|
|
1129
|
-
const onlyInTable = rule.
|
|
1140
|
+
for (const rule of this.device.routing?.policy_routing ?? []) {
|
|
1141
|
+
const onlyInTable = rule.routing_table !== "main" && (rule.fallback ?? "none") === "none";
|
|
1130
1142
|
this.add("/routing rule", "add", {
|
|
1131
1143
|
action: onlyInTable ? "lookup-only-in-table" : "lookup",
|
|
1132
1144
|
comment: rule.description,
|
|
1133
1145
|
"dst-address": rule.match?.dst,
|
|
1134
1146
|
interface: rule.match?.in_interface && this.name(rule.match.in_interface),
|
|
1135
1147
|
"src-address": rule.match?.src,
|
|
1136
|
-
table: rule.
|
|
1148
|
+
table: rule.routing_table,
|
|
1137
1149
|
});
|
|
1138
1150
|
}
|
|
1139
1151
|
}
|
|
1140
1152
|
|
|
1141
1153
|
private staticRoutes(): void {
|
|
1142
|
-
for (const route of this.device.routing?.
|
|
1154
|
+
for (const route of this.device.routing?.static_routes ?? []) {
|
|
1143
1155
|
const menu = Address.family(Address.host(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
|
|
1144
1156
|
const verb = route.blackhole ? "add blackhole" : "add";
|
|
1145
|
-
const
|
|
1157
|
+
const nextHop = route.next_hop && Address.host(route.next_hop);
|
|
1158
|
+
const gateway = nextHop ?? (route.interface ? this.name(route.interface) : undefined);
|
|
1146
1159
|
this.add(menu, verb, {
|
|
1147
1160
|
comment: route.description,
|
|
1148
1161
|
distance: route.distance,
|
|
1149
1162
|
"dst-address": route.prefix,
|
|
1150
1163
|
gateway,
|
|
1151
|
-
"routing-table": route.vrf ?? Utils.otherTable(route.
|
|
1164
|
+
"routing-table": route.vrf ?? Utils.otherTable(route.routing_table),
|
|
1152
1165
|
});
|
|
1153
1166
|
}
|
|
1154
1167
|
}
|
|
@@ -1171,28 +1184,28 @@ export class RouterOsRender {
|
|
|
1171
1184
|
}
|
|
1172
1185
|
|
|
1173
1186
|
/** A neighbor's settings with its group's beneath them. */
|
|
1174
|
-
private settingsOf(neighbor:
|
|
1175
|
-
const group = neighbor.
|
|
1187
|
+
private settingsOf(neighbor: Neighbor): NeighborSettings {
|
|
1188
|
+
const group = neighbor.neighbor_group ? this.device.routing?.bgp?.neighbor_groups?.[neighbor.neighbor_group] : undefined;
|
|
1176
1189
|
return { ...group, ...neighbor };
|
|
1177
1190
|
}
|
|
1178
1191
|
|
|
1179
1192
|
/** The families a neighbor exchanges: the ones it states, or its address's. */
|
|
1180
|
-
private familiesOf(neighbor:
|
|
1181
|
-
return this.settingsOf(neighbor).
|
|
1193
|
+
private familiesOf(neighbor: Neighbor): readonly Family[] {
|
|
1194
|
+
return this.settingsOf(neighbor).address_families ?? [Address.family(neighbor.address)];
|
|
1182
1195
|
}
|
|
1183
1196
|
|
|
1184
1197
|
/** Does a rule belong on a family's chain? A rule naming a prefix, a family, or a prefix set of one family belongs only there. */
|
|
1185
|
-
private appliesTo(rule:
|
|
1186
|
-
const match = rule.
|
|
1198
|
+
private appliesTo(rule: PolicyRule, family: Family): boolean {
|
|
1199
|
+
const match = RouterOsRender.onRoutes(rule) ? rule.if : undefined;
|
|
1187
1200
|
if (match?.family && match.family !== family) return false;
|
|
1188
1201
|
|
|
1189
1202
|
if (match?.prefix && Address.family(Address.host(match.prefix)) !== family) return false;
|
|
1190
1203
|
|
|
1191
1204
|
if (match?.prefix_set) {
|
|
1192
|
-
const set = this.device.prefix_sets?.[match.prefix_set];
|
|
1205
|
+
const set = this.device.routing?.prefix_sets?.[match.prefix_set];
|
|
1193
1206
|
if (!set) return false;
|
|
1194
1207
|
|
|
1195
|
-
const firstPrefix =
|
|
1208
|
+
const firstPrefix = RouterOsRender.prefixesOf(this.device, match.prefix_set)?.[0];
|
|
1196
1209
|
const setFamily = "source" in set ? set.source.family : firstPrefix && Address.family(Address.host(firstPrefix));
|
|
1197
1210
|
if (setFamily !== family) return false;
|
|
1198
1211
|
}
|
|
@@ -1207,7 +1220,7 @@ export class RouterOsRender {
|
|
|
1207
1220
|
*/
|
|
1208
1221
|
private policies(): void {
|
|
1209
1222
|
const bgp = this.device.routing!.bgp!;
|
|
1210
|
-
const policies = this.device.policies ?? {};
|
|
1223
|
+
const policies = this.device.routing?.policies ?? {};
|
|
1211
1224
|
|
|
1212
1225
|
const wanted = new Map<string, Set<Family>>();
|
|
1213
1226
|
const want = (policy: string, family: Family) => {
|
|
@@ -1225,14 +1238,14 @@ export class RouterOsRender {
|
|
|
1225
1238
|
const settings = this.settingsOf(neighbor);
|
|
1226
1239
|
|
|
1227
1240
|
for (const family of this.familiesOf(neighbor)) {
|
|
1228
|
-
for (const policy of [settings.
|
|
1241
|
+
for (const policy of [settings.import_policy, settings.export_policy]) {
|
|
1229
1242
|
if (policy) want(policy, family);
|
|
1230
1243
|
else rejectAllNeeded = true;
|
|
1231
1244
|
}
|
|
1232
1245
|
}
|
|
1233
1246
|
}
|
|
1234
1247
|
|
|
1235
|
-
const called = new Set(Object.values(policies).flatMap((rules) => rules.flatMap((rule) => (rule.call ? [rule.call] : []))));
|
|
1248
|
+
const called = new Set(Object.values(policies).flatMap((rules) => (rules ?? []).flatMap((rule) => (rule.call ? [rule.call] : []))));
|
|
1236
1249
|
|
|
1237
1250
|
for (const policy of [...wanted.keys()].sort()) {
|
|
1238
1251
|
for (const family of ["ipv4", "ipv6"] as const) {
|
|
@@ -1243,11 +1256,12 @@ export class RouterOsRender {
|
|
|
1243
1256
|
|
|
1244
1257
|
for (const rule of rules) {
|
|
1245
1258
|
const text = RouterOsRender.ruleText(rule, family);
|
|
1246
|
-
|
|
1259
|
+
const comment = RouterOsRender.onRoutes(rule) ? rule.description : undefined;
|
|
1260
|
+
if (text) this.add("/routing filter rule", "add", { chain, comment, rule: text });
|
|
1247
1261
|
}
|
|
1248
1262
|
|
|
1249
1263
|
const last = rules.at(-1);
|
|
1250
|
-
const decides = last !== undefined &&
|
|
1264
|
+
const decides = last !== undefined && RouterOsRender.onRoutes(last) && !last.if && last.then !== undefined;
|
|
1251
1265
|
if (!called.has(policy) && !decides) this.add("/routing filter rule", "add", { chain, rule: "reject" });
|
|
1252
1266
|
}
|
|
1253
1267
|
}
|
|
@@ -1270,7 +1284,7 @@ export class RouterOsRender {
|
|
|
1270
1284
|
const [family] = this.familiesOf(neighbor) as [Family];
|
|
1271
1285
|
const addressFamily = Address.family(neighbor.address);
|
|
1272
1286
|
const internal = settings.remote_as === bgp.asn;
|
|
1273
|
-
const originates = bgp.
|
|
1287
|
+
const originates = bgp.originated_prefixes?.some((prefix) => Address.family(Address.host(prefix)) === family);
|
|
1274
1288
|
const role = settings.role ? RouterOsRender.ROLES[settings.role] : internal ? "ibgp" : "ebgp";
|
|
1275
1289
|
|
|
1276
1290
|
this.add("/routing bgp connection", "add", {
|
|
@@ -1279,21 +1293,21 @@ export class RouterOsRender {
|
|
|
1279
1293
|
as: bgp.asn,
|
|
1280
1294
|
connect: settings.passive ? "no" : undefined,
|
|
1281
1295
|
"hold-time": settings.hold_time ? RouterOsRender.duration(settings.hold_time) : undefined,
|
|
1282
|
-
"input.filter": RouterOsRender.chainName(settings.
|
|
1296
|
+
"input.filter": RouterOsRender.chainName(settings.import_policy ?? RouterOsRender.REJECT_ALL, family),
|
|
1283
1297
|
// The one instance bgpInstance adds. 7.23 refuses a new connection without it.
|
|
1284
1298
|
instance: "default",
|
|
1285
1299
|
[`input.limit-process-routes-${family}`]: settings.max_prefixes,
|
|
1286
1300
|
"keepalive-time": settings.keepalive ? RouterOsRender.duration(settings.keepalive) : undefined,
|
|
1287
1301
|
listen: settings.passive ? "yes" : undefined,
|
|
1288
|
-
"local.address": settings.local_address,
|
|
1302
|
+
"local.address": settings.local_address && Address.host(settings.local_address),
|
|
1289
1303
|
"local.role": role,
|
|
1290
1304
|
multihop: settings.multihop ? "yes" : undefined,
|
|
1291
1305
|
name,
|
|
1292
|
-
"output.filter-chain": RouterOsRender.chainName(settings.
|
|
1306
|
+
"output.filter-chain": RouterOsRender.chainName(settings.export_policy ?? RouterOsRender.REJECT_ALL, family),
|
|
1293
1307
|
"output.network": originates ? RouterOsRender.NETWORKS[family] : undefined,
|
|
1294
1308
|
"remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
|
|
1295
1309
|
"remote.as": settings.remote_as,
|
|
1296
|
-
"routing-table": Utils.otherTable(settings.
|
|
1310
|
+
"routing-table": Utils.otherTable(settings.routing_table),
|
|
1297
1311
|
"tcp-md5-key": settings.password && Secrets.marker(settings.password),
|
|
1298
1312
|
"use-bfd": settings.bfd ? "yes" : undefined,
|
|
1299
1313
|
});
|
|
@@ -1305,7 +1319,7 @@ export class RouterOsRender {
|
|
|
1305
1319
|
const ospf = this.device.routing?.ospf;
|
|
1306
1320
|
if (!ospf) return;
|
|
1307
1321
|
|
|
1308
|
-
for (const family of ospf.
|
|
1322
|
+
for (const family of ospf.address_families ?? ["ipv4"]) {
|
|
1309
1323
|
const version = family === "ipv4" ? 2 : 3;
|
|
1310
1324
|
const instance = `ospf-v${version}`;
|
|
1311
1325
|
this.add("/routing ospf instance", "add", {
|
|
@@ -1367,6 +1381,12 @@ export class RouterOsRender {
|
|
|
1367
1381
|
this.certificates();
|
|
1368
1382
|
}
|
|
1369
1383
|
|
|
1384
|
+
/** The sources a service takes: its own, or `management.allow`. `unsupported()` refuses an address set named there. */
|
|
1385
|
+
private allowed(own: readonly Prefix[] | undefined): readonly string[] {
|
|
1386
|
+
const shared = this.device.management?.allow;
|
|
1387
|
+
return own ?? (typeof shared === "string" ? [] : shared) ?? [];
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1370
1390
|
/** Every service the platform has is stated. One the config does not name is disabled. */
|
|
1371
1391
|
private services(): void {
|
|
1372
1392
|
const management = this.device.management ?? {};
|
|
@@ -1378,7 +1398,7 @@ export class RouterOsRender {
|
|
|
1378
1398
|
continue;
|
|
1379
1399
|
}
|
|
1380
1400
|
|
|
1381
|
-
const allow = service.allow
|
|
1401
|
+
const allow = this.allowed(service.allow);
|
|
1382
1402
|
this.add("/ip service", `set ${platform}`, {
|
|
1383
1403
|
address: allow.join(","),
|
|
1384
1404
|
certificate: service.certificate,
|
|
@@ -1394,7 +1414,7 @@ export class RouterOsRender {
|
|
|
1394
1414
|
|
|
1395
1415
|
/** What works at layer 2: the platform's client over MAC, MAC ping, the bandwidth test server and neighbor discovery. */
|
|
1396
1416
|
private layerTwo(): void {
|
|
1397
|
-
const nativeList = this.device.management?.
|
|
1417
|
+
const nativeList = this.device.management?.native_protocol?.interfaces ? "native" : "none";
|
|
1398
1418
|
this.add("/tool mac-server", "set", { "allowed-interface-list": nativeList });
|
|
1399
1419
|
this.add("/tool mac-server mac-winbox", "set", { "allowed-interface-list": nativeList });
|
|
1400
1420
|
this.add("/tool mac-server ping", "set", { enabled: "no" });
|
|
@@ -1402,7 +1422,7 @@ export class RouterOsRender {
|
|
|
1402
1422
|
|
|
1403
1423
|
let discovery = "none";
|
|
1404
1424
|
if (this.lldpMembers()) discovery = "lldp";
|
|
1405
|
-
else if (this.device.lldp) discovery = "all";
|
|
1425
|
+
else if (this.device.switching?.lldp) discovery = "all";
|
|
1406
1426
|
this.add("/ip neighbor discovery-settings", "set", { "discover-interface-list": discovery, protocol: "lldp" });
|
|
1407
1427
|
}
|
|
1408
1428
|
|
|
@@ -1410,8 +1430,7 @@ export class RouterOsRender {
|
|
|
1410
1430
|
* The platform keeps one community it will not delete. With version 2c it becomes the read community; without, it is disabled. Version 3 users are communities of their own.
|
|
1411
1431
|
*/
|
|
1412
1432
|
private snmp(): void {
|
|
1413
|
-
const
|
|
1414
|
-
const snmp = management.snmp;
|
|
1433
|
+
const snmp = this.device.monitoring?.snmp;
|
|
1415
1434
|
|
|
1416
1435
|
if (!snmp) {
|
|
1417
1436
|
this.add("/snmp", "set", { enabled: "no" });
|
|
@@ -1419,7 +1438,7 @@ export class RouterOsRender {
|
|
|
1419
1438
|
return;
|
|
1420
1439
|
}
|
|
1421
1440
|
|
|
1422
|
-
const allow = (snmp.allow
|
|
1441
|
+
const allow = this.allowed(snmp.allow).join(",");
|
|
1423
1442
|
this.add("/snmp", "set", { contact: snmp.contact ?? "", enabled: "yes", location: snmp.location ?? "" });
|
|
1424
1443
|
|
|
1425
1444
|
if (snmp.community)
|
|
@@ -1430,7 +1449,8 @@ export class RouterOsRender {
|
|
|
1430
1449
|
this.add("/snmp community", "add", {
|
|
1431
1450
|
addresses: allow,
|
|
1432
1451
|
"authentication-password": Secrets.marker(user.auth_password),
|
|
1433
|
-
|
|
1452
|
+
// SHA1 only: unsupported() refuses sha256, so it is never written as SHA1.
|
|
1453
|
+
"authentication-protocol": "SHA1",
|
|
1434
1454
|
"encryption-password": Secrets.marker(user.privacy_password),
|
|
1435
1455
|
"encryption-protocol": RouterOsRender.SNMP_PRIVACY[user.privacy],
|
|
1436
1456
|
name,
|
|
@@ -1440,7 +1460,7 @@ export class RouterOsRender {
|
|
|
1440
1460
|
}
|
|
1441
1461
|
|
|
1442
1462
|
private users(): void {
|
|
1443
|
-
for (const [name, user] of
|
|
1463
|
+
for (const [name, user] of RouterOsRender.entries(this.device.system?.users)) {
|
|
1444
1464
|
this.add("/user", "add", {
|
|
1445
1465
|
comment: user.description,
|
|
1446
1466
|
group: RouterOsRender.USER_GROUPS[user.role],
|
|
@@ -1455,9 +1475,9 @@ export class RouterOsRender {
|
|
|
1455
1475
|
* Certificates are not part of an export, so the reader lists them in this same form: a name, the SHA-256 fingerprint of the certificate, and whether the device holds its private key. A difference is installed from the config, not edited.
|
|
1456
1476
|
*/
|
|
1457
1477
|
private certificates(): void {
|
|
1458
|
-
for (const [name, certificate] of
|
|
1478
|
+
for (const [name, certificate] of RouterOsRender.entries(this.device.system?.certificates)) {
|
|
1459
1479
|
this.add("/certificate", "add", {
|
|
1460
|
-
fingerprint: RouterOsRender.fingerprint(certificate.
|
|
1480
|
+
fingerprint: RouterOsRender.fingerprint(Utils.projectFile(certificate.certificate_file)),
|
|
1461
1481
|
name,
|
|
1462
1482
|
"private-key": RouterOsRender.yesNo(Boolean(certificate.private_key)),
|
|
1463
1483
|
});
|
|
@@ -1468,8 +1488,8 @@ export class RouterOsRender {
|
|
|
1468
1488
|
const system = this.device.system ?? {};
|
|
1469
1489
|
|
|
1470
1490
|
this.add("/ip settings", "set", {
|
|
1471
|
-
"send-redirects": RouterOsRender.yesNo(system.
|
|
1472
|
-
"tcp-syncookies": RouterOsRender.yesNo(system.
|
|
1491
|
+
"send-redirects": RouterOsRender.yesNo(system.ip_stack?.icmp_redirects),
|
|
1492
|
+
"tcp-syncookies": RouterOsRender.yesNo(system.ip_stack?.syn_cookies),
|
|
1473
1493
|
});
|
|
1474
1494
|
this.add("/system identity", "set", { name: this.device.name });
|
|
1475
1495
|
|
|
@@ -1478,7 +1498,7 @@ export class RouterOsRender {
|
|
|
1478
1498
|
this.add("/system note", "set", { note: system.banner ?? "", "show-at-login": "yes" });
|
|
1479
1499
|
|
|
1480
1500
|
this.add("/system ntp client", "set", { enabled: RouterOsRender.yesNo(Boolean(system.ntp?.servers.length)) });
|
|
1481
|
-
this.add("/system ntp server", "set", { enabled: RouterOsRender.yesNo(
|
|
1501
|
+
this.add("/system ntp server", "set", { enabled: RouterOsRender.yesNo(this.device.services?.ntp_server) });
|
|
1482
1502
|
for (const server of system.ntp?.servers ?? []) this.add("/system ntp client servers", "add", { address: server });
|
|
1483
1503
|
|
|
1484
1504
|
this.logging();
|
|
@@ -1490,10 +1510,9 @@ export class RouterOsRender {
|
|
|
1490
1510
|
this.flowExport();
|
|
1491
1511
|
}
|
|
1492
1512
|
|
|
1493
|
-
/** Local logging uses the platform's built-in disk action, number 1. Each
|
|
1513
|
+
/** Local logging uses the platform's built-in disk action, number 1. Each syslog collector gets an action of its own. */
|
|
1494
1514
|
private logging(): void {
|
|
1495
|
-
const
|
|
1496
|
-
const local = logging?.local;
|
|
1515
|
+
const local = this.device.system?.logging?.local;
|
|
1497
1516
|
|
|
1498
1517
|
this.add("/system logging action", "set 1", {
|
|
1499
1518
|
"disk-file-count": local?.files ?? 2,
|
|
@@ -1503,7 +1522,7 @@ export class RouterOsRender {
|
|
|
1503
1522
|
this.add("/system logging", "add", { action: "disk", topics: topic });
|
|
1504
1523
|
}
|
|
1505
1524
|
|
|
1506
|
-
(
|
|
1525
|
+
(this.device.monitoring?.syslog ?? []).forEach((collector, index) => {
|
|
1507
1526
|
const action = `remote${index + 1}`;
|
|
1508
1527
|
this.add("/system logging action", "add", {
|
|
1509
1528
|
name: action,
|
|
@@ -1517,7 +1536,7 @@ export class RouterOsRender {
|
|
|
1517
1536
|
|
|
1518
1537
|
/** NetFlow version 9 and IPFIX through traffic flow. The platform has no sFlow, which `unsupported()` reports. */
|
|
1519
1538
|
private flowExport(): void {
|
|
1520
|
-
const flow = this.device.flow_export;
|
|
1539
|
+
const flow = this.device.monitoring?.flow_export;
|
|
1521
1540
|
if (!flow) {
|
|
1522
1541
|
this.add("/ip traffic-flow", "set", { enabled: "no" });
|
|
1523
1542
|
return;
|