@takodotid/circuit 0.25.0 → 0.26.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 +3 -3
- package/dist/chunks/{index-wm9ypkjq.js → index-22sbskdx.js} +13 -5
- package/dist/chunks/{server-n2q4tzy4.js → server-1ftkqkx5.js} +1 -1
- package/dist/cli/index.js +2990 -264
- package/dist/types/adapters/devices/catalog.d.ts +14 -0
- package/dist/types/adapters/devices/fortios/firewall.d.ts +21 -0
- package/dist/types/adapters/devices/fortios/index.d.ts +55 -0
- package/dist/types/adapters/devices/fortios/models.d.ts +14 -0
- package/dist/types/adapters/devices/fortios/names.d.ts +32 -0
- package/dist/types/adapters/devices/fortios/plan.d.ts +26 -0
- package/dist/types/adapters/devices/fortios/render.d.ts +36 -0
- package/dist/types/adapters/devices/fortios/routing.d.ts +32 -0
- package/dist/types/adapters/devices/fortios/tree.d.ts +33 -0
- package/package.json +5 -2
- package/src/adapters/devices/catalog.ts +2 -1
- package/src/adapters/devices/fortios/TRAPS.md +40 -0
- package/src/adapters/devices/fortios/firewall.ts +352 -0
- package/src/adapters/devices/fortios/index.ts +800 -0
- package/src/adapters/devices/fortios/models.ts +7 -0
- package/src/adapters/devices/fortios/names.ts +78 -0
- package/src/adapters/devices/fortios/plan.ts +840 -0
- package/src/adapters/devices/fortios/render.ts +734 -0
- package/src/adapters/devices/fortios/routing.ts +444 -0
- package/src/adapters/devices/fortios/tree.ts +121 -0
- package/src/adapters/devices/index.ts +2 -1
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
// The neutral model as FortiOS configuration, in the form `show` prints, so the plan can compare the two section by section.
|
|
2
|
+
|
|
3
|
+
import { familyOf, host, netmask, split } from "../../../core/addr";
|
|
4
|
+
import { marker } from "../../../core/secrets";
|
|
5
|
+
import type { Device, Helper, Severity } from "../../../schema";
|
|
6
|
+
import { portsOf, spell } from "../catalog";
|
|
7
|
+
import { firewall, natRules } from "./firewall";
|
|
8
|
+
import { addressed, addressesOf, holderOf, interfaceFor, nameOf, network, subnet, vrfNumber } from "./names";
|
|
9
|
+
import { routing, usesBfd } from "./routing";
|
|
10
|
+
import { Block, print, quote, quoteAll } from "./tree";
|
|
11
|
+
|
|
12
|
+
/** Where the pre-login banner's text is kept. */
|
|
13
|
+
export const BANNER = 'system replacemsg admin "pre_admin-disclaimer-text"';
|
|
14
|
+
|
|
15
|
+
/** Every section Circuit manages, in the order a change goes in: what others name comes first. */
|
|
16
|
+
export const SECTIONS = [
|
|
17
|
+
"vpn certificate local",
|
|
18
|
+
"vpn certificate ca",
|
|
19
|
+
"system global",
|
|
20
|
+
BANNER,
|
|
21
|
+
"system settings",
|
|
22
|
+
"system accprofile",
|
|
23
|
+
"system admin",
|
|
24
|
+
"system interface",
|
|
25
|
+
"system gre-tunnel",
|
|
26
|
+
"system vxlan",
|
|
27
|
+
"system dns",
|
|
28
|
+
"system dns-server",
|
|
29
|
+
"system ntp",
|
|
30
|
+
"system snmp sysinfo",
|
|
31
|
+
"system snmp community",
|
|
32
|
+
"system snmp user",
|
|
33
|
+
"log memory setting",
|
|
34
|
+
"log memory filter",
|
|
35
|
+
"log disk setting",
|
|
36
|
+
...["", "2", "3", "4"].flatMap((number) => [`log syslogd${number} setting`, `log syslogd${number} filter`]),
|
|
37
|
+
"system sflow",
|
|
38
|
+
"system netflow",
|
|
39
|
+
"system dhcp server",
|
|
40
|
+
"router static",
|
|
41
|
+
"router static6",
|
|
42
|
+
"router prefix-list",
|
|
43
|
+
"router prefix-list6",
|
|
44
|
+
"router community-list",
|
|
45
|
+
"router route-map",
|
|
46
|
+
"router ospf",
|
|
47
|
+
"router ospf6",
|
|
48
|
+
"router bgp",
|
|
49
|
+
"system session-helper",
|
|
50
|
+
"firewall address",
|
|
51
|
+
"firewall address6",
|
|
52
|
+
"firewall addrgrp",
|
|
53
|
+
"firewall addrgrp6",
|
|
54
|
+
"firewall service custom",
|
|
55
|
+
"firewall ippool",
|
|
56
|
+
"firewall vip",
|
|
57
|
+
"firewall policy",
|
|
58
|
+
"firewall policy6",
|
|
59
|
+
"firewall local-in-policy",
|
|
60
|
+
"firewall local-in-policy6",
|
|
61
|
+
"firewall central-snat-map",
|
|
62
|
+
"firewall acl",
|
|
63
|
+
"firewall acl6",
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
export type Section = (typeof SECTIONS)[number];
|
|
67
|
+
|
|
68
|
+
/** Sections `show` prints without what matters, which `show full-configuration` prints: a certificate's text. */
|
|
69
|
+
export const FULL: readonly string[] = ["vpn certificate local", "vpn certificate ca"];
|
|
70
|
+
|
|
71
|
+
/** The FortiOS zone number of each IANA zone it has one for. A number names a list of places in one zone; `00` is the device's default. */
|
|
72
|
+
const ZONES: Record<string, readonly string[]> = {
|
|
73
|
+
"00": ["Pacific/Kwajalein"],
|
|
74
|
+
"01": ["Pacific/Midway", "Pacific/Pago_Pago"],
|
|
75
|
+
"02": ["Pacific/Honolulu"],
|
|
76
|
+
"03": ["America/Anchorage"],
|
|
77
|
+
"04": ["America/Los_Angeles", "America/Vancouver", "America/Tijuana"],
|
|
78
|
+
"05": ["America/Phoenix"],
|
|
79
|
+
"06": ["America/Denver", "America/Edmonton"],
|
|
80
|
+
"07": ["America/Guatemala", "America/Costa_Rica", "America/El_Salvador", "America/Tegucigalpa", "America/Managua"],
|
|
81
|
+
"08": ["America/Chicago", "America/Winnipeg"],
|
|
82
|
+
"09": ["America/Mexico_City"],
|
|
83
|
+
"10": ["America/Regina"],
|
|
84
|
+
"11": ["America/Bogota", "America/Lima", "America/Guayaquil"],
|
|
85
|
+
"12": ["America/New_York", "America/Toronto"],
|
|
86
|
+
"13": ["America/Indiana/Indianapolis"],
|
|
87
|
+
"14": ["America/Halifax"],
|
|
88
|
+
"15": ["America/La_Paz"],
|
|
89
|
+
"16": ["America/Santiago"],
|
|
90
|
+
"17": ["America/St_Johns"],
|
|
91
|
+
"18": ["America/Sao_Paulo"],
|
|
92
|
+
"19": ["America/Argentina/Buenos_Aires"],
|
|
93
|
+
"20": ["America/Nuuk", "America/Godthab"],
|
|
94
|
+
"21": ["Atlantic/South_Georgia"],
|
|
95
|
+
"22": ["Atlantic/Azores"],
|
|
96
|
+
"23": ["Atlantic/Cape_Verde"],
|
|
97
|
+
"24": ["Africa/Monrovia"],
|
|
98
|
+
"25": ["Europe/London", "Europe/Dublin", "Europe/Lisbon", "Atlantic/Canary"],
|
|
99
|
+
"26": ["Europe/Amsterdam", "Europe/Berlin", "Europe/Zurich", "Europe/Rome", "Europe/Stockholm", "Europe/Vienna"],
|
|
100
|
+
"27": ["Europe/Belgrade", "Europe/Bratislava", "Europe/Budapest", "Europe/Ljubljana", "Europe/Prague"],
|
|
101
|
+
"28": ["Europe/Brussels", "Europe/Copenhagen", "Europe/Madrid", "Europe/Paris"],
|
|
102
|
+
"29": ["Europe/Sarajevo", "Europe/Skopje", "Europe/Warsaw", "Europe/Zagreb"],
|
|
103
|
+
"30": ["Africa/Lagos"],
|
|
104
|
+
"31": ["Europe/Athens", "Europe/Sofia", "Europe/Vilnius"],
|
|
105
|
+
"32": ["Europe/Bucharest"],
|
|
106
|
+
"33": ["Africa/Cairo"],
|
|
107
|
+
"34": ["Africa/Harare", "Africa/Johannesburg"],
|
|
108
|
+
"35": ["Europe/Helsinki", "Europe/Riga", "Europe/Tallinn"],
|
|
109
|
+
"36": ["Asia/Jerusalem"],
|
|
110
|
+
"37": ["Asia/Baghdad"],
|
|
111
|
+
"38": ["Asia/Kuwait", "Asia/Riyadh"],
|
|
112
|
+
"39": ["Europe/Volgograd"],
|
|
113
|
+
"40": ["Africa/Nairobi"],
|
|
114
|
+
"41": ["Asia/Tehran"],
|
|
115
|
+
"42": ["Asia/Dubai", "Asia/Muscat"],
|
|
116
|
+
"43": ["Asia/Baku"],
|
|
117
|
+
"44": ["Asia/Kabul"],
|
|
118
|
+
"45": ["Asia/Yekaterinburg"],
|
|
119
|
+
"46": ["Asia/Karachi", "Asia/Tashkent"],
|
|
120
|
+
"47": ["Asia/Kolkata", "Asia/Calcutta"],
|
|
121
|
+
"48": ["Asia/Kathmandu"],
|
|
122
|
+
"49": ["Asia/Almaty", "Asia/Novosibirsk"],
|
|
123
|
+
"50": ["Asia/Dhaka"],
|
|
124
|
+
"51": ["Asia/Colombo"],
|
|
125
|
+
"52": ["Asia/Yangon", "Asia/Rangoon"],
|
|
126
|
+
"53": ["Asia/Jakarta", "Asia/Bangkok", "Asia/Ho_Chi_Minh"],
|
|
127
|
+
"54": ["Asia/Krasnoyarsk"],
|
|
128
|
+
"55": ["Asia/Shanghai", "Asia/Chongqing", "Asia/Hong_Kong", "Asia/Urumqi", "Asia/Irkutsk"],
|
|
129
|
+
"56": ["Asia/Ulaanbaatar"],
|
|
130
|
+
"57": ["Asia/Kuala_Lumpur", "Asia/Singapore"],
|
|
131
|
+
"58": ["Australia/Perth"],
|
|
132
|
+
"59": ["Asia/Taipei"],
|
|
133
|
+
"60": ["Asia/Tokyo", "Asia/Seoul"],
|
|
134
|
+
"61": ["Asia/Yakutsk"],
|
|
135
|
+
"62": ["Australia/Adelaide"],
|
|
136
|
+
"63": ["Australia/Darwin"],
|
|
137
|
+
"64": ["Australia/Brisbane"],
|
|
138
|
+
"65": ["Australia/Sydney", "Australia/Melbourne", "Australia/Canberra"],
|
|
139
|
+
"66": ["Pacific/Guam", "Pacific/Port_Moresby"],
|
|
140
|
+
"67": ["Australia/Hobart"],
|
|
141
|
+
"68": ["Asia/Vladivostok"],
|
|
142
|
+
"69": ["Asia/Magadan"],
|
|
143
|
+
"70": ["Pacific/Guadalcanal", "Pacific/Noumea"],
|
|
144
|
+
"71": ["Pacific/Auckland"],
|
|
145
|
+
"72": ["Pacific/Fiji", "Asia/Kamchatka", "Pacific/Majuro"],
|
|
146
|
+
"73": ["Pacific/Tongatapu"],
|
|
147
|
+
"74": ["America/Caracas"],
|
|
148
|
+
"75": ["America/Montevideo"],
|
|
149
|
+
"76": ["Pacific/Kiritimati"],
|
|
150
|
+
"77": ["America/Guyana"],
|
|
151
|
+
"78": ["Africa/Windhoek"],
|
|
152
|
+
"79": ["Africa/Casablanca"],
|
|
153
|
+
"80": ["UTC", "Etc/UTC", "Etc/GMT", "GMT"],
|
|
154
|
+
"81": ["America/Mazatlan", "America/Chihuahua"],
|
|
155
|
+
"82": ["Pacific/Chatham"],
|
|
156
|
+
"83": ["Europe/Moscow"],
|
|
157
|
+
"84": ["Europe/Minsk"],
|
|
158
|
+
"85": ["Europe/Istanbul"],
|
|
159
|
+
"86": ["Pacific/Apia"],
|
|
160
|
+
"87": ["America/Asuncion"],
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** The zone number for an IANA zone. UTC when the config names none. */
|
|
164
|
+
export function zoneOf(timezone = "UTC"): string | undefined {
|
|
165
|
+
return Object.entries(ZONES).find(([, places]) => places.includes(timezone))?.[0];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** What each role may do: the factory profile for an admin, and a profile Circuit adds for the others. */
|
|
169
|
+
export const PROFILE = { admin: "super_admin", operator: "circuit-operator", "read-only": "circuit-read-only" } as const;
|
|
170
|
+
|
|
171
|
+
/** The permission groups of an access profile. */
|
|
172
|
+
const PERMISSION_GROUPS = [
|
|
173
|
+
"secfabgrp",
|
|
174
|
+
"ftviewgrp",
|
|
175
|
+
"authgrp",
|
|
176
|
+
"sysgrp",
|
|
177
|
+
"netgrp",
|
|
178
|
+
"loggrp",
|
|
179
|
+
"fwgrp",
|
|
180
|
+
"vpngrp",
|
|
181
|
+
"utmgrp",
|
|
182
|
+
"wanoptgrp",
|
|
183
|
+
"wifi",
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
/** The most public keys a user holds. */
|
|
187
|
+
export const SSH_KEYS = 3;
|
|
188
|
+
|
|
189
|
+
/** The most resolvers the device and a DHCP server hold, per family. */
|
|
190
|
+
export const RESOLVERS = { device: 2, dhcp: 4 } as const;
|
|
191
|
+
|
|
192
|
+
/** The most syslog collectors the device sends to. */
|
|
193
|
+
export const COLLECTORS = 4;
|
|
194
|
+
|
|
195
|
+
/** The most VRFs the device holds besides its default table. */
|
|
196
|
+
export const VRFS = 31;
|
|
197
|
+
|
|
198
|
+
/** A severity as the platform names it. */
|
|
199
|
+
export const LEVEL: Record<Severity, string> = {
|
|
200
|
+
emergency: "emergency",
|
|
201
|
+
alert: "alert",
|
|
202
|
+
critical: "critical",
|
|
203
|
+
error: "error",
|
|
204
|
+
warning: "warning",
|
|
205
|
+
notice: "notification",
|
|
206
|
+
info: "information",
|
|
207
|
+
debug: "debug",
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** The helpers the factory runs, by the name a config gives them: protocol and port of each. */
|
|
211
|
+
export const HELPERS: Partial<Record<Helper, readonly [string, number, number][]>> = {
|
|
212
|
+
ftp: [["ftp", 6, 21]],
|
|
213
|
+
tftp: [["tftp", 17, 69]],
|
|
214
|
+
sip: [["sip", 17, 5060]],
|
|
215
|
+
h323: [
|
|
216
|
+
["h323", 6, 1720],
|
|
217
|
+
["ras", 17, 1719],
|
|
218
|
+
],
|
|
219
|
+
pptp: [["pptp", 6, 1723]],
|
|
220
|
+
rtsp: [
|
|
221
|
+
["rtsp", 6, 554],
|
|
222
|
+
["rtsp", 6, 7070],
|
|
223
|
+
["rtsp", 6, 8554],
|
|
224
|
+
],
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/** Speed by the platform's spelling, for each class a port's cage takes. */
|
|
228
|
+
export const SPEEDS: Record<string, Partial<Record<string, string>>> = {
|
|
229
|
+
"40g": { "40g": "40000full" },
|
|
230
|
+
"10g": { "10g": "10000full", "1g": "1000full" },
|
|
231
|
+
"1g": { "1g": "1000full", "100m": "100full" },
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/** The services a port or an interface answers on its addresses: ping always, and every management service the config turns on. */
|
|
235
|
+
function allowAccess(device: Device): string {
|
|
236
|
+
const management = device.management ?? {};
|
|
237
|
+
const services = ["ping"];
|
|
238
|
+
|
|
239
|
+
if (management.https) services.push("https");
|
|
240
|
+
if (management.ssh) services.push("ssh");
|
|
241
|
+
if (management.snmp) services.push("snmp");
|
|
242
|
+
if (management.http) services.push("http");
|
|
243
|
+
if (management.telnet) services.push("telnet");
|
|
244
|
+
if (management.native) services.push("fgfm");
|
|
245
|
+
|
|
246
|
+
return services.join(" ");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function render(device: Device): string {
|
|
250
|
+
const root = new Block();
|
|
251
|
+
for (const section of SECTIONS) root.block(section);
|
|
252
|
+
|
|
253
|
+
const section = (name: Section) => root.block(name);
|
|
254
|
+
|
|
255
|
+
certificates(section("vpn certificate local"), section("vpn certificate ca"), device);
|
|
256
|
+
global(section("system global"), device);
|
|
257
|
+
if (device.system?.banner) section(BANNER).set("buffer", quote(device.system.banner));
|
|
258
|
+
if (natRules(device).length) section("system settings").set("central-nat", "enable");
|
|
259
|
+
// SIP is otherwise rewritten by its proxy whatever the helpers say; this way only the `sip` helper does.
|
|
260
|
+
section("system settings").set("default-voip-alg-mode", "kernel-helper-based");
|
|
261
|
+
if (usesBfd(device)) section("system settings").set("bfd", "enable");
|
|
262
|
+
users(section("system accprofile"), section("system admin"), device);
|
|
263
|
+
interfaces(root, device);
|
|
264
|
+
dns(section("system dns"), section("system dns-server"), device);
|
|
265
|
+
ntp(section("system ntp"), device);
|
|
266
|
+
snmp(root, device);
|
|
267
|
+
logging(root, device);
|
|
268
|
+
flowExport(root, device);
|
|
269
|
+
dhcpServers(section("system dhcp server"), device);
|
|
270
|
+
staticRoutes(section("router static"), section("router static6"), device);
|
|
271
|
+
routing(root, device);
|
|
272
|
+
helpers(section("system session-helper"), device);
|
|
273
|
+
firewall(root, device);
|
|
274
|
+
|
|
275
|
+
return print(root);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A certificate with its key is the device's own; one without is only trusted. */
|
|
279
|
+
function certificates(local: Block, trusted: Block, device: Device): void {
|
|
280
|
+
for (const [name, certificate] of Object.entries(device.certificates ?? {})) {
|
|
281
|
+
const text = quote(certificate.certificate.trim());
|
|
282
|
+
|
|
283
|
+
if (certificate.private_key)
|
|
284
|
+
local
|
|
285
|
+
.entry(name)
|
|
286
|
+
.set("private-key", quote(marker(certificate.private_key)))
|
|
287
|
+
.set("certificate", text);
|
|
288
|
+
else trusted.entry(name).set("ca", text);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function global(block: Block, device: Device): void {
|
|
293
|
+
const management = device.management ?? {};
|
|
294
|
+
|
|
295
|
+
block.set("hostname", quote(device.name));
|
|
296
|
+
|
|
297
|
+
const zone = zoneOf(device.system?.timezone);
|
|
298
|
+
if (zone && zone !== "00") block.set("timezone", zone);
|
|
299
|
+
|
|
300
|
+
if (management.ssh?.port) block.set("admin-ssh-port", management.ssh.port);
|
|
301
|
+
if (management.ssh?.auth_timeout) block.set("admin-ssh-grace-time", management.ssh.auth_timeout);
|
|
302
|
+
if (management.ssh?.weak_crypto) block.set("strong-crypto", "disable");
|
|
303
|
+
if (management.https?.port) block.set("admin-sport", management.https.port);
|
|
304
|
+
if (management.https?.certificate) block.set("admin-server-cert", quote(management.https.certificate));
|
|
305
|
+
if (management.http?.port) block.set("admin-port", management.http.port);
|
|
306
|
+
if (management.telnet?.port) block.set("admin-telnet-port", management.telnet.port);
|
|
307
|
+
// The web interface over HTTP otherwise only redirects to HTTPS.
|
|
308
|
+
if (management.http) block.set("admin-https-redirect", "disable");
|
|
309
|
+
if (device.system?.banner) block.set("pre-login-banner", "enable");
|
|
310
|
+
|
|
311
|
+
if (device.lldp === true) block.set("lldp-transmission", "enable").set("lldp-reception", "enable");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Each user, with the profile its role takes. */
|
|
315
|
+
function users(profiles: Block, admins: Block, device: Device): void {
|
|
316
|
+
const roles = new Set(Object.values(device.users ?? {}).map((user) => user.role));
|
|
317
|
+
|
|
318
|
+
// An operator changes everything but the system's own settings, which hold the users; read-only changes nothing.
|
|
319
|
+
for (const role of ["operator", "read-only"] as const) {
|
|
320
|
+
if (!roles.has(role)) continue;
|
|
321
|
+
|
|
322
|
+
const profile = profiles.entry(PROFILE[role]);
|
|
323
|
+
for (const group of PERMISSION_GROUPS) profile.set(group, role === "operator" && group !== "sysgrp" ? "read-write" : "read");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const [name, user] of Object.entries(device.users ?? {})) {
|
|
327
|
+
const admin = admins.entry(name);
|
|
328
|
+
|
|
329
|
+
admin.set("accprofile", quote(PROFILE[user.role]));
|
|
330
|
+
if (user.description) admin.set("comments", quote(user.description));
|
|
331
|
+
admin.set("vdom", quote("root"));
|
|
332
|
+
(user.ssh_keys ?? []).forEach((key, index) => admin.set(`ssh-public-key${index + 1}`, quote(key)));
|
|
333
|
+
admin.set("password", quote(marker(user.password)));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Ports, then the LAGs they join, VLAN interfaces on them, loopbacks, and the tunnels with their own tables. */
|
|
338
|
+
function interfaces(root: Block, device: Device): void {
|
|
339
|
+
const block = root.block("system interface");
|
|
340
|
+
const access = allowAccess(device);
|
|
341
|
+
|
|
342
|
+
for (const port of portsOf(device.platform, device.model)) {
|
|
343
|
+
const settings = device.ports?.[port as keyof typeof device.ports];
|
|
344
|
+
const entry = block.entry(spell(device.platform, device.model, port));
|
|
345
|
+
|
|
346
|
+
entry.set("vdom", quote("root"));
|
|
347
|
+
entry.set("type", "physical");
|
|
348
|
+
|
|
349
|
+
if (!settings) {
|
|
350
|
+
redirects(entry, device);
|
|
351
|
+
entry.set("status", "down");
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (settings.description) entry.set("description", quote(settings.description));
|
|
356
|
+
|
|
357
|
+
const speed = settings.speed && SPEEDS[port.split("-")[0]!]?.[settings.speed];
|
|
358
|
+
if (speed) entry.set("speed", speed);
|
|
359
|
+
|
|
360
|
+
// A port in a LAG has no redirects of its own: `command parse error`.
|
|
361
|
+
if (settings.lag) continue;
|
|
362
|
+
|
|
363
|
+
redirects(entry, device);
|
|
364
|
+
if (settings.mtu) entry.set("mtu-override", "enable").set("mtu", settings.mtu);
|
|
365
|
+
routed(entry, device, port, settings, access);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
369
|
+
if (iface.type !== "lag") continue;
|
|
370
|
+
|
|
371
|
+
const members = Object.entries(device.ports ?? {})
|
|
372
|
+
.filter(([, port]) => port?.lag === name)
|
|
373
|
+
.map(([port]) => spell(device.platform, device.model, port));
|
|
374
|
+
|
|
375
|
+
const entry = block.entry(name);
|
|
376
|
+
entry.set("vdom", quote("root"));
|
|
377
|
+
entry.set("type", "aggregate");
|
|
378
|
+
if (members.length) entry.set("member", quoteAll(members));
|
|
379
|
+
if (iface.mode === "static") entry.set("lacp-mode", "static");
|
|
380
|
+
if (iface.description) entry.set("description", quote(iface.description));
|
|
381
|
+
if (iface.mtu) entry.set("mtu-override", "enable").set("mtu", iface.mtu);
|
|
382
|
+
redirects(entry, device);
|
|
383
|
+
routed(entry, device, name, iface, access);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
387
|
+
if (iface.type !== "vlan") continue;
|
|
388
|
+
|
|
389
|
+
const parent = vlanParent(device, iface.vlan);
|
|
390
|
+
const entry = block.entry(name);
|
|
391
|
+
entry.set("vdom", quote("root"));
|
|
392
|
+
if (parent) entry.set("interface", quote(nameOf(device, parent)));
|
|
393
|
+
entry.set("vlanid", device.vlans?.[iface.vlan]?.id ?? 0);
|
|
394
|
+
if (iface.description) entry.set("description", quote(iface.description));
|
|
395
|
+
if (iface.mtu) entry.set("mtu-override", "enable").set("mtu", iface.mtu);
|
|
396
|
+
redirects(entry, device);
|
|
397
|
+
routed(entry, device, name, iface, access);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
401
|
+
if (iface.type !== "loopback") continue;
|
|
402
|
+
|
|
403
|
+
const entry = block.entry(name);
|
|
404
|
+
entry.set("vdom", quote("root"));
|
|
405
|
+
entry.set("type", "loopback");
|
|
406
|
+
if (iface.description) entry.set("description", quote(iface.description));
|
|
407
|
+
redirects(entry, device);
|
|
408
|
+
routed(entry, device, name, iface, access);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// A tunnel's interface is created by its own table, which names the interface it leaves through.
|
|
412
|
+
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
413
|
+
if (iface.type !== "gre" && iface.type !== "vxlan") continue;
|
|
414
|
+
|
|
415
|
+
const outgoing = holderOf(device, iface.local);
|
|
416
|
+
const via = outgoing ? quote(nameOf(device, outgoing)) : '""';
|
|
417
|
+
|
|
418
|
+
// Between IPv6 endpoints the family is stated first: the addresses of the other family are not settings until it is.
|
|
419
|
+
const ipv6 = familyOf(iface.remote) === "ipv6";
|
|
420
|
+
|
|
421
|
+
if (iface.type === "gre") {
|
|
422
|
+
const tunnel = root.block("system gre-tunnel").entry(name).set("interface", via);
|
|
423
|
+
if (ipv6) tunnel.set("ip-version", 6).set("remote-gw6", iface.remote).set("local-gw6", iface.local);
|
|
424
|
+
else tunnel.set("remote-gw", iface.remote).set("local-gw", iface.local);
|
|
425
|
+
} else {
|
|
426
|
+
const tunnel = root.block("system vxlan").entry(name).set("interface", via).set("vni", iface.vni);
|
|
427
|
+
if (ipv6) tunnel.set("ip-version", "ipv6-unicast").set("remote-ip6", quote(iface.remote));
|
|
428
|
+
else tunnel.set("remote-ip", quote(iface.remote));
|
|
429
|
+
if (iface.port !== undefined && iface.port !== 4789) tunnel.set("dstport", iface.port);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const entry = block.entry(name);
|
|
433
|
+
entry.set("vdom", quote("root"));
|
|
434
|
+
entry.set("type", iface.type === "gre" ? "tunnel" : "vxlan");
|
|
435
|
+
entry.set("interface", via);
|
|
436
|
+
if (iface.type === "vxlan" && iface.mac) entry.set("macaddr", iface.mac.toLowerCase());
|
|
437
|
+
if (iface.description) entry.set("description", quote(iface.description));
|
|
438
|
+
if (iface.mtu) entry.set("mtu-override", "enable").set("mtu", iface.mtu);
|
|
439
|
+
redirects(entry, device);
|
|
440
|
+
routed(entry, device, name, iface, access);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** The platform sends ICMP redirects unless an interface says otherwise; the config sends them only when asked. */
|
|
445
|
+
function redirects(entry: Block, device: Device): void {
|
|
446
|
+
if (!device.system?.ip?.icmp_redirects) entry.set("icmp-send-redirect", "disable");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** The one port or LAG that carries a VLAN tagged, which its VLAN interface sits on. `unsupported()` refuses any other case. */
|
|
450
|
+
export function vlanParent(device: Device, vlan: string): string | undefined {
|
|
451
|
+
const carriers = [
|
|
452
|
+
...Object.entries(device.ports ?? {}).filter(([, port]) => !port?.lag && port?.trunk_vlans?.includes(vlan)),
|
|
453
|
+
...Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "lag" && iface.trunk_vlans?.includes(vlan)),
|
|
454
|
+
];
|
|
455
|
+
|
|
456
|
+
return carriers.length === 1 ? carriers[0]![0] : undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
type RoutedSettings = {
|
|
460
|
+
addresses?: readonly (string | { address: string; peer: string })[];
|
|
461
|
+
vrrp?: readonly { id: number; address: string; priority?: number; preempt?: boolean; interval?: number }[];
|
|
462
|
+
lldp?: boolean;
|
|
463
|
+
vrf?: string;
|
|
464
|
+
ipv6_ra?: boolean;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/** Addresses and their routing table, the services answered on them, VRRP, DHCP relay, router advertisements, LLDP and flow sampling of a port or an interface. */
|
|
468
|
+
function routed(entry: Block, device: Device, name: string, settings: RoutedSettings, access: string): void {
|
|
469
|
+
const addresses = addressesOf(settings);
|
|
470
|
+
const ipv4 = addresses.filter((address) => familyOf(host(address)) === "ipv4");
|
|
471
|
+
const ipv6 = addresses.filter((address) => familyOf(host(address)) === "ipv6");
|
|
472
|
+
|
|
473
|
+
const vrf = vrfNumber(device, settings.vrf);
|
|
474
|
+
if (vrf) entry.set("vrf", vrf);
|
|
475
|
+
|
|
476
|
+
// A tunnel holds an IPv4 address as a host and its peer with the link's mask.
|
|
477
|
+
const peer = settings.addresses?.find((item): item is { address: string; peer: string } => typeof item !== "string");
|
|
478
|
+
|
|
479
|
+
if (peer) {
|
|
480
|
+
entry.set("ip", `${host(peer.address)} 255.255.255.255`);
|
|
481
|
+
entry.set("remote-ip", `${peer.peer} ${netmask(split(peer.address).len)}`);
|
|
482
|
+
entry.set("allowaccess", access);
|
|
483
|
+
} else if (ipv4.length) {
|
|
484
|
+
entry.set("ip", subnet(ipv4[0]!));
|
|
485
|
+
entry.set("allowaccess", access);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!peer && ipv4.length > 1) {
|
|
489
|
+
entry.set("secondary-IP", "enable");
|
|
490
|
+
const secondaries = entry.block("secondaryip");
|
|
491
|
+
ipv4.slice(1).forEach((address, index) =>
|
|
492
|
+
secondaries
|
|
493
|
+
.entry(index + 1)
|
|
494
|
+
.set("ip", subnet(address))
|
|
495
|
+
.set("allowaccess", access)
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const relays = Object.values(device.dhcp_relay ?? {}).filter((relay) => relay.interface === name);
|
|
500
|
+
const relayed = relays.flatMap((relay) => relay.servers);
|
|
501
|
+
const relayed4 = relayed.filter((server) => familyOf(server) === "ipv4");
|
|
502
|
+
const relayed6 = relayed.filter((server) => familyOf(server) === "ipv6");
|
|
503
|
+
|
|
504
|
+
if (ipv6.length || relayed6.length) {
|
|
505
|
+
const block = entry.block("ipv6");
|
|
506
|
+
|
|
507
|
+
if (ipv6.length) {
|
|
508
|
+
block.set("ip6-address", ipv6[0]!);
|
|
509
|
+
block.set("ip6-allowaccess", access);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (settings.ipv6_ra) {
|
|
513
|
+
block.set("ip6-send-adv", "enable");
|
|
514
|
+
const prefixes = block.block("ip6-prefix-list");
|
|
515
|
+
for (const address of ipv6) prefixes.entry(network(address), true);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (relayed6.length) block.set("dhcp6-relay-service", "enable").set("dhcp6-relay-ip", relayed6.join(" "));
|
|
519
|
+
|
|
520
|
+
if (ipv6.length > 1) {
|
|
521
|
+
const extra = block.block("ip6-extra-addr");
|
|
522
|
+
for (const address of ipv6.slice(1)) extra.entry(address, true);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (settings.vrrp?.length) {
|
|
527
|
+
const groups = entry.block("vrrp");
|
|
528
|
+
|
|
529
|
+
for (const group of settings.vrrp) {
|
|
530
|
+
const vrrp = groups.entry(group.id).set("vrip", group.address);
|
|
531
|
+
if (group.priority !== undefined && group.priority !== 100) vrrp.set("priority", group.priority);
|
|
532
|
+
if (group.interval !== undefined && group.interval !== 1) vrrp.set("adv-interval", group.interval);
|
|
533
|
+
// The platform preempts unless told not to; the config preempts only when asked.
|
|
534
|
+
if (!group.preempt) vrrp.set("preempt", "disable");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (relayed4.length) {
|
|
539
|
+
entry.set("dhcp-relay-service", "enable");
|
|
540
|
+
entry.set("dhcp-relay-ip", quoteAll(relayed4));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// LLDP follows the device unless a port says otherwise, or the device lists where it runs.
|
|
544
|
+
const listed = typeof device.lldp === "object" && device.lldp.interfaces.includes(name as never);
|
|
545
|
+
if (settings.lldp === false) entry.set("lldp-transmission", "disable").set("lldp-reception", "disable");
|
|
546
|
+
else if (listed || (settings.lldp === true && device.lldp !== true))
|
|
547
|
+
entry.set("lldp-transmission", "enable").set("lldp-reception", "enable");
|
|
548
|
+
|
|
549
|
+
// A tunnel is not sampled: its traffic is, on the interface it leaves through.
|
|
550
|
+
const flow = device.flow_export;
|
|
551
|
+
const tunnel = ["gre", "vxlan"].includes(device.interfaces?.[name]?.type ?? "");
|
|
552
|
+
const sampled = flow && !tunnel && (!flow.interfaces || flow.interfaces.includes(name as never));
|
|
553
|
+
|
|
554
|
+
if (sampled && flow.protocol === "sflow") {
|
|
555
|
+
entry.set("sflow-sampler", "enable");
|
|
556
|
+
if (flow.sampling !== undefined && flow.sampling !== 2000) entry.set("sample-rate", flow.sampling);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (sampled && flow.protocol === "netflow-v9") entry.set("netflow-sampler", "both");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** The device's resolvers, and the interfaces it answers DNS queries on when it serves them. */
|
|
563
|
+
function dns(block: Block, servers: Block, device: Device): void {
|
|
564
|
+
const resolvers = device.system?.dns?.servers ?? [];
|
|
565
|
+
const ipv4 = resolvers.filter((server) => familyOf(server) === "ipv4");
|
|
566
|
+
const ipv6 = resolvers.filter((server) => familyOf(server) === "ipv6");
|
|
567
|
+
|
|
568
|
+
if (ipv4[0]) block.set("primary", ipv4[0]);
|
|
569
|
+
if (ipv4[1]) block.set("secondary", ipv4[1]);
|
|
570
|
+
if (ipv6[0]) block.set("ip6-primary", ipv6[0]);
|
|
571
|
+
if (ipv6[1]) block.set("ip6-secondary", ipv6[1]);
|
|
572
|
+
|
|
573
|
+
// Recursive, the default mode, answers from what the device has resolved itself.
|
|
574
|
+
if (device.system?.dns?.serve) for (const name of addressed(device)) servers.entry(nameOf(device, name));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** The servers the device keeps time from, and the interfaces it answers time queries on when it serves them. */
|
|
578
|
+
function ntp(block: Block, device: Device): void {
|
|
579
|
+
const ntp = device.system?.ntp;
|
|
580
|
+
const servers = ntp?.servers ?? [];
|
|
581
|
+
|
|
582
|
+
if (servers.length) {
|
|
583
|
+
block.set("ntpsync", "enable");
|
|
584
|
+
block.set("type", "custom");
|
|
585
|
+
|
|
586
|
+
const list = block.block("ntpserver");
|
|
587
|
+
servers.forEach((server, index) => list.entry(index + 1).set("server", quote(server)));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (ntp?.serve) {
|
|
591
|
+
block.set("server-mode", "enable");
|
|
592
|
+
block.set("interface", quoteAll(addressed(device).map((name) => nameOf(device, name))));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** SNMP version 2c with one community, guarded by its hosts, and version 3 users. */
|
|
597
|
+
function snmp(root: Block, device: Device): void {
|
|
598
|
+
const snmp = device.management?.snmp;
|
|
599
|
+
if (!snmp) return;
|
|
600
|
+
|
|
601
|
+
const sysinfo = root.block("system snmp sysinfo");
|
|
602
|
+
sysinfo.set("status", "enable");
|
|
603
|
+
if (snmp.contact) sysinfo.set("contact-info", quote(snmp.contact));
|
|
604
|
+
if (snmp.location) sysinfo.set("location", quote(snmp.location));
|
|
605
|
+
|
|
606
|
+
if (snmp.community) {
|
|
607
|
+
const community = root.block("system snmp community").entry(1);
|
|
608
|
+
community.set("name", quote(marker(snmp.community)));
|
|
609
|
+
|
|
610
|
+
const allow = snmp.allow ?? device.management?.allow ?? [];
|
|
611
|
+
const ipv4 = allow.filter((prefix) => familyOf(host(prefix)) === "ipv4");
|
|
612
|
+
const ipv6 = allow.filter((prefix) => familyOf(host(prefix)) === "ipv6");
|
|
613
|
+
ipv4.forEach((prefix, index) =>
|
|
614
|
+
community
|
|
615
|
+
.block("hosts")
|
|
616
|
+
.entry(index + 1)
|
|
617
|
+
.set("ip", subnet(network(prefix)))
|
|
618
|
+
);
|
|
619
|
+
ipv6.forEach((prefix, index) =>
|
|
620
|
+
community
|
|
621
|
+
.block("hosts6")
|
|
622
|
+
.entry(index + 1)
|
|
623
|
+
.set("ipv6", network(prefix))
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
// Version 2c only, and no traps: the config has nowhere to send them.
|
|
627
|
+
community.set("query-v1-status", "disable");
|
|
628
|
+
if (snmp.port) community.set("query-v2c-port", snmp.port);
|
|
629
|
+
community.set("trap-v1-status", "disable");
|
|
630
|
+
community.set("trap-v2c-status", "disable");
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
for (const [name, user] of Object.entries(snmp.users ?? {})) {
|
|
634
|
+
const entry = root.block("system snmp user").entry(name);
|
|
635
|
+
entry.set("security-level", "auth-priv");
|
|
636
|
+
if (snmp.port) entry.set("query-port", snmp.port);
|
|
637
|
+
// SHA1 and AES128 are the platform's defaults and are not printed.
|
|
638
|
+
entry.set("auth-pwd", quote(marker(user.auth_password)));
|
|
639
|
+
if (user.privacy === "des") entry.set("priv-proto", "des");
|
|
640
|
+
entry.set("priv-pwd", quote(marker(user.privacy_password)));
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Logs kept in memory from a level up, none on disk, and up to four syslog collectors, each with the least severe message it takes. */
|
|
645
|
+
function logging(root: Block, device: Device): void {
|
|
646
|
+
const logging = device.system?.logging;
|
|
647
|
+
|
|
648
|
+
// The device prints whether it keeps logs in memory either way, and keeps them unless told not to.
|
|
649
|
+
root.block("log memory setting").set("status", logging?.local ? "enable" : "disable");
|
|
650
|
+
if (logging?.local && logging.local.level !== "info") root.block("log memory filter").set("severity", LEVEL[logging.local.level]);
|
|
651
|
+
|
|
652
|
+
// The disk keeps logs unless told not to; the config keeps them only in memory.
|
|
653
|
+
root.block("log disk setting").set("status", "disable");
|
|
654
|
+
|
|
655
|
+
(logging?.remote ?? []).slice(0, COLLECTORS).forEach((collector, index) => {
|
|
656
|
+
const number = index === 0 ? "" : String(index + 1);
|
|
657
|
+
const setting = root.block(`log syslogd${number} setting`);
|
|
658
|
+
setting.set("status", "enable");
|
|
659
|
+
setting.set("server", quote(collector.address));
|
|
660
|
+
if (collector.port && collector.port !== 514) setting.set("port", collector.port);
|
|
661
|
+
|
|
662
|
+
if (collector.level !== "info") root.block(`log syslogd${number} filter`).set("severity", LEVEL[collector.level]);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/** One collector, and sampling on each interface it observes. */
|
|
667
|
+
function flowExport(root: Block, device: Device): void {
|
|
668
|
+
const flow = device.flow_export;
|
|
669
|
+
const collector = flow?.collectors[0];
|
|
670
|
+
if (!flow || !collector) return;
|
|
671
|
+
|
|
672
|
+
if (flow.protocol === "sflow") {
|
|
673
|
+
const block = root.block("system sflow").set("collector-ip", collector.address);
|
|
674
|
+
if (collector.port && collector.port !== 6343) block.set("collector-port", collector.port);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (flow.protocol === "netflow-v9") {
|
|
678
|
+
const block = root.block("system netflow").set("collector-ip", collector.address);
|
|
679
|
+
if (collector.port && collector.port !== 2055) block.set("collector-port", collector.port);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function dhcpServers(block: Block, device: Device): void {
|
|
684
|
+
Object.values(device.dhcp ?? {}).forEach((server, index) => {
|
|
685
|
+
const entry = block.entry(index + 1);
|
|
686
|
+
if (server.lease_time !== undefined && server.lease_time !== 604800) entry.set("lease-time", server.lease_time);
|
|
687
|
+
entry.set("default-gateway", server.gateway);
|
|
688
|
+
entry.set("netmask", netmask(split(server.network).len));
|
|
689
|
+
entry.set("interface", quote(nameOf(device, server.interface)));
|
|
690
|
+
entry.set("dns-service", "specify");
|
|
691
|
+
entry.block("ip-range").entry(1).set("start-ip", server.pool[0]).set("end-ip", server.pool[1]);
|
|
692
|
+
|
|
693
|
+
(server.reservations ?? []).forEach((reservation, number) => {
|
|
694
|
+
const reserved = entry.block("reserved-address").entry(number + 1);
|
|
695
|
+
reserved.set("ip", reservation.address).set("mac", reservation.mac.toLowerCase());
|
|
696
|
+
if (reservation.description) reserved.set("description", quote(reservation.description));
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
(server.dns ?? []).forEach((resolver, number) => entry.set(`dns-server${number + 1}`, resolver));
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/** Each route by its position. A route goes in the routing table of the interface it leaves through. A default route is the platform's default destination and is not printed. */
|
|
704
|
+
function staticRoutes(ipv4: Block, ipv6: Block, device: Device): void {
|
|
705
|
+
const counters = { ipv4: 0, ipv6: 0 };
|
|
706
|
+
|
|
707
|
+
for (const route of device.routing?.static ?? []) {
|
|
708
|
+
const family = familyOf(split(route.prefix).addr);
|
|
709
|
+
const entry = (family === "ipv4" ? ipv4 : ipv6).entry(++counters[family]);
|
|
710
|
+
const prefix = network(route.prefix);
|
|
711
|
+
|
|
712
|
+
if (family === "ipv6" && prefix !== "::/0") entry.set("dst", prefix);
|
|
713
|
+
if (family === "ipv4" && prefix !== "0.0.0.0/0") entry.set("dst", subnet(prefix));
|
|
714
|
+
|
|
715
|
+
if (route.via) entry.set("gateway", route.via);
|
|
716
|
+
|
|
717
|
+
const outgoing = route.interface ?? (route.via ? interfaceFor(device, route.via, route.vrf) : undefined);
|
|
718
|
+
if (outgoing && !route.blackhole) entry.set("device", quote(nameOf(device, outgoing)));
|
|
719
|
+
|
|
720
|
+
if (route.distance !== undefined && route.distance !== 10) entry.set("distance", route.distance);
|
|
721
|
+
if (route.description) entry.set("comment", quote(route.description));
|
|
722
|
+
if (route.blackhole) entry.set("blackhole", "enable");
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** The helpers the config lists, each on the protocols and ports the factory gives it. None run otherwise. */
|
|
727
|
+
function helpers(block: Block, device: Device): void {
|
|
728
|
+
let number = 0;
|
|
729
|
+
|
|
730
|
+
for (const helper of device.firewall?.helpers ?? []) {
|
|
731
|
+
for (const [name, protocol, port] of HELPERS[helper] ?? [])
|
|
732
|
+
block.entry(++number).set("name", name).set("protocol", protocol).set("port", port);
|
|
733
|
+
}
|
|
734
|
+
}
|