@takodotid/circuit 0.34.2 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11347 -9018
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +17 -11
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -25
- package/src/adapters/devices/raisecom-ros/index.ts +0 -693
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
|
@@ -0,0 +1,1483 @@
|
|
|
1
|
+
// The neutral model as a RouterOS 7 export, written in the form `/export` prints so the two compare line for line.
|
|
2
|
+
|
|
3
|
+
import { X509Certificate } from "node:crypto";
|
|
4
|
+
import { Address } from "../../core/address";
|
|
5
|
+
import { Secrets } from "../../core/secrets";
|
|
6
|
+
import type {
|
|
7
|
+
AclRule,
|
|
8
|
+
Device,
|
|
9
|
+
Ethertype,
|
|
10
|
+
Family,
|
|
11
|
+
FilterMatch,
|
|
12
|
+
Interface,
|
|
13
|
+
NatRule,
|
|
14
|
+
Neighbor,
|
|
15
|
+
NeighborSettings,
|
|
16
|
+
PolicyRule,
|
|
17
|
+
Port,
|
|
18
|
+
Ports,
|
|
19
|
+
Service,
|
|
20
|
+
Severity,
|
|
21
|
+
} from "../../schema";
|
|
22
|
+
import { Catalog } from "../catalog";
|
|
23
|
+
|
|
24
|
+
/** A property value as a command takes it. `undefined` and `false` leave the property out. */
|
|
25
|
+
export type Value = string | number | undefined | false;
|
|
26
|
+
|
|
27
|
+
export type AnyPort = Port<string, string, string, string>;
|
|
28
|
+
|
|
29
|
+
/** 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. */
|
|
30
|
+
export type SwitchRule = { port: string; rule: AclRule<string>; srcPort?: string; dstPort?: string };
|
|
31
|
+
export type AnyInterface = Interface<string, string>;
|
|
32
|
+
|
|
33
|
+
type Speed = NonNullable<AnyPort["speed"]>;
|
|
34
|
+
type AnyRule = PolicyRule<string, string>;
|
|
35
|
+
type AnyNeighbor = Neighbor<string, string>;
|
|
36
|
+
|
|
37
|
+
/** One device's render: the device, and the commands collected under their menu. */
|
|
38
|
+
export class RouterOsRender {
|
|
39
|
+
/** The bridge every switched port joins. One per device. */
|
|
40
|
+
private static readonly BRIDGE = "bridge";
|
|
41
|
+
|
|
42
|
+
/** Destination of spanning tree BPDUs, so a filter can drop them on one port. */
|
|
43
|
+
private static readonly BPDU_DESTINATION = "01:80:C2:00:00:00/FF:FF:FF:FF:FF:FF";
|
|
44
|
+
|
|
45
|
+
/** The platform names a fixed speed by its medium: an optic in a cage, or copper. */
|
|
46
|
+
private static readonly MEDIUM: Record<"optical" | "copper", Partial<Record<Speed, string>>> = {
|
|
47
|
+
optical: {
|
|
48
|
+
"1g": "1G-baseX",
|
|
49
|
+
"10g": "10G-baseSR-LR",
|
|
50
|
+
"25g": "25G-baseSR-LR",
|
|
51
|
+
"40g": "40G-baseSR4-LR4",
|
|
52
|
+
"50g": "50G-baseSR2-LR2",
|
|
53
|
+
"100g": "100G-baseSR4-LR4",
|
|
54
|
+
},
|
|
55
|
+
copper: {
|
|
56
|
+
"100m": "100M-baseT-full",
|
|
57
|
+
"1g": "1G-baseT-full",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Address lists holding the prefixes BGP originates. A session reads the one of its family. */
|
|
62
|
+
private static readonly NETWORKS: Record<Family, string> = { ipv4: "bgp-networks-v4", ipv6: "bgp-networks-v6" };
|
|
63
|
+
|
|
64
|
+
/** A kind of frame as a switch rule's `mac-protocol` names it. */
|
|
65
|
+
private static readonly MAC_PROTOCOLS: Record<Ethertype, string> = { ipv4: "ip", ipv6: "ipv6", arp: "arp" };
|
|
66
|
+
|
|
67
|
+
/** Helpers the platform has, all stated so that one the config does not list is off. */
|
|
68
|
+
private static readonly HELPERS = ["ftp", "tftp", "irc", "h323", "sip", "pptp", "rtsp"] as const;
|
|
69
|
+
|
|
70
|
+
private static readonly CHAINS = ["input", "forward", "output"] as const;
|
|
71
|
+
|
|
72
|
+
/** The validator group every RPKI server joins. */
|
|
73
|
+
private static readonly RPKI_GROUP = "rpki";
|
|
74
|
+
|
|
75
|
+
/** The chain a session without a policy uses, so that it learns or announces nothing. */
|
|
76
|
+
private static readonly REJECT_ALL = "REJECT-ALL";
|
|
77
|
+
|
|
78
|
+
// RouterOS takes our own role. The config states the neighbor's, so each becomes its opposite: a neighbor that is our provider makes us its customer.
|
|
79
|
+
private static readonly ROLES = {
|
|
80
|
+
provider: "ebgp-customer",
|
|
81
|
+
customer: "ebgp-provider",
|
|
82
|
+
peer: "ebgp-peer",
|
|
83
|
+
rs: "ebgp-rs-client",
|
|
84
|
+
"rs-client": "ebgp-rs",
|
|
85
|
+
} as const;
|
|
86
|
+
|
|
87
|
+
/** Neutral service name to the platform's. `native` is the platform's own desktop client. */
|
|
88
|
+
private static readonly SERVICES = {
|
|
89
|
+
ftp: "ftp",
|
|
90
|
+
ssh: "ssh",
|
|
91
|
+
telnet: "telnet",
|
|
92
|
+
http: "www",
|
|
93
|
+
https: "www-ssl",
|
|
94
|
+
api: "api",
|
|
95
|
+
api_tls: "api-ssl",
|
|
96
|
+
native: "winbox",
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
private static readonly USER_GROUPS = { admin: "full", operator: "write", "read-only": "read" } as const;
|
|
100
|
+
|
|
101
|
+
private static readonly SNMP_AUTH = { sha1: "SHA1", sha256: "SHA1" } as const;
|
|
102
|
+
private static readonly SNMP_PRIVACY = { aes128: "AES", des: "DES" } as const;
|
|
103
|
+
|
|
104
|
+
/** Severity threshold to the topics the platform logs under, most severe first. */
|
|
105
|
+
private static readonly TOPICS: [Severity, string][] = [
|
|
106
|
+
["critical", "critical"],
|
|
107
|
+
["error", "error"],
|
|
108
|
+
["warning", "warning"],
|
|
109
|
+
["info", "info"],
|
|
110
|
+
["debug", "debug"],
|
|
111
|
+
];
|
|
112
|
+
private static readonly SEVERITY_ORDER: Severity[] = ["emergency", "alert", "critical", "error", "warning", "notice", "info", "debug"];
|
|
113
|
+
|
|
114
|
+
private readonly ports: Record<string, AnyPort>;
|
|
115
|
+
private readonly interfaces: Record<string, AnyInterface>;
|
|
116
|
+
|
|
117
|
+
/** Commands under their menu. */
|
|
118
|
+
private readonly menus = new Map<string, string[]>();
|
|
119
|
+
|
|
120
|
+
private constructor(private readonly device: Device) {
|
|
121
|
+
this.ports = Object.fromEntries(
|
|
122
|
+
Object.entries(device.ports ?? {}).filter((entry): entry is [string, AnyPort] => entry[1] !== undefined)
|
|
123
|
+
);
|
|
124
|
+
this.interfaces = device.interfaces ?? {};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The whole configuration the device should run. */
|
|
128
|
+
static of(device: Device): string {
|
|
129
|
+
const render = new RouterOsRender(device);
|
|
130
|
+
|
|
131
|
+
render.interfaceMenus();
|
|
132
|
+
render.addressing();
|
|
133
|
+
render.firewall();
|
|
134
|
+
render.routing();
|
|
135
|
+
render.management();
|
|
136
|
+
render.system();
|
|
137
|
+
|
|
138
|
+
return render.text();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The platform's name for a fixed speed on a port, or undefined when it has none. */
|
|
142
|
+
static medium(platformPortName: string, speed: Speed): string | undefined {
|
|
143
|
+
const medium = platformPortName.startsWith("ether") ? "copper" : "optical";
|
|
144
|
+
return RouterOsRender.MEDIUM[medium][speed];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Quote a value when it holds anything but plain characters. */
|
|
148
|
+
private static quote(value: string): string {
|
|
149
|
+
const plain = /^[\w.:/@-]+$/.test(value);
|
|
150
|
+
if (plain) return value;
|
|
151
|
+
// Inside a quoted string `$` starts a variable, so it is escaped like a quote.
|
|
152
|
+
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$");
|
|
153
|
+
return `"${escaped}"`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A command: a verb such as `add` or `set [ find name=x ]`, then `property=value` for every property that has a value.
|
|
158
|
+
*
|
|
159
|
+
* Properties are written in the order given.
|
|
160
|
+
*/
|
|
161
|
+
private static command(verb: string, properties: Record<string, Value>): string {
|
|
162
|
+
const words = [verb];
|
|
163
|
+
|
|
164
|
+
for (const [name, value] of Object.entries(properties)) {
|
|
165
|
+
if (value === undefined || value === false) continue;
|
|
166
|
+
|
|
167
|
+
const written = typeof value === "number" ? String(value) : RouterOsRender.quote(value);
|
|
168
|
+
words.push(`${name}=${written}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return words.join(" ");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** `yes` or `no`. */
|
|
175
|
+
private static yesNo(on: boolean | undefined): string {
|
|
176
|
+
return on ? "yes" : "no";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** `V4` or `V6`, the suffix of a chain rendered for one family. */
|
|
180
|
+
private static familySuffix(family: Family): string {
|
|
181
|
+
return family === "ipv4" ? "V4" : "V6";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Each port or range of ports as the platform writes it: `53`, `0-52`. */
|
|
185
|
+
private static eachPort(ports: Ports): string[] {
|
|
186
|
+
return [ports].flat().map((port) => (typeof port === "number" ? String(port) : `${port.min}-${port.max}`));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** One port, a range, or a list of them, as `53,0-52`. */
|
|
190
|
+
private static portList(ports: Ports | undefined): string | undefined {
|
|
191
|
+
if (ports === undefined) return undefined;
|
|
192
|
+
|
|
193
|
+
return RouterOsRender.eachPort(ports).join(",");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Seconds as the platform writes a duration: `3600` becomes `1h`, `90` becomes `1m30s`. */
|
|
197
|
+
private static duration(seconds: number): string {
|
|
198
|
+
const units = [
|
|
199
|
+
["w", 604800],
|
|
200
|
+
["d", 86400],
|
|
201
|
+
["h", 3600],
|
|
202
|
+
["m", 60],
|
|
203
|
+
["s", 1],
|
|
204
|
+
] as const;
|
|
205
|
+
let remaining = seconds;
|
|
206
|
+
let text = "";
|
|
207
|
+
|
|
208
|
+
for (const [unit, size] of units) {
|
|
209
|
+
const count = Math.floor(remaining / size);
|
|
210
|
+
if (count > 0) text += `${count}${unit}`;
|
|
211
|
+
remaining %= size;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return text || "0s";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** A rate as the switch chip takes it: `2G` becomes `2.0Gbps`. */
|
|
218
|
+
private static bitrate(rate: string): string {
|
|
219
|
+
const match = /^([\d.]+)([kMG])$/.exec(rate);
|
|
220
|
+
if (!match) return rate;
|
|
221
|
+
|
|
222
|
+
const [, amount, unit] = match;
|
|
223
|
+
const suffix = unit === "k" ? "kbps" : `${unit}bps`;
|
|
224
|
+
return `${Number(amount).toFixed(1)}${suffix}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** An IPv4 prefix with a dotted mask, as the switch chip takes it: `10.0.0.0/8` becomes `10.0.0.0/255.0.0.0`. */
|
|
228
|
+
private static dottedMask(cidr: string): string {
|
|
229
|
+
const { addr, len } = Address.split(cidr);
|
|
230
|
+
const mask = len === 0 ? 0 : (0xffffffff << (32 - len)) >>> 0;
|
|
231
|
+
const octets = [24, 16, 8, 0].map((shift) => (mask >>> shift) & 255);
|
|
232
|
+
return `${addr}/${octets.join(".")}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A community pattern with `*` as a regular expression the platform matches on: `64500:*:*` becomes `^64500:[0-9]+:[0-9]+$`. */
|
|
236
|
+
private static communityPattern(pattern: string): string {
|
|
237
|
+
const fields = pattern.split(":").map((field) => (field === "*" ? "[0-9]+" : field));
|
|
238
|
+
return `^${fields.join(":")}$`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** The name of the VLAN interface under one with a MAC of its own, which takes the configured name itself. */
|
|
242
|
+
private static underMacvlan(name: string): string {
|
|
243
|
+
return `${name}-vlan`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The interface holding one VRRP group. */
|
|
247
|
+
private static vrrpName(iface: string, id: number): string {
|
|
248
|
+
return `${iface}-vrrp${id}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** `host:port` into its parts. The last colon separates them, so an IPv6 host keeps its own. */
|
|
252
|
+
private static splitEndpoint(endpoint: string): { host: string; port: string } {
|
|
253
|
+
const colon = endpoint.lastIndexOf(":");
|
|
254
|
+
return { host: endpoint.slice(0, colon), port: endpoint.slice(colon + 1) };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** The chain a policy is rendered as for one family. A policy can test prefix lengths that mean different things per family, so each family gets its own. */
|
|
258
|
+
private static chainName(policy: string, family: Family): string {
|
|
259
|
+
return `${policy}-${RouterOsRender.familySuffix(family)}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** The conditions of a rule, as the platform's filter language writes them. */
|
|
263
|
+
private static conditions(rule: AnyRule): string[] {
|
|
264
|
+
const match = rule.match ?? {};
|
|
265
|
+
const tests: string[] = [];
|
|
266
|
+
if (match.prefix) tests.push(`dst==${match.prefix}`);
|
|
267
|
+
if (match.prefix_set) tests.push(`dst in ${match.prefix_set}`);
|
|
268
|
+
if (match.prefix_length?.min !== undefined) tests.push(`dst-len>${match.prefix_length.min - 1}`);
|
|
269
|
+
if (match.prefix_length?.max !== undefined) tests.push(`dst-len<${match.prefix_length.max + 1}`);
|
|
270
|
+
if (match.as_path_length?.min !== undefined) tests.push(`bgp-path-len>${match.as_path_length.min - 1}`);
|
|
271
|
+
if (match.as_path_length?.max !== undefined) tests.push(`bgp-path-len<${match.as_path_length.max + 1}`);
|
|
272
|
+
// The platform's AS path regexp matches each AS as a whole number, so a bare number is that AS anywhere in the path.
|
|
273
|
+
if (match.through_as !== undefined) tests.push(`bgp-as-path ${match.through_as}`);
|
|
274
|
+
if (match.rpki) tests.push(`rpki ${match.rpki}`);
|
|
275
|
+
if (match.community) tests.push(`bgp-communities includes ${match.community}`);
|
|
276
|
+
if (match.large_community) tests.push(`bgp-large-communities includes ${match.large_community}`);
|
|
277
|
+
return tests;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** What a rule does to a matching route, then its decision. */
|
|
281
|
+
private static actions(rule: AnyRule, family: Family): string[] {
|
|
282
|
+
const set = rule.set ?? {};
|
|
283
|
+
const steps: string[] = [];
|
|
284
|
+
if (set.local_pref !== undefined) steps.push(`set bgp-local-pref ${set.local_pref}`);
|
|
285
|
+
if (set.med !== undefined) steps.push(`set bgp-med ${set.med}`);
|
|
286
|
+
if (set.prepend !== undefined) steps.push(`set bgp-path-prepend ${set.prepend}`);
|
|
287
|
+
for (const pattern of set.remove_communities ?? []) {
|
|
288
|
+
steps.push(`delete bgp-communities regexp "${RouterOsRender.communityPattern(pattern)}"`);
|
|
289
|
+
}
|
|
290
|
+
if (set.communities) steps.push(`set bgp-communities ${set.communities.join(",")}`);
|
|
291
|
+
if (set.add_communities) steps.push(`append bgp-communities ${set.add_communities.join(",")}`);
|
|
292
|
+
|
|
293
|
+
for (const pattern of set.remove_large_communities ?? []) {
|
|
294
|
+
steps.push(`delete bgp-large-communities regexp "${RouterOsRender.communityPattern(pattern)}"`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (set.large_communities) steps.push(`set bgp-large-communities ${set.large_communities.join(",")}`);
|
|
298
|
+
if (set.add_large_communities) steps.push(`append bgp-large-communities ${set.add_large_communities.join(",")}`);
|
|
299
|
+
// An address only means something to routes of its own family.
|
|
300
|
+
if (set.next_hop && Address.family(set.next_hop) === family) steps.push(`set gw ${set.next_hop}`);
|
|
301
|
+
if (set.preferred_source && Address.family(set.preferred_source) === family) steps.push(`set pref-src ${set.preferred_source}`);
|
|
302
|
+
if (set.blackhole) steps.push("set blackhole yes");
|
|
303
|
+
if (rule.action) steps.push(rule.action);
|
|
304
|
+
return steps;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** One rule as the platform writes it: hand off, ask the validator, test, change, decide. */
|
|
308
|
+
private static ruleText(rule: AnyRule, family: Family): string {
|
|
309
|
+
const parts: string[] = [];
|
|
310
|
+
if (rule.call) parts.push(`jump ${RouterOsRender.chainName(rule.call, family)}`);
|
|
311
|
+
// An RPKI state is only known once the validator has been asked, per route, so the question goes on the same rule.
|
|
312
|
+
if (rule.match?.rpki) parts.push(`rpki-verify ${RouterOsRender.RPKI_GROUP}`);
|
|
313
|
+
|
|
314
|
+
const tests = RouterOsRender.conditions(rule);
|
|
315
|
+
const steps = RouterOsRender.actions(rule, family);
|
|
316
|
+
if (tests.length) parts.push(`if (${tests.join(" && ")})${steps.length ? ` { ${steps.join("; ")} }` : ""}`);
|
|
317
|
+
else parts.push(...steps);
|
|
318
|
+
return parts.join("; ");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** SHA-256 of the certificate's DER encoding, lowercase hex, as the platform prints it. */
|
|
322
|
+
private static fingerprint(pem: string): string {
|
|
323
|
+
return new X509Certificate(pem).fingerprint256.replace(/:/g, "").toLowerCase();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Every topic at or above a severity. Emergency and alert are logged as critical; the platform has no notice topic. */
|
|
327
|
+
private static topicsFrom(level: Severity): string[] {
|
|
328
|
+
const order = RouterOsRender.SEVERITY_ORDER;
|
|
329
|
+
// Emergency and alert fall back to critical, the most severe topic the platform has.
|
|
330
|
+
const threshold = Math.max(order.indexOf(level), order.indexOf("critical"));
|
|
331
|
+
|
|
332
|
+
return RouterOsRender.TOPICS.filter(([severity]) => order.indexOf(severity) <= threshold).map(([, topic]) => topic);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Add a line to a menu as it is. */
|
|
336
|
+
private addLine(menu: string, line: string): void {
|
|
337
|
+
const commands = this.menus.get(menu) ?? [];
|
|
338
|
+
commands.push(line);
|
|
339
|
+
this.menus.set(menu, commands);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Add a command to a menu. */
|
|
343
|
+
private add(menu: string, verb: string, properties: Record<string, Value>): void {
|
|
344
|
+
this.addLine(menu, RouterOsRender.command(verb, properties));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** The whole configuration as text. */
|
|
348
|
+
private text(): string {
|
|
349
|
+
const blocks = [...this.menus].map(([menu, commands]) => [menu, ...commands].join("\n"));
|
|
350
|
+
return blocks.join("\n") + "\n";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** What the platform calls a port or an interface. A port is spelled by the model; an interface keeps its config name. */
|
|
354
|
+
private name(portOrInterface: string): string {
|
|
355
|
+
if (portOrInterface in this.interfaces) return portOrInterface;
|
|
356
|
+
|
|
357
|
+
return Catalog.spell(this.device.platform, this.device.model, portOrInterface);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** The number of a VLAN, by its config name. */
|
|
361
|
+
private vlanId(vlan: string): number {
|
|
362
|
+
const declared = this.device.vlans?.[vlan];
|
|
363
|
+
if (!declared) throw new Error(`${this.device.name}: VLAN ${vlan} is not declared`);
|
|
364
|
+
return declared.id;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Ports and LAGs that are layer 2 members of a VLAN, with their settings. A port in a LAG is represented by the LAG. */
|
|
368
|
+
private switched(): [string, AnyPort | Extract<AnyInterface, { type: "lag" }>][] {
|
|
369
|
+
const ports = Object.entries(this.ports).filter(([, port]) => !port.lag && (port.access_vlan || port.trunk_vlans));
|
|
370
|
+
const lags = Object.entries(this.interfaces).filter((entry): entry is [string, Extract<AnyInterface, { type: "lag" }>] => {
|
|
371
|
+
const lag = entry[1];
|
|
372
|
+
return lag.type === "lag" && Boolean(lag.access_vlan || lag.trunk_vlans);
|
|
373
|
+
});
|
|
374
|
+
return [...ports, ...lags];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** VLAN interfaces, with their settings. */
|
|
378
|
+
private declaredVlanInterfaces(): [string, Extract<AnyInterface, { type: "vlan" }>][] {
|
|
379
|
+
return Object.entries(this.interfaces).filter(
|
|
380
|
+
(
|
|
381
|
+
entry
|
|
382
|
+
): entry is [
|
|
383
|
+
string,
|
|
384
|
+
Extract<
|
|
385
|
+
AnyInterface,
|
|
386
|
+
{
|
|
387
|
+
type: "vlan";
|
|
388
|
+
}
|
|
389
|
+
>,
|
|
390
|
+
] => entry[1].type === "vlan"
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Every port and interface that holds addresses or other layer 3 settings. Ports are named as the platform spells them. */
|
|
395
|
+
private routed(): { name: string; settings: AnyPort | AnyInterface }[] {
|
|
396
|
+
const ports = Object.entries(this.ports).map(([port, settings]) => ({ name: this.name(port), settings }));
|
|
397
|
+
const interfaces = Object.entries(this.interfaces).map(([name, settings]) => ({ name, settings }));
|
|
398
|
+
return [...ports, ...interfaces];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private interfaceMenus(): void {
|
|
402
|
+
this.hardwareOffload();
|
|
403
|
+
this.bridge();
|
|
404
|
+
this.ethernet();
|
|
405
|
+
this.switchPorts();
|
|
406
|
+
this.lags();
|
|
407
|
+
this.vlanInterfaces();
|
|
408
|
+
this.tunnels();
|
|
409
|
+
this.wireguard();
|
|
410
|
+
this.vrrp();
|
|
411
|
+
this.bridgePorts();
|
|
412
|
+
this.bridgeVlans();
|
|
413
|
+
this.bridgeFilters();
|
|
414
|
+
this.interfaceLists();
|
|
415
|
+
this.vrfs();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Routing in the switch chip. With it on, routed traffic bypasses the forward chain unless its interface opts out.
|
|
420
|
+
*
|
|
421
|
+
* Restarting the offload after a failure, and offloading IPv6 too, follow the same switch.
|
|
422
|
+
*/
|
|
423
|
+
private hardwareOffload(): void {
|
|
424
|
+
const on = RouterOsRender.yesNo(this.device.hardware_offload);
|
|
425
|
+
this.add("/interface ethernet switch", "set switch1", { "l3-hw-offloading": on });
|
|
426
|
+
this.add("/interface ethernet switch l3hw-settings", "set", { autorestart: on, "ipv6-hw": on });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** One VLAN-filtering bridge carries every switched port. A loopback is a bridge with no ports, the platform's idiom for one. */
|
|
430
|
+
private bridge(): void {
|
|
431
|
+
const { device } = this;
|
|
432
|
+
const needsBridge = this.switched().length > 0 || this.declaredVlanInterfaces().length > 0;
|
|
433
|
+
|
|
434
|
+
if (needsBridge) {
|
|
435
|
+
const stp = device.stp;
|
|
436
|
+
const protocolMode = stp ? (stp.mode ?? "rstp") : "none";
|
|
437
|
+
const priority = stp?.priority === undefined ? undefined : `0x${stp.priority.toString(16)}`;
|
|
438
|
+
this.add("/interface bridge", "add", {
|
|
439
|
+
name: RouterOsRender.BRIDGE,
|
|
440
|
+
"protocol-mode": protocolMode,
|
|
441
|
+
priority,
|
|
442
|
+
"vlan-filtering": "yes",
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (const [name, iface] of Object.entries(this.interfaces)) {
|
|
447
|
+
if (iface.type !== "loopback") continue;
|
|
448
|
+
|
|
449
|
+
this.add("/interface bridge", "add", { comment: iface.description, name });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Every port of the model is stated. One the config does not declare is disabled. */
|
|
454
|
+
private ethernet(): void {
|
|
455
|
+
const { device } = this;
|
|
456
|
+
|
|
457
|
+
for (const port of Catalog.ports(device.platform, device.model)) {
|
|
458
|
+
const selector = `set [ find default-name=${Catalog.spell(device.platform, device.model, port)} ]`;
|
|
459
|
+
const settings = this.ports[port];
|
|
460
|
+
if (!settings) {
|
|
461
|
+
this.add("/interface ethernet", selector, { disabled: "yes" });
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const fixedSpeed = settings.speed && RouterOsRender.medium(Catalog.spell(device.platform, device.model, port), settings.speed);
|
|
466
|
+
|
|
467
|
+
this.add("/interface ethernet", selector, {
|
|
468
|
+
"auto-negotiation": fixedSpeed ? "no" : undefined,
|
|
469
|
+
comment: settings.description,
|
|
470
|
+
mtu: settings.mtu,
|
|
471
|
+
speed: fixedSpeed,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Storm control in the switch chip. The platform takes one rate, in percent, and which kinds of flooded traffic it limits.
|
|
478
|
+
*
|
|
479
|
+
* `unsupported()` refuses packets per second and different percentages per kind, so here they are always one shared percent.
|
|
480
|
+
*/
|
|
481
|
+
private switchPorts(): void {
|
|
482
|
+
for (const [port, settings] of Object.entries(this.ports)) {
|
|
483
|
+
const storm = settings.storm_control;
|
|
484
|
+
if (!storm) continue;
|
|
485
|
+
|
|
486
|
+
const thresholds = [storm.broadcast, storm.multicast, storm.unknown_unicast].filter((threshold) => threshold !== undefined);
|
|
487
|
+
const percent = thresholds.find((threshold) => "percent" in threshold);
|
|
488
|
+
if (!percent || !("percent" in percent)) continue;
|
|
489
|
+
|
|
490
|
+
this.add("/interface ethernet switch port", `set ${this.name(port)}`, {
|
|
491
|
+
"limit-broadcasts": RouterOsRender.yesNo(storm.broadcast !== undefined),
|
|
492
|
+
"limit-unknown-multicasts": RouterOsRender.yesNo(storm.multicast !== undefined),
|
|
493
|
+
"limit-unknown-unicasts": RouterOsRender.yesNo(storm.unknown_unicast !== undefined),
|
|
494
|
+
"storm-rate": percent.percent,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** A LAG is a bonding interface. Its members are named as the platform spells them. */
|
|
500
|
+
private lags(): void {
|
|
501
|
+
for (const [name, lag] of Object.entries(this.interfaces)) {
|
|
502
|
+
if (lag.type !== "lag") continue;
|
|
503
|
+
|
|
504
|
+
const members = Object.entries(this.ports)
|
|
505
|
+
.filter(([, port]) => port.lag === name)
|
|
506
|
+
.map(([port]) => this.name(port));
|
|
507
|
+
const mode = lag.mode === "static" ? "balance-xor" : "802.3ad";
|
|
508
|
+
this.add("/interface bonding", "add", {
|
|
509
|
+
comment: lag.description,
|
|
510
|
+
mode,
|
|
511
|
+
mtu: lag.mtu,
|
|
512
|
+
name,
|
|
513
|
+
slaves: members.join(","),
|
|
514
|
+
"transmit-hash-policy": "layer-2-and-3",
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* A VLAN interface sits on the bridge. When the device offloads, one that opts out is routed in software so the firewall applies.
|
|
521
|
+
*
|
|
522
|
+
* Every VLAN interface on the bridge has the bridge's MAC. One with a MAC of its own is a MACVLAN on top of the VLAN interface: the MACVLAN takes the configured name, so its addresses and every rule naming it land on it.
|
|
523
|
+
*/
|
|
524
|
+
private vlanInterfaces(): void {
|
|
525
|
+
for (const [name, iface] of this.declaredVlanInterfaces()) {
|
|
526
|
+
const softwareOnly = this.device.hardware_offload && iface.hardware_offload === false;
|
|
527
|
+
const vlanName = iface.mac ? RouterOsRender.underMacvlan(name) : name;
|
|
528
|
+
|
|
529
|
+
this.add("/interface vlan", "add", {
|
|
530
|
+
comment: iface.description,
|
|
531
|
+
interface: RouterOsRender.BRIDGE,
|
|
532
|
+
"l3-hw-offloading": softwareOnly ? "no" : undefined,
|
|
533
|
+
mtu: iface.mtu,
|
|
534
|
+
name: vlanName,
|
|
535
|
+
"vlan-id": this.vlanId(iface.vlan),
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
if (iface.mac) {
|
|
539
|
+
this.add("/interface macvlan", "add", {
|
|
540
|
+
comment: iface.description,
|
|
541
|
+
interface: vlanName,
|
|
542
|
+
"mac-address": iface.mac.toUpperCase(),
|
|
543
|
+
mtu: iface.mtu,
|
|
544
|
+
name,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private tunnels(): void {
|
|
551
|
+
for (const [name, iface] of Object.entries(this.interfaces)) {
|
|
552
|
+
if (iface.type === "gre") {
|
|
553
|
+
this.add("/interface gre", "add", {
|
|
554
|
+
comment: iface.description,
|
|
555
|
+
"local-address": iface.local,
|
|
556
|
+
mtu: iface.mtu,
|
|
557
|
+
name,
|
|
558
|
+
"remote-address": iface.remote,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (iface.type === "vxlan") {
|
|
563
|
+
// 4789 is the platform's default and is not written.
|
|
564
|
+
const port = iface.port && iface.port !== 4789 ? iface.port : undefined;
|
|
565
|
+
this.add("/interface vxlan", "add", {
|
|
566
|
+
comment: iface.description,
|
|
567
|
+
"local-address": iface.local,
|
|
568
|
+
"mac-address": iface.mac,
|
|
569
|
+
mtu: iface.mtu,
|
|
570
|
+
name,
|
|
571
|
+
port,
|
|
572
|
+
vni: iface.vni,
|
|
573
|
+
});
|
|
574
|
+
this.add("/interface vxlan vteps", "add", { interface: name, "remote-ip": iface.remote });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private wireguard(): void {
|
|
580
|
+
for (const [name, iface] of Object.entries(this.interfaces)) {
|
|
581
|
+
if (iface.type !== "wireguard") continue;
|
|
582
|
+
|
|
583
|
+
this.add("/interface wireguard", "add", {
|
|
584
|
+
comment: iface.description,
|
|
585
|
+
"listen-port": iface.listen_port,
|
|
586
|
+
mtu: iface.mtu,
|
|
587
|
+
name,
|
|
588
|
+
"private-key": Secrets.marker(iface.private_key),
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
for (const peer of iface.peers ?? []) {
|
|
592
|
+
const endpoint = peer.endpoint ? RouterOsRender.splitEndpoint(peer.endpoint) : undefined;
|
|
593
|
+
this.add("/interface wireguard peers", "add", {
|
|
594
|
+
"allowed-address": peer.allowed_addresses.join(","),
|
|
595
|
+
"client-allowed-address": peer.client_allowed_addresses?.join(","),
|
|
596
|
+
"endpoint-address": endpoint?.host,
|
|
597
|
+
"endpoint-port": endpoint?.port,
|
|
598
|
+
interface: name,
|
|
599
|
+
// Peer names are unique across the device, so they carry the interface.
|
|
600
|
+
name: `${name}-${peer.name}`,
|
|
601
|
+
"persistent-keepalive": peer.keepalive ? RouterOsRender.duration(peer.keepalive) : undefined,
|
|
602
|
+
"public-key": peer.public_key,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** Each VRRP group is an interface of its own, holding the shared address. Version 3 carries IPv6. */
|
|
609
|
+
private vrrp(): void {
|
|
610
|
+
for (const { name, settings } of this.routed()) {
|
|
611
|
+
for (const group of settings.vrrp ?? []) {
|
|
612
|
+
const ipv6 = Address.family(group.address) === "ipv6";
|
|
613
|
+
this.add("/interface vrrp", "add", {
|
|
614
|
+
interface: name,
|
|
615
|
+
interval: group.interval ? RouterOsRender.duration(group.interval) : undefined,
|
|
616
|
+
name: RouterOsRender.vrrpName(name, group.id),
|
|
617
|
+
"preemption-mode": RouterOsRender.yesNo(group.preempt),
|
|
618
|
+
priority: group.priority,
|
|
619
|
+
"v3-protocol": ipv6 ? "ipv6" : undefined,
|
|
620
|
+
version: ipv6 ? 3 : undefined,
|
|
621
|
+
vrid: group.id,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** A switched port or LAG joins the bridge. Its untagged VLAN is its port VLAN ID. */
|
|
628
|
+
private bridgePorts(): void {
|
|
629
|
+
for (const [port, settings] of this.switched()) {
|
|
630
|
+
const isTrunk = Boolean(settings.trunk_vlans);
|
|
631
|
+
const untagged = isTrunk ? settings.native_vlan : settings.access_vlan;
|
|
632
|
+
const pvid = untagged ? this.vlanId(untagged) : undefined;
|
|
633
|
+
|
|
634
|
+
// A trunk without a native VLAN takes tagged frames only; an access port untagged only.
|
|
635
|
+
let frameTypes: string | undefined;
|
|
636
|
+
if (!isTrunk) frameTypes = "admit-only-untagged-and-priority-tagged";
|
|
637
|
+
else if (pvid === undefined) frameTypes = "admit-only-vlan-tagged";
|
|
638
|
+
|
|
639
|
+
const stp = typeof settings.stp === "object" ? settings.stp : undefined;
|
|
640
|
+
this.add("/interface bridge port", "add", {
|
|
641
|
+
bridge: RouterOsRender.BRIDGE,
|
|
642
|
+
edge: stp?.edge ? "yes" : undefined,
|
|
643
|
+
"frame-types": frameTypes,
|
|
644
|
+
interface: this.name(port),
|
|
645
|
+
"path-cost": stp?.cost,
|
|
646
|
+
priority: stp?.priority === undefined ? undefined : `0x${stp.priority.toString(16)}`,
|
|
647
|
+
pvid,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** A VLAN is tagged on every trunk carrying it, and on the bridge itself when the router has an interface in it. */
|
|
653
|
+
private bridgeVlans(): void {
|
|
654
|
+
for (const [vlanName, vlan] of Object.entries(this.device.vlans ?? {})) {
|
|
655
|
+
const routedHere = this.declaredVlanInterfaces().some(([, iface]) => iface.vlan === vlanName);
|
|
656
|
+
const trunks = this.switched()
|
|
657
|
+
.filter(([, settings]) => settings.trunk_vlans?.includes(vlanName))
|
|
658
|
+
.map(([port]) => this.name(port));
|
|
659
|
+
const tagged = [...(routedHere ? [RouterOsRender.BRIDGE] : []), ...trunks];
|
|
660
|
+
if (!tagged.length) continue;
|
|
661
|
+
|
|
662
|
+
this.add("/interface bridge vlan", "add", {
|
|
663
|
+
bridge: RouterOsRender.BRIDGE,
|
|
664
|
+
comment: vlan.description,
|
|
665
|
+
tagged: tagged.join(","),
|
|
666
|
+
"vlan-ids": vlan.id,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** The platform cannot take a port out of spanning tree, so `stp: false` drops the BPDUs it would send and receive. */
|
|
672
|
+
private bridgeFilters(): void {
|
|
673
|
+
for (const [port, settings] of this.switched()) {
|
|
674
|
+
if (settings.stp !== false) continue;
|
|
675
|
+
|
|
676
|
+
const name = this.name(port);
|
|
677
|
+
const comment = `${name} out of spanning tree`;
|
|
678
|
+
this.add("/interface bridge filter", "add", {
|
|
679
|
+
action: "drop",
|
|
680
|
+
chain: "input",
|
|
681
|
+
comment,
|
|
682
|
+
"dst-mac-address": RouterOsRender.BPDU_DESTINATION,
|
|
683
|
+
"in-interface": name,
|
|
684
|
+
});
|
|
685
|
+
this.add("/interface bridge filter", "add", {
|
|
686
|
+
action: "drop",
|
|
687
|
+
chain: "output",
|
|
688
|
+
comment,
|
|
689
|
+
"dst-mac-address": RouterOsRender.BPDU_DESTINATION,
|
|
690
|
+
"out-interface": name,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Interface lists scope what works at layer 2: neighbor discovery, and the platform's own client over MAC. The IP firewall never sees either.
|
|
697
|
+
*/
|
|
698
|
+
private interfaceLists(): void {
|
|
699
|
+
const lists = new Map<string, string[]>();
|
|
700
|
+
|
|
701
|
+
const lldp = this.lldpMembers();
|
|
702
|
+
if (lldp) lists.set("lldp", lldp);
|
|
703
|
+
|
|
704
|
+
const native = this.device.management?.native?.interfaces;
|
|
705
|
+
if (native)
|
|
706
|
+
lists.set(
|
|
707
|
+
"native",
|
|
708
|
+
native.map((member) => this.name(member))
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
for (const list of lists.keys()) this.add("/interface list", "add", { name: list });
|
|
712
|
+
for (const [list, members] of lists)
|
|
713
|
+
for (const member of members)
|
|
714
|
+
this.add("/interface list member", "add", {
|
|
715
|
+
interface: member,
|
|
716
|
+
list,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Where LLDP runs, when it is limited to some ports. Undefined when it runs everywhere or nowhere, which the discovery settings say directly.
|
|
722
|
+
*/
|
|
723
|
+
private lldpMembers(): string[] | undefined {
|
|
724
|
+
const lldp = this.device.lldp;
|
|
725
|
+
if (!lldp) return undefined;
|
|
726
|
+
|
|
727
|
+
const overridden = Object.entries(this.ports).filter(([, port]) => port.lldp !== undefined);
|
|
728
|
+
if (lldp === true && !overridden.length) return undefined;
|
|
729
|
+
|
|
730
|
+
const base = lldp === true ? Object.keys(this.ports) : [...lldp.interfaces];
|
|
731
|
+
const members = new Set(base.map((member) => this.name(member)));
|
|
732
|
+
|
|
733
|
+
for (const [port, settings] of overridden) {
|
|
734
|
+
if (settings.lldp) members.add(this.name(port));
|
|
735
|
+
else members.delete(this.name(port));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return [...members];
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/** A VRF lists its interfaces. */
|
|
742
|
+
private vrfs(): void {
|
|
743
|
+
for (const [vrf, settings] of Object.entries(this.device.vrfs ?? {})) {
|
|
744
|
+
const members = this.routed()
|
|
745
|
+
.filter(({ settings: iface }) => iface.vrf === vrf)
|
|
746
|
+
.map(({ name }) => name);
|
|
747
|
+
this.add("/ip vrf", "add", { comment: settings.description, interfaces: members.join(","), name: vrf });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private addressing(): void {
|
|
752
|
+
this.addresses();
|
|
753
|
+
this.dhcp();
|
|
754
|
+
this.dns();
|
|
755
|
+
this.addressLists();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** Every address goes on the menu of its family, with the interface's description as its comment. */
|
|
759
|
+
private addresses(): void {
|
|
760
|
+
for (const { name, settings } of this.routed()) {
|
|
761
|
+
for (const entry of settings.addresses ?? []) {
|
|
762
|
+
const address = typeof entry === "string" ? entry : entry.address;
|
|
763
|
+
// The far end of a point-to-point address, which the platform calls its network.
|
|
764
|
+
const peer = typeof entry === "string" ? undefined : entry.peer;
|
|
765
|
+
|
|
766
|
+
if (Address.family(address) === "ipv4") {
|
|
767
|
+
this.add("/ip address", "add", { address, comment: settings.description, interface: name, network: peer });
|
|
768
|
+
} else {
|
|
769
|
+
this.add("/ipv6 address", "add", {
|
|
770
|
+
address,
|
|
771
|
+
advertise: RouterOsRender.yesNo(settings.ipv6_ra),
|
|
772
|
+
comment: settings.description,
|
|
773
|
+
interface: name,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// A VRRP address lives on the group's own interface, as a host address.
|
|
779
|
+
for (const group of settings.vrrp ?? []) {
|
|
780
|
+
const vrrpInterface = RouterOsRender.vrrpName(name, group.id);
|
|
781
|
+
const comment = `VRRP ${group.id} on ${name}`;
|
|
782
|
+
if (Address.family(group.address) === "ipv4") {
|
|
783
|
+
this.add("/ip address", "add", { address: `${group.address}/32`, comment, interface: vrrpInterface });
|
|
784
|
+
} else {
|
|
785
|
+
this.add("/ipv6 address", "add", {
|
|
786
|
+
address: `${group.address}/128`,
|
|
787
|
+
advertise: "no",
|
|
788
|
+
comment,
|
|
789
|
+
interface: vrrpInterface,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private dhcp(): void {
|
|
797
|
+
for (const [name, server] of Object.entries(this.device.dhcp ?? {})) {
|
|
798
|
+
const [first, last] = server.pool;
|
|
799
|
+
// The pool shares the server's name.
|
|
800
|
+
this.add("/ip pool", "add", { name, ranges: `${first}-${last}` });
|
|
801
|
+
this.add("/ip dhcp-server", "add", {
|
|
802
|
+
"address-pool": name,
|
|
803
|
+
interface: this.name(server.interface),
|
|
804
|
+
"lease-time": server.lease_time ? RouterOsRender.duration(server.lease_time) : undefined,
|
|
805
|
+
name,
|
|
806
|
+
});
|
|
807
|
+
this.add("/ip dhcp-server network", "add", {
|
|
808
|
+
address: server.network,
|
|
809
|
+
"dns-server": server.dns?.join(","),
|
|
810
|
+
gateway: server.gateway,
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
for (const reservation of server.reservations ?? []) {
|
|
814
|
+
this.add("/ip dhcp-server lease", "add", {
|
|
815
|
+
address: reservation.address,
|
|
816
|
+
comment: reservation.description,
|
|
817
|
+
"mac-address": reservation.mac,
|
|
818
|
+
server: name,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
for (const [name, relay] of Object.entries(this.device.dhcp_relay ?? {})) {
|
|
824
|
+
this.add("/ip dhcp-relay", "add", { "dhcp-server": relay.servers.join(","), interface: this.name(relay.interface), name });
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
private dns(): void {
|
|
829
|
+
const dns = this.device.system?.dns;
|
|
830
|
+
this.add("/ip dns", "set", { "allow-remote-requests": RouterOsRender.yesNo(dns?.serve), servers: dns?.servers.join(",") ?? "" });
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Address lists come from firewall address sets, prefix sets typed in the config, and what BGP originates.
|
|
835
|
+
*
|
|
836
|
+
* A prefix set fetched from a registry is filled by `refresh`, so only its name is marked, and the plan leaves its entries alone.
|
|
837
|
+
*/
|
|
838
|
+
private addressLists(): void {
|
|
839
|
+
const { device } = this;
|
|
840
|
+
const entries: { list: string; address: string }[] = [];
|
|
841
|
+
|
|
842
|
+
for (const [list, members] of Object.entries(device.firewall?.address_sets ?? {})) {
|
|
843
|
+
for (const address of members) entries.push({ list, address });
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
for (const [list, set] of Object.entries(device.prefix_sets ?? {})) {
|
|
847
|
+
if ("prefixes" in set) for (const address of set.prefixes) entries.push({ list, address });
|
|
848
|
+
else this.addLine("/ip firewall address-list", `# generated ${list}`);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
for (const prefix of device.routing?.bgp?.networks ?? []) {
|
|
852
|
+
entries.push({ list: RouterOsRender.NETWORKS[Address.family(Address.host(prefix))], address: prefix });
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
for (const family of ["ipv4", "ipv6"] as const) {
|
|
856
|
+
const menu = family === "ipv4" ? "/ip firewall address-list" : "/ipv6 firewall address-list";
|
|
857
|
+
|
|
858
|
+
for (const { list, address } of entries) {
|
|
859
|
+
if (Address.family(Address.host(address)) !== family) continue;
|
|
860
|
+
|
|
861
|
+
this.add(menu, "add", { address, list });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
private firewall(): void {
|
|
867
|
+
for (const family of ["ipv4", "ipv6"] as const) {
|
|
868
|
+
this.untracked(family);
|
|
869
|
+
this.filter(family);
|
|
870
|
+
this.nat(family);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
this.helpers();
|
|
874
|
+
this.switchAcls();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Untracked prefixes skip connection tracking in the raw table, both ways, before a connection is ever made. */
|
|
878
|
+
private untracked(family: Family): void {
|
|
879
|
+
const menu = family === "ipv4" ? "/ip firewall raw" : "/ipv6 firewall raw";
|
|
880
|
+
|
|
881
|
+
for (const prefix of this.device.firewall?.untracked ?? []) {
|
|
882
|
+
if (Address.family(prefix) !== family) continue;
|
|
883
|
+
|
|
884
|
+
for (const [direction, property] of [
|
|
885
|
+
["to", "dst-address"],
|
|
886
|
+
["from", "src-address"],
|
|
887
|
+
] as const) {
|
|
888
|
+
this.add(menu, "add", {
|
|
889
|
+
action: "notrack",
|
|
890
|
+
chain: "prerouting",
|
|
891
|
+
comment: `Untracked ${direction} ${prefix}`,
|
|
892
|
+
[property]: prefix,
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* The families a rule applies to. An address in the rule decides it; otherwise it applies to both.
|
|
900
|
+
*
|
|
901
|
+
* A rule naming an address set applies only to the families the set has members in.
|
|
902
|
+
*/
|
|
903
|
+
private families(match: FilterMatch<string, string> | undefined): Family[] {
|
|
904
|
+
const sets = this.device.firewall?.address_sets ?? {};
|
|
905
|
+
const address = match?.src ?? match?.dst;
|
|
906
|
+
|
|
907
|
+
let candidates: Family[] = ["ipv4", "ipv6"];
|
|
908
|
+
if (match?.family) candidates = [match.family];
|
|
909
|
+
else if (address) candidates = [Address.family(address)];
|
|
910
|
+
|
|
911
|
+
const setHas = (set: string | undefined, family: Family) =>
|
|
912
|
+
!set || (sets[set] ?? []).some((member) => Address.family(member) === family);
|
|
913
|
+
return candidates.filter((family) => setHas(match?.src_set, family) && setHas(match?.dst_set, family));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** The match properties of a rule, as the platform names them. */
|
|
917
|
+
private matchProperties(match: FilterMatch<string, string> | undefined, family: Family) {
|
|
918
|
+
const protocol = match?.protocol === "icmp" && family === "ipv6" ? "icmpv6" : match?.protocol;
|
|
919
|
+
const setFlags = match?.tcp_flags?.set ?? [];
|
|
920
|
+
const unsetFlags = (match?.tcp_flags?.unset ?? []).map((flag) => `!${flag}`);
|
|
921
|
+
const flags = match?.tcp_flags ? [...setFlags, ...unsetFlags].join(",") : undefined;
|
|
922
|
+
return {
|
|
923
|
+
"connection-state": match?.state?.join(","),
|
|
924
|
+
"dst-address": match?.dst,
|
|
925
|
+
"dst-address-list": match?.dst_set,
|
|
926
|
+
"dst-port": RouterOsRender.portList(match?.dst_port),
|
|
927
|
+
"in-interface": match?.in_interface && this.name(match.in_interface),
|
|
928
|
+
"out-interface": match?.out_interface && this.name(match.out_interface),
|
|
929
|
+
protocol,
|
|
930
|
+
"src-address": match?.src,
|
|
931
|
+
"src-address-list": match?.src_set,
|
|
932
|
+
"src-port": RouterOsRender.portList(match?.src_port),
|
|
933
|
+
"tcp-flags": flags,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private filter(family: Family): void {
|
|
938
|
+
const menu = family === "ipv4" ? "/ip firewall filter" : "/ipv6 firewall filter";
|
|
939
|
+
|
|
940
|
+
for (const chain of RouterOsRender.CHAINS) {
|
|
941
|
+
const settings = this.device.firewall?.filter?.[chain];
|
|
942
|
+
if (!settings) continue;
|
|
943
|
+
|
|
944
|
+
for (const rule of settings.rules ?? []) {
|
|
945
|
+
if (!this.families(rule.match).includes(family)) continue;
|
|
946
|
+
|
|
947
|
+
const match = this.matchProperties(rule.match, family);
|
|
948
|
+
// The platform's fast path takes a flow only on IPv4. The accept below still applies on both.
|
|
949
|
+
if (rule.offload && family === "ipv4") {
|
|
950
|
+
this.add(menu, "add", { action: "fasttrack-connection", chain, comment: rule.description, ...match });
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
this.add(menu, "add", { action: rule.action, chain, comment: rule.description, ...match });
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (settings.default === "drop") this.add(menu, "add", { action: "drop", chain, comment: "Default" });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
private nat(family: Family): void {
|
|
961
|
+
const menu = family === "ipv4" ? "/ip firewall nat" : "/ipv6 firewall nat";
|
|
962
|
+
const chains: [string, readonly NatRule<string, string>[] | undefined][] = [
|
|
963
|
+
["srcnat", this.device.firewall?.nat?.source],
|
|
964
|
+
["dstnat", this.device.firewall?.nat?.destination],
|
|
965
|
+
];
|
|
966
|
+
|
|
967
|
+
for (const [chain, rules] of chains) {
|
|
968
|
+
for (const rule of rules ?? []) {
|
|
969
|
+
if (!this.families(rule.match).includes(family)) continue;
|
|
970
|
+
// A translation to an address belongs to that address's family.
|
|
971
|
+
if (rule.action !== "masquerade" && Address.family(rule.to) !== family) continue;
|
|
972
|
+
|
|
973
|
+
const action =
|
|
974
|
+
rule.action === "masquerade"
|
|
975
|
+
? {
|
|
976
|
+
action: "masquerade",
|
|
977
|
+
}
|
|
978
|
+
: { action: rule.action === "snat" ? "src-nat" : "dst-nat", "to-addresses": rule.to, "to-ports": rule.to_port };
|
|
979
|
+
this.add(menu, "add", { ...action, chain, comment: rule.description, ...this.matchProperties(rule.match, family) });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private helpers(): void {
|
|
985
|
+
const enabled = this.device.firewall?.helpers ?? [];
|
|
986
|
+
|
|
987
|
+
for (const helper of RouterOsRender.HELPERS) {
|
|
988
|
+
this.add("/ip firewall service-port", `set ${helper}`, { disabled: RouterOsRender.yesNo(!enabled.includes(helper)) });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* ACLs run in the switch chip on the port traffic arrives on. The chip matches addresses with a dotted mask on IPv4, drops by forwarding to no port, and limits by rate. A rule takes one port or range, so a list becomes one rule for each.
|
|
994
|
+
*/
|
|
995
|
+
private switchAcls(): void {
|
|
996
|
+
for (const { port, rule, srcPort, dstPort } of RouterOsRender.switchRules(this.device)) {
|
|
997
|
+
const match = rule.match ?? {};
|
|
998
|
+
const ipv6 = match.family === "ipv6" || [match.src, match.dst].some((address) => address && Address.family(address) === "ipv6");
|
|
999
|
+
const address = (prefix: string | undefined) => (prefix && !ipv6 ? RouterOsRender.dottedMask(prefix) : prefix);
|
|
1000
|
+
|
|
1001
|
+
this.add("/interface ethernet switch rule", "add", {
|
|
1002
|
+
comment: rule.description,
|
|
1003
|
+
[ipv6 ? "dst-address6" : "dst-address"]: address(match.dst),
|
|
1004
|
+
"dst-port": dstPort,
|
|
1005
|
+
"mac-protocol": ipv6 ? "ipv6" : "ip",
|
|
1006
|
+
"new-dst-ports": rule.action === "drop" ? "" : undefined,
|
|
1007
|
+
ports: this.name(port),
|
|
1008
|
+
protocol: match.protocol,
|
|
1009
|
+
rate: rule.rate && RouterOsRender.bitrate(rule.rate),
|
|
1010
|
+
[ipv6 ? "src-address6" : "src-address"]: address(match.src),
|
|
1011
|
+
"src-port": srcPort,
|
|
1012
|
+
switch: "switch1",
|
|
1013
|
+
"vlan-id": match.vlan && this.vlanId(match.vlan),
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
this.ethertypeFilters();
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* A port that takes only some kinds of frame: a rule that lets each kind through, then one that drops the rest. The chip takes the first rule that matches on the port, so these come after the port's ACL, whose rules match only IPv4 or IPv6.
|
|
1022
|
+
*/
|
|
1023
|
+
private ethertypeFilters(): void {
|
|
1024
|
+
for (const { port, ethertypes } of RouterOsRender.ethertypeFilters(this.device)) {
|
|
1025
|
+
const name = this.name(port);
|
|
1026
|
+
|
|
1027
|
+
for (const ethertype of ethertypes) {
|
|
1028
|
+
this.add("/interface ethernet switch rule", "add", {
|
|
1029
|
+
comment: `${name} takes ${ethertype}`,
|
|
1030
|
+
"mac-protocol": RouterOsRender.MAC_PROTOCOLS[ethertype],
|
|
1031
|
+
ports: name,
|
|
1032
|
+
switch: "switch1",
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
this.add("/interface ethernet switch rule", "add", {
|
|
1037
|
+
comment: `${name} takes nothing else`,
|
|
1038
|
+
"new-dst-ports": "",
|
|
1039
|
+
ports: name,
|
|
1040
|
+
switch: "switch1",
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** The ports with an ethertype filter, with the kinds each takes. Each makes one switch rule per kind and one more. */
|
|
1046
|
+
static ethertypeFilters(device: Device): { port: string; ethertypes: readonly Ethertype[] }[] {
|
|
1047
|
+
return Object.entries(device.ports ?? {}).flatMap(([port, settings]) =>
|
|
1048
|
+
settings?.ethertypes ? [{ port, ethertypes: settings.ethertypes }] : []
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** 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. */
|
|
1053
|
+
static switchRules(device: Device): SwitchRule[] {
|
|
1054
|
+
const each = (ports: Ports | undefined) => (ports === undefined ? [undefined] : RouterOsRender.eachPort(ports));
|
|
1055
|
+
const rules: SwitchRule[] = [];
|
|
1056
|
+
|
|
1057
|
+
for (const [port, settings] of Object.entries(device.ports ?? {})) {
|
|
1058
|
+
if (!settings?.acl) continue;
|
|
1059
|
+
|
|
1060
|
+
for (const rule of device.acls?.[settings.acl] ?? []) {
|
|
1061
|
+
for (const srcPort of each(rule.match?.src_port)) {
|
|
1062
|
+
for (const dstPort of each(rule.match?.dst_port)) rules.push({ port, rule, srcPort, dstPort });
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return rules;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
private routing(): void {
|
|
1071
|
+
this.staticRoutes();
|
|
1072
|
+
this.rpki();
|
|
1073
|
+
if (this.device.routing?.bgp) {
|
|
1074
|
+
this.bgpInstance();
|
|
1075
|
+
this.policies();
|
|
1076
|
+
this.bgpConnections();
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
this.ospf();
|
|
1080
|
+
this.bfd();
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
private staticRoutes(): void {
|
|
1084
|
+
for (const route of this.device.routing?.static ?? []) {
|
|
1085
|
+
const menu = Address.family(Address.host(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
|
|
1086
|
+
const verb = route.blackhole ? "add blackhole" : "add";
|
|
1087
|
+
const gateway = route.via ?? (route.interface ? this.name(route.interface) : undefined);
|
|
1088
|
+
this.add(menu, verb, {
|
|
1089
|
+
comment: route.description,
|
|
1090
|
+
distance: route.distance,
|
|
1091
|
+
"dst-address": route.prefix,
|
|
1092
|
+
gateway,
|
|
1093
|
+
"routing-table": route.vrf,
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private rpki(): void {
|
|
1099
|
+
for (const server of this.device.routing?.rpki ?? []) {
|
|
1100
|
+
this.add("/routing rpki", "add", {
|
|
1101
|
+
address: server.address,
|
|
1102
|
+
comment: server.description,
|
|
1103
|
+
group: RouterOsRender.RPKI_GROUP,
|
|
1104
|
+
port: server.port,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/** The default instance exists on every device; it is stated in the form the export prints it. */
|
|
1110
|
+
private bgpInstance(): void {
|
|
1111
|
+
const bgp = this.device.routing!.bgp!;
|
|
1112
|
+
this.add("/routing bgp instance", "add", { as: bgp.asn, name: "default", "router-id": this.device.routing?.router_id });
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/** A neighbor's settings with its group's beneath them. */
|
|
1116
|
+
private settingsOf(neighbor: AnyNeighbor): NeighborSettings<string> {
|
|
1117
|
+
const group = neighbor.group ? this.device.routing?.bgp?.groups?.[neighbor.group] : undefined;
|
|
1118
|
+
return { ...group, ...neighbor };
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/** The families a neighbor exchanges: the ones it states, or its address's. */
|
|
1122
|
+
private familiesOf(neighbor: AnyNeighbor): readonly Family[] {
|
|
1123
|
+
return this.settingsOf(neighbor).families ?? [Address.family(neighbor.address)];
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/** 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. */
|
|
1127
|
+
private appliesTo(rule: AnyRule, family: Family): boolean {
|
|
1128
|
+
const match = rule.match;
|
|
1129
|
+
if (match?.family && match.family !== family) return false;
|
|
1130
|
+
|
|
1131
|
+
if (match?.prefix && Address.family(Address.host(match.prefix)) !== family) return false;
|
|
1132
|
+
|
|
1133
|
+
if (match?.prefix_set) {
|
|
1134
|
+
const set = this.device.prefix_sets?.[match.prefix_set];
|
|
1135
|
+
if (!set) return false;
|
|
1136
|
+
|
|
1137
|
+
const firstPrefix = "source" in set ? undefined : set.prefixes[0];
|
|
1138
|
+
const setFamily = "source" in set ? set.source.family : firstPrefix && Address.family(Address.host(firstPrefix));
|
|
1139
|
+
if (setFamily !== family) return false;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
return true;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Policies become filter chains, one per family a session uses, plus every chain those call.
|
|
1147
|
+
*
|
|
1148
|
+
* A chain a session uses directly ends in a reject when its last rule does not decide, so a route nothing accepted is rejected on any platform default. A chain only called returns to its caller instead.
|
|
1149
|
+
*/
|
|
1150
|
+
private policies(): void {
|
|
1151
|
+
const bgp = this.device.routing!.bgp!;
|
|
1152
|
+
const policies = this.device.policies ?? {};
|
|
1153
|
+
|
|
1154
|
+
const wanted = new Map<string, Set<Family>>();
|
|
1155
|
+
const want = (policy: string, family: Family) => {
|
|
1156
|
+
const families = wanted.get(policy) ?? new Set();
|
|
1157
|
+
if (families.has(family)) return;
|
|
1158
|
+
|
|
1159
|
+
families.add(family);
|
|
1160
|
+
wanted.set(policy, families);
|
|
1161
|
+
for (const rule of policies[policy] ?? []) if (rule.call && this.appliesTo(rule, family)) want(rule.call, family);
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
let rejectAllNeeded = false;
|
|
1165
|
+
|
|
1166
|
+
for (const neighbor of Object.values(bgp.neighbors ?? {})) {
|
|
1167
|
+
const settings = this.settingsOf(neighbor);
|
|
1168
|
+
|
|
1169
|
+
for (const family of this.familiesOf(neighbor)) {
|
|
1170
|
+
for (const policy of [settings.import, settings.export]) {
|
|
1171
|
+
if (policy) want(policy, family);
|
|
1172
|
+
else rejectAllNeeded = true;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const called = new Set(Object.values(policies).flatMap((rules) => rules.flatMap((rule) => (rule.call ? [rule.call] : []))));
|
|
1178
|
+
|
|
1179
|
+
for (const policy of [...wanted.keys()].sort()) {
|
|
1180
|
+
for (const family of ["ipv4", "ipv6"] as const) {
|
|
1181
|
+
if (!wanted.get(policy)!.has(family)) continue;
|
|
1182
|
+
|
|
1183
|
+
const chain = RouterOsRender.chainName(policy, family);
|
|
1184
|
+
const rules = (policies[policy] ?? []).filter((rule) => this.appliesTo(rule, family));
|
|
1185
|
+
|
|
1186
|
+
for (const rule of rules) {
|
|
1187
|
+
const text = RouterOsRender.ruleText(rule, family);
|
|
1188
|
+
if (text) this.add("/routing filter rule", "add", { chain, comment: rule.description, rule: text });
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const last = rules.at(-1);
|
|
1192
|
+
const decides = last !== undefined && !last.match && !last.call && last.action !== undefined;
|
|
1193
|
+
if (!called.has(policy) && !decides) this.add("/routing filter rule", "add", { chain, rule: "reject" });
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
if (rejectAllNeeded) {
|
|
1198
|
+
for (const family of ["ipv4", "ipv6"] as const)
|
|
1199
|
+
this.add("/routing filter rule", "add", {
|
|
1200
|
+
chain: RouterOsRender.chainName(RouterOsRender.REJECT_ALL, family),
|
|
1201
|
+
rule: "reject",
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/** One connection per neighbor. The platform carries one family per connection. */
|
|
1207
|
+
private bgpConnections(): void {
|
|
1208
|
+
const bgp = this.device.routing!.bgp!;
|
|
1209
|
+
|
|
1210
|
+
for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
|
|
1211
|
+
const settings = this.settingsOf(neighbor);
|
|
1212
|
+
const [family] = this.familiesOf(neighbor) as [Family];
|
|
1213
|
+
const addressFamily = Address.family(neighbor.address);
|
|
1214
|
+
const internal = settings.remote_as === bgp.asn;
|
|
1215
|
+
const originates = bgp.networks?.some((prefix) => Address.family(Address.host(prefix)) === family);
|
|
1216
|
+
const role = settings.role ? RouterOsRender.ROLES[settings.role] : internal ? "ibgp" : "ebgp";
|
|
1217
|
+
|
|
1218
|
+
this.add("/routing bgp connection", "add", {
|
|
1219
|
+
// Only stated when the routes differ in family from the address they are carried over.
|
|
1220
|
+
afi: addressFamily === family ? undefined : family === "ipv6" ? "ipv6" : "ip",
|
|
1221
|
+
as: bgp.asn,
|
|
1222
|
+
connect: settings.passive ? "no" : undefined,
|
|
1223
|
+
"hold-time": settings.hold_time ? RouterOsRender.duration(settings.hold_time) : undefined,
|
|
1224
|
+
"input.filter": RouterOsRender.chainName(settings.import ?? RouterOsRender.REJECT_ALL, family),
|
|
1225
|
+
// The one instance bgpInstance adds. 7.23 refuses a new connection without it.
|
|
1226
|
+
instance: "default",
|
|
1227
|
+
[`input.limit-process-routes-${family}`]: settings.max_prefixes,
|
|
1228
|
+
"keepalive-time": settings.keepalive ? RouterOsRender.duration(settings.keepalive) : undefined,
|
|
1229
|
+
listen: settings.passive ? "yes" : undefined,
|
|
1230
|
+
"local.address": settings.local_address,
|
|
1231
|
+
"local.role": role,
|
|
1232
|
+
multihop: settings.multihop ? "yes" : undefined,
|
|
1233
|
+
name,
|
|
1234
|
+
"output.filter-chain": RouterOsRender.chainName(settings.export ?? RouterOsRender.REJECT_ALL, family),
|
|
1235
|
+
"output.network": originates ? RouterOsRender.NETWORKS[family] : undefined,
|
|
1236
|
+
"remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
|
|
1237
|
+
"remote.as": settings.remote_as,
|
|
1238
|
+
"tcp-md5-key": settings.password && Secrets.marker(settings.password),
|
|
1239
|
+
"use-bfd": settings.bfd ? "yes" : undefined,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/** One instance per OSPF version, an area per area and instance, and a template per interface. */
|
|
1245
|
+
private ospf(): void {
|
|
1246
|
+
const ospf = this.device.routing?.ospf;
|
|
1247
|
+
if (!ospf) return;
|
|
1248
|
+
|
|
1249
|
+
for (const family of ospf.families ?? ["ipv4"]) {
|
|
1250
|
+
const version = family === "ipv4" ? 2 : 3;
|
|
1251
|
+
const instance = `ospf-v${version}`;
|
|
1252
|
+
this.add("/routing ospf instance", "add", {
|
|
1253
|
+
name: instance,
|
|
1254
|
+
redistribute: ospf.redistribute?.join(","),
|
|
1255
|
+
"router-id": this.device.routing?.router_id,
|
|
1256
|
+
version,
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
for (const [areaId, area] of Object.entries(ospf.areas)) {
|
|
1260
|
+
const areaName = `${instance}-${areaId}`;
|
|
1261
|
+
this.add("/routing ospf area", "add", { "area-id": areaId, instance, name: areaName });
|
|
1262
|
+
|
|
1263
|
+
for (const [iface, settings] of Object.entries(area.interfaces)) {
|
|
1264
|
+
// `passive` is a flag, written without a value.
|
|
1265
|
+
const verb = settings?.passive ? "add passive" : "add";
|
|
1266
|
+
this.add("/routing ospf interface-template", verb, {
|
|
1267
|
+
area: areaName,
|
|
1268
|
+
cost: settings?.cost,
|
|
1269
|
+
interfaces: this.name(iface),
|
|
1270
|
+
type: settings?.network === "point-to-point" ? "ptp" : undefined,
|
|
1271
|
+
"use-bfd": settings?.bfd ? "yes" : undefined,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* BFD runs where a session or an OSPF interface asks for it, with the device's timers when it states them. One configuration covers every interface. A multihop session has no interface, and BFD for it is forbidden unless a configuration names its address, so each gets one of its own.
|
|
1280
|
+
*/
|
|
1281
|
+
private bfd(): void {
|
|
1282
|
+
const routing = this.device.routing;
|
|
1283
|
+
const bgpBfd = Object.values(routing?.bgp?.neighbors ?? {}).filter((neighbor) => this.settingsOf(neighbor).bfd);
|
|
1284
|
+
const multihop = bgpBfd.filter((neighbor) => this.settingsOf(neighbor).multihop);
|
|
1285
|
+
const ospfWants = Object.values(routing?.ospf?.areas ?? {}).some((area) =>
|
|
1286
|
+
Object.values(area.interfaces).some((iface) => iface?.bfd)
|
|
1287
|
+
);
|
|
1288
|
+
const timers = routing?.bfd;
|
|
1289
|
+
const interval = timers ? `${timers.interval}ms` : undefined;
|
|
1290
|
+
const timerProperties = { "min-rx": interval, "min-tx": interval, multiplier: timers?.multiplier };
|
|
1291
|
+
|
|
1292
|
+
const directWants = bgpBfd.length > multihop.length || ospfWants;
|
|
1293
|
+
if (directWants || (timers && !multihop.length)) {
|
|
1294
|
+
this.add("/routing bfd configuration", "add", { interfaces: "all", ...timerProperties });
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
for (const neighbor of multihop) {
|
|
1298
|
+
const length = Address.family(neighbor.address) === "ipv4" ? 32 : 128;
|
|
1299
|
+
this.add("/routing bfd configuration", "add", { addresses: `${neighbor.address}/${length}`, ...timerProperties });
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
private management(): void {
|
|
1304
|
+
this.services();
|
|
1305
|
+
this.layerTwo();
|
|
1306
|
+
this.snmp();
|
|
1307
|
+
this.users();
|
|
1308
|
+
this.certificates();
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/** Every service the platform has is stated. One the config does not name is disabled. */
|
|
1312
|
+
private services(): void {
|
|
1313
|
+
const management = this.device.management ?? {};
|
|
1314
|
+
|
|
1315
|
+
for (const [neutral, platform] of Object.entries(RouterOsRender.SERVICES)) {
|
|
1316
|
+
const service = management[neutral as keyof typeof RouterOsRender.SERVICES] as (Service & { certificate?: string }) | undefined;
|
|
1317
|
+
if (!service) {
|
|
1318
|
+
this.add("/ip service", `set ${platform}`, { disabled: "yes" });
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
const allow = service.allow ?? management.allow ?? [];
|
|
1323
|
+
this.add("/ip service", `set ${platform}`, {
|
|
1324
|
+
address: allow.join(","),
|
|
1325
|
+
certificate: service.certificate,
|
|
1326
|
+
disabled: "no",
|
|
1327
|
+
port: service.port,
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// A newer service with no neutral name. Off, as everything not stated is.
|
|
1332
|
+
this.add("/ip service", "set reverse-proxy", { disabled: "yes" });
|
|
1333
|
+
this.add("/ip ssh", "set", { "strong-crypto": RouterOsRender.yesNo(!management.ssh?.weak_crypto) });
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/** What works at layer 2: the platform's client over MAC, MAC ping, the bandwidth test server and neighbor discovery. */
|
|
1337
|
+
private layerTwo(): void {
|
|
1338
|
+
const nativeList = this.device.management?.native?.interfaces ? "native" : "none";
|
|
1339
|
+
this.add("/tool mac-server", "set", { "allowed-interface-list": nativeList });
|
|
1340
|
+
this.add("/tool mac-server mac-winbox", "set", { "allowed-interface-list": nativeList });
|
|
1341
|
+
this.add("/tool mac-server ping", "set", { enabled: "no" });
|
|
1342
|
+
this.add("/tool bandwidth-server", "set", { enabled: "no" });
|
|
1343
|
+
|
|
1344
|
+
let discovery = "none";
|
|
1345
|
+
if (this.lldpMembers()) discovery = "lldp";
|
|
1346
|
+
else if (this.device.lldp) discovery = "all";
|
|
1347
|
+
this.add("/ip neighbor discovery-settings", "set", { "discover-interface-list": discovery, protocol: "lldp" });
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* 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.
|
|
1352
|
+
*/
|
|
1353
|
+
private snmp(): void {
|
|
1354
|
+
const management = this.device.management ?? {};
|
|
1355
|
+
const snmp = management.snmp;
|
|
1356
|
+
|
|
1357
|
+
if (!snmp) {
|
|
1358
|
+
this.add("/snmp", "set", { enabled: "no" });
|
|
1359
|
+
this.add("/snmp community", "set [ find default=yes ]", { addresses: "::/0", name: "public" });
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const allow = (snmp.allow ?? management.allow ?? []).join(",");
|
|
1364
|
+
this.add("/snmp", "set", { contact: snmp.contact ?? "", enabled: "yes", location: snmp.location ?? "" });
|
|
1365
|
+
|
|
1366
|
+
if (snmp.community)
|
|
1367
|
+
this.add("/snmp community", "set [ find default=yes ]", { addresses: allow, name: Secrets.marker(snmp.community) });
|
|
1368
|
+
else this.add("/snmp community", "set [ find default=yes ]", { addresses: "::/0", disabled: "yes", name: "public" });
|
|
1369
|
+
|
|
1370
|
+
for (const [name, user] of Object.entries(snmp.users ?? {})) {
|
|
1371
|
+
this.add("/snmp community", "add", {
|
|
1372
|
+
addresses: allow,
|
|
1373
|
+
"authentication-password": Secrets.marker(user.auth_password),
|
|
1374
|
+
"authentication-protocol": RouterOsRender.SNMP_AUTH[user.auth],
|
|
1375
|
+
"encryption-password": Secrets.marker(user.privacy_password),
|
|
1376
|
+
"encryption-protocol": RouterOsRender.SNMP_PRIVACY[user.privacy],
|
|
1377
|
+
name,
|
|
1378
|
+
security: "private",
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
private users(): void {
|
|
1384
|
+
for (const [name, user] of Object.entries(this.device.users ?? {})) {
|
|
1385
|
+
this.add("/user", "add", {
|
|
1386
|
+
comment: user.description,
|
|
1387
|
+
group: RouterOsRender.USER_GROUPS[user.role],
|
|
1388
|
+
name,
|
|
1389
|
+
password: Secrets.marker(user.password),
|
|
1390
|
+
});
|
|
1391
|
+
for (const key of user.ssh_keys ?? []) this.add("/user ssh-keys", "add", { key, user: name });
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* 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.
|
|
1397
|
+
*/
|
|
1398
|
+
private certificates(): void {
|
|
1399
|
+
for (const [name, certificate] of Object.entries(this.device.certificates ?? {})) {
|
|
1400
|
+
this.add("/certificate", "add", {
|
|
1401
|
+
fingerprint: RouterOsRender.fingerprint(certificate.certificate),
|
|
1402
|
+
name,
|
|
1403
|
+
"private-key": RouterOsRender.yesNo(Boolean(certificate.private_key)),
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
private system(): void {
|
|
1409
|
+
const system = this.device.system ?? {};
|
|
1410
|
+
|
|
1411
|
+
this.add("/ip settings", "set", {
|
|
1412
|
+
"send-redirects": RouterOsRender.yesNo(system.ip?.icmp_redirects),
|
|
1413
|
+
"tcp-syncookies": RouterOsRender.yesNo(system.ip?.syn_cookies),
|
|
1414
|
+
});
|
|
1415
|
+
this.add("/system identity", "set", { name: this.device.name });
|
|
1416
|
+
|
|
1417
|
+
// Autodetect wins after a restart and has moved a router to another zone, so it is always off.
|
|
1418
|
+
this.add("/system clock", "set", { "time-zone-autodetect": "no", "time-zone-name": system.timezone ?? "UTC" });
|
|
1419
|
+
this.add("/system note", "set", { note: system.banner ?? "", "show-at-login": "yes" });
|
|
1420
|
+
|
|
1421
|
+
this.add("/system ntp client", "set", { enabled: RouterOsRender.yesNo(Boolean(system.ntp?.servers.length)) });
|
|
1422
|
+
this.add("/system ntp server", "set", { enabled: RouterOsRender.yesNo(system.ntp?.serve) });
|
|
1423
|
+
for (const server of system.ntp?.servers ?? []) this.add("/system ntp client servers", "add", { address: server });
|
|
1424
|
+
|
|
1425
|
+
this.logging();
|
|
1426
|
+
|
|
1427
|
+
this.add("/system package update", "set", { channel: system.release_channel ?? "stable" });
|
|
1428
|
+
// Bootloader setup only from a deliberate key on the console, so line noise on a serial cable cannot enter it.
|
|
1429
|
+
this.add("/system routerboard settings", "set", { "enter-setup-on": "delete-key" });
|
|
1430
|
+
|
|
1431
|
+
this.flowExport();
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/** Local logging uses the platform's built-in disk action, number 1. Each remote collector gets an action of its own. */
|
|
1435
|
+
private logging(): void {
|
|
1436
|
+
const logging = this.device.system?.logging;
|
|
1437
|
+
const local = logging?.local;
|
|
1438
|
+
|
|
1439
|
+
this.add("/system logging action", "set 1", {
|
|
1440
|
+
"disk-file-count": local?.files ?? 2,
|
|
1441
|
+
"disk-lines-per-file": local?.lines_per_file ?? 1000,
|
|
1442
|
+
});
|
|
1443
|
+
for (const topic of local ? RouterOsRender.topicsFrom(local.level) : []) {
|
|
1444
|
+
this.add("/system logging", "add", { action: "disk", topics: topic });
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
(logging?.remote ?? []).forEach((collector, index) => {
|
|
1448
|
+
const action = `remote${index + 1}`;
|
|
1449
|
+
this.add("/system logging action", "add", {
|
|
1450
|
+
name: action,
|
|
1451
|
+
remote: collector.address,
|
|
1452
|
+
"remote-port": collector.port,
|
|
1453
|
+
target: "remote",
|
|
1454
|
+
});
|
|
1455
|
+
for (const topic of RouterOsRender.topicsFrom(collector.level)) this.add("/system logging", "add", { action, topics: topic });
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/** NetFlow version 9 and IPFIX through traffic flow. The platform has no sFlow, which `unsupported()` reports. */
|
|
1460
|
+
private flowExport(): void {
|
|
1461
|
+
const flow = this.device.flow_export;
|
|
1462
|
+
if (!flow) {
|
|
1463
|
+
this.add("/ip traffic-flow", "set", { enabled: "no" });
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const sampled = flow.sampling !== undefined && flow.sampling > 1;
|
|
1468
|
+
const interfaces = flow.interfaces?.map((iface) => this.name(iface)).join(",") ?? "all";
|
|
1469
|
+
this.add("/ip traffic-flow", "set", {
|
|
1470
|
+
enabled: "yes",
|
|
1471
|
+
interfaces,
|
|
1472
|
+
"packet-sampling": RouterOsRender.yesNo(sampled),
|
|
1473
|
+
"sampling-interval": sampled ? flow.sampling : undefined,
|
|
1474
|
+
"sampling-space": sampled ? 1 : undefined,
|
|
1475
|
+
});
|
|
1476
|
+
|
|
1477
|
+
const version = flow.protocol === "ipfix" ? "ipfix" : "9";
|
|
1478
|
+
|
|
1479
|
+
for (const collector of flow.collectors) {
|
|
1480
|
+
this.add("/ip traffic-flow target", "add", { "dst-address": collector.address, port: collector.port ?? 2055, version });
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|