@takodotid/circuit 0.34.3 → 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 +11345 -9024
- 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 -26
- package/src/adapters/devices/raisecom-ros/index.ts +0 -700
- 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,615 @@
|
|
|
1
|
+
// The configuration a VRP device should run, in the form the device prints it.
|
|
2
|
+
|
|
3
|
+
import { Address } from "../../core/address";
|
|
4
|
+
import { Secrets } from "../../core/secrets";
|
|
5
|
+
import { Utils } from "../../core/utils";
|
|
6
|
+
import type { Device, Interface, Port, Vrrp } from "../../schema";
|
|
7
|
+
import { Catalog } from "../catalog";
|
|
8
|
+
import { Derive } from "../derive";
|
|
9
|
+
|
|
10
|
+
type AnyPort = Port<string, string, string, string>;
|
|
11
|
+
type AnyInterface = Interface<string, string>;
|
|
12
|
+
type LagInterface = Extract<AnyInterface, { type: "lag" }>;
|
|
13
|
+
|
|
14
|
+
/** Builds the configuration as blocks: a header line, its indented lines, then `#`. */
|
|
15
|
+
export class VrpRender {
|
|
16
|
+
/** The ACL that guards every management service. */
|
|
17
|
+
private static readonly MANAGEMENT_ACL = 2000;
|
|
18
|
+
|
|
19
|
+
/** The ACL that guards SNMP when it has sources of its own. */
|
|
20
|
+
private static readonly SNMP_ACL = 2001;
|
|
21
|
+
|
|
22
|
+
/** The channel that writes the log files on the device's own storage. */
|
|
23
|
+
private static readonly LOGFILE_CHANNEL = 9;
|
|
24
|
+
|
|
25
|
+
/** How many log files the device keeps when the config sets none. It is not printed. */
|
|
26
|
+
private static readonly LOGFILES_DEFAULT = 200;
|
|
27
|
+
|
|
28
|
+
/** The BFD multiplier the device uses when the config sets none. It is not printed. */
|
|
29
|
+
private static readonly BFD_MULTIPLIER = 3;
|
|
30
|
+
|
|
31
|
+
/** The syslog port the device uses when the config sets none. It is not printed. */
|
|
32
|
+
private static readonly SYSLOG_PORT = 514;
|
|
33
|
+
|
|
34
|
+
/** A severity as the platform spells it. `debugging` is the default for a log host and for the log files, and is not printed. */
|
|
35
|
+
private static readonly LOG_LEVEL = {
|
|
36
|
+
emergency: "emergencies",
|
|
37
|
+
alert: "alert",
|
|
38
|
+
critical: "critical",
|
|
39
|
+
error: "error",
|
|
40
|
+
warning: "warning",
|
|
41
|
+
notice: "notification",
|
|
42
|
+
info: "informational",
|
|
43
|
+
debug: "debugging",
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
/** The OSPF process number. One process per device. */
|
|
47
|
+
private static readonly OSPF_PROCESS = 1;
|
|
48
|
+
|
|
49
|
+
/** The sFlow collectors are numbered from here, in the order the config lists them. */
|
|
50
|
+
private static readonly FIRST_COLLECTOR = 1;
|
|
51
|
+
|
|
52
|
+
/** The SNMPv3 group every user joins, with read access to everything. */
|
|
53
|
+
private static readonly SNMP_GROUP = "circuit";
|
|
54
|
+
|
|
55
|
+
/** An alarm severity as the platform prints it. */
|
|
56
|
+
private static readonly ALARM_SEVERITY = { critical: "Critical", major: "Major", minor: "Minor", warning: "Warning" } as const;
|
|
57
|
+
|
|
58
|
+
private static readonly USER_LEVEL = { admin: 3, operator: 2, "read-only": 1 } as const;
|
|
59
|
+
private static readonly SNMP_AUTH = { sha1: "sha", sha256: "sha2-256" } as const;
|
|
60
|
+
private static readonly SNMP_PRIVACY = { aes128: "aes128", des: "des56" } as const;
|
|
61
|
+
|
|
62
|
+
/** Layer 2 ACLs, one for each set of ethertypes a port takes, are numbered from here. */
|
|
63
|
+
private static readonly FIRST_L2_ACL = 4000;
|
|
64
|
+
|
|
65
|
+
/** An ethertype as the platform prints it in a layer 2 ACL rule: `0x0800` prints as `type ip`. */
|
|
66
|
+
private static readonly ETHERTYPE = { ipv4: "ip", ipv6: "ipv6", arp: "arp" } as const;
|
|
67
|
+
|
|
68
|
+
private readonly lines: string[] = [];
|
|
69
|
+
private readonly vlans: NonNullable<Device["vlans"]>;
|
|
70
|
+
private readonly interfaces: NonNullable<Device["interfaces"]>;
|
|
71
|
+
private readonly lagNumbers: Map<string, number>;
|
|
72
|
+
/** The layer 2 ACL of each set of ethertypes the ports take, by the set written as `ipv4 ipv6 arp`. */
|
|
73
|
+
private readonly ethertypeAcls = new Map<string, number>();
|
|
74
|
+
|
|
75
|
+
private constructor(private readonly device: Device) {
|
|
76
|
+
this.vlans = device.vlans ?? {};
|
|
77
|
+
this.interfaces = device.interfaces ?? {};
|
|
78
|
+
this.lagNumbers = Derive.lagIds(device);
|
|
79
|
+
|
|
80
|
+
for (const port of Object.values(device.ports ?? {})) {
|
|
81
|
+
const key = port?.ethertypes?.join(" ");
|
|
82
|
+
if (key && !this.ethertypeAcls.has(key)) this.ethertypeAcls.set(key, VrpRender.FIRST_L2_ACL + this.ethertypeAcls.size);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The whole configuration the device should run. Secrets appear as markers, never values. */
|
|
87
|
+
static of(device: Device): string {
|
|
88
|
+
return new VrpRender(device).build();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `99 999 to 1000 2000`, the platform's compression of a VLAN list. */
|
|
92
|
+
static vlanBatch(ids: number[]): string {
|
|
93
|
+
const sorted = [...new Set(ids)].sort((a, b) => a - b);
|
|
94
|
+
const ranges: string[] = [];
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
97
|
+
let end = i;
|
|
98
|
+
while (sorted[end + 1] === sorted[end]! + 1) end++;
|
|
99
|
+
ranges.push(i === end ? `${sorted[i]}` : `${sorted[i]} to ${sorted[end]}`);
|
|
100
|
+
i = end;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return ranges.join(" ");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private build(): string {
|
|
107
|
+
const device = this.device;
|
|
108
|
+
|
|
109
|
+
this.system();
|
|
110
|
+
this.vlanBlocks();
|
|
111
|
+
this.managementAcl();
|
|
112
|
+
this.snmpAcl();
|
|
113
|
+
this.ethertypeAclBlocks();
|
|
114
|
+
this.users();
|
|
115
|
+
this.vrfs();
|
|
116
|
+
|
|
117
|
+
for (const [name, lag] of Object.entries(this.interfaces)) {
|
|
118
|
+
if (lag.type === "lag") this.block(`interface ${this.nameOf(name)}`, [...VrpRender.lagMode(lag), ...this.switching(lag)]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const [name, iface] of Object.entries(this.interfaces)) {
|
|
122
|
+
if (iface.type === "vlan") this.block(`interface ${this.nameOf(name)}`, this.vlanInterface(name, iface));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const port of Catalog.ports(device.platform, device.model)) {
|
|
126
|
+
const settings = device.ports?.[port as keyof typeof device.ports];
|
|
127
|
+
this.block(`interface ${this.nameOf(port)}`, this.physicalPort(port, settings));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.staticRoutes();
|
|
131
|
+
this.ospf();
|
|
132
|
+
if (this.wantsBfd()) this.block("bfd", []);
|
|
133
|
+
if (device.dhcp_relay && Object.keys(device.dhcp_relay).length) {
|
|
134
|
+
this.line("dhcp enable");
|
|
135
|
+
this.separator();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.flowExport();
|
|
139
|
+
this.logging();
|
|
140
|
+
this.alarms();
|
|
141
|
+
this.snmp();
|
|
142
|
+
this.ssh();
|
|
143
|
+
this.userInterfaces();
|
|
144
|
+
return this.text();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private line(text: string): void {
|
|
148
|
+
this.lines.push(text);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A header and its lines. A block with nothing in it is still written, so that its header exists. */
|
|
152
|
+
private block(header: string, body: string[]): void {
|
|
153
|
+
this.lines.push(header, ...body.map((text) => ` ${text}`), "#");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private separator(): void {
|
|
157
|
+
this.lines.push("#");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private text(): string {
|
|
161
|
+
return this.lines.join("\n") + "\n";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private vlanId(name: string): number {
|
|
165
|
+
return this.vlans[name]!.id;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** What the platform calls a port, a LAG or a VLAN interface. */
|
|
169
|
+
private nameOf(portOrInterface: string): string {
|
|
170
|
+
const iface = this.interfaces[portOrInterface];
|
|
171
|
+
if (iface?.type === "lag") return `Eth-Trunk${this.lagNumbers.get(portOrInterface)}`;
|
|
172
|
+
|
|
173
|
+
if (iface?.type === "vlan") return `Vlanif${this.vlanId(iface.vlan)}`;
|
|
174
|
+
|
|
175
|
+
return Catalog.spell(this.device.platform, this.device.model, portOrInterface);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private system(): void {
|
|
179
|
+
const device = this.device;
|
|
180
|
+
|
|
181
|
+
this.line(`sysname ${device.name}`);
|
|
182
|
+
this.separator();
|
|
183
|
+
|
|
184
|
+
const timezone = device.system?.timezone;
|
|
185
|
+
if (timezone) {
|
|
186
|
+
const minutes = Utils.timeZoneOffset(timezone);
|
|
187
|
+
const absolute = Math.abs(minutes);
|
|
188
|
+
const hours = String(Math.floor(absolute / 60)).padStart(2, "0");
|
|
189
|
+
const remainder = String(absolute % 60).padStart(2, "0");
|
|
190
|
+
// The platform names the zone with one word; the last part of the IANA name serves.
|
|
191
|
+
this.line(`clock timezone ${timezone.split("/").at(-1)} ${minutes < 0 ? "minus" : "add"} ${hours}:${remainder}:00`);
|
|
192
|
+
this.separator();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const stp = device.stp;
|
|
196
|
+
if (!stp) this.line("stp disable");
|
|
197
|
+
else {
|
|
198
|
+
// MSTP is the platform's default and is not written.
|
|
199
|
+
if (stp.mode && stp.mode !== "mstp") this.line(`stp mode ${stp.mode}`);
|
|
200
|
+
if (stp.priority !== undefined) this.line(`stp priority ${stp.priority}`);
|
|
201
|
+
}
|
|
202
|
+
// LLDP is off by default and turned on by the config.
|
|
203
|
+
if (device.lldp) this.line("lldp enable");
|
|
204
|
+
this.separator();
|
|
205
|
+
|
|
206
|
+
const ntp = device.system?.ntp;
|
|
207
|
+
if (!ntp?.serve) {
|
|
208
|
+
this.line("ntp server disable");
|
|
209
|
+
this.line("ntp ipv6 server disable");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const server of ntp?.servers ?? []) this.line(`ntp unicast-server ${server}`);
|
|
213
|
+
this.separator();
|
|
214
|
+
|
|
215
|
+
const ids = Object.values(device.vlans ?? {}).map((vlan) => vlan.id);
|
|
216
|
+
if (ids.length) {
|
|
217
|
+
this.line(`vlan batch ${VrpRender.vlanBatch(ids)}`);
|
|
218
|
+
this.separator();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!device.management?.telnet) {
|
|
222
|
+
this.line("telnet server disable");
|
|
223
|
+
this.line("telnet ipv6 server disable");
|
|
224
|
+
this.separator();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private vlanBlocks(): void {
|
|
229
|
+
for (const [name, vlan] of Object.entries(this.device.vlans ?? {})) {
|
|
230
|
+
this.block(`vlan ${vlan.id}`, [`name ${name}`, ...(vlan.description ? [`description ${vlan.description}`] : [])]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** One ACL guards every management service: a permit per source, then an explicit deny. */
|
|
235
|
+
private managementAcl(): void {
|
|
236
|
+
const allow = this.device.management?.allow ?? [];
|
|
237
|
+
if (!allow.length) return;
|
|
238
|
+
|
|
239
|
+
this.block(`acl number ${VrpRender.MANAGEMENT_ACL}`, ["description Management access", ...VrpRender.aclRules(allow)]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** SNMP with sources of its own gets an ACL of its own, built the same way. */
|
|
243
|
+
private ethertypeAclBlocks(): void {
|
|
244
|
+
for (const [key, number] of this.ethertypeAcls) {
|
|
245
|
+
const kinds = key.split(" ") as (keyof typeof VrpRender.ETHERTYPE)[];
|
|
246
|
+
const rules = kinds.map((kind, index) => `rule ${(index + 1) * 5} permit type ${VrpRender.ETHERTYPE[kind]}`);
|
|
247
|
+
this.block(`acl number ${number}`, [...rules, `rule ${(kinds.length + 1) * 5} deny`]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private snmpAcl(): void {
|
|
252
|
+
const allow = this.device.management?.snmp?.allow ?? [];
|
|
253
|
+
if (!allow.length) return;
|
|
254
|
+
|
|
255
|
+
this.block(`acl number ${VrpRender.SNMP_ACL}`, ["description SNMP access", ...VrpRender.aclRules(allow)]);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** A permit per source, then an explicit deny. The platform prints the wildcard of a single address as `0`. */
|
|
259
|
+
private static aclRules(allow: readonly string[]): string[] {
|
|
260
|
+
const rules = allow.map((cidr, index) => {
|
|
261
|
+
const length = Address.split(cidr).len;
|
|
262
|
+
const wildcard = length === 32 ? "0" : Address.wildcard(length);
|
|
263
|
+
return `rule ${(index + 1) * 5} permit source ${Address.host(cidr)} ${wildcard}`;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return [...rules, `rule ${(allow.length + 1) * 5} deny`];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private users(): void {
|
|
270
|
+
const lines: string[] = [];
|
|
271
|
+
|
|
272
|
+
for (const [name, user] of Object.entries(this.device.users ?? {})) {
|
|
273
|
+
lines.push(`local-user ${name} password irreversible-cipher ${Secrets.marker(user.password)}`);
|
|
274
|
+
lines.push(`local-user ${name} service-type ssh`);
|
|
275
|
+
lines.push(`local-user ${name} level ${VrpRender.USER_LEVEL[user.role]}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.block("aaa", lines);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private vrfs(): void {
|
|
282
|
+
for (const name of Object.keys(this.device.vrfs ?? {})) this.block(`ip vpn-instance ${name}`, ["ipv4-family"]);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private static lagMode(lag: LagInterface): string[] {
|
|
286
|
+
const lines = [lag.mode === "static" ? "mode manual load-balance" : "mode lacp-static"];
|
|
287
|
+
if (lag.description) lines.unshift(`description ${lag.description}`);
|
|
288
|
+
return lines;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Layer 2 membership of a port or LAG. */
|
|
292
|
+
private switching(settings: AnyPort | LagInterface): string[] {
|
|
293
|
+
const lines: string[] = [];
|
|
294
|
+
if (settings.access_vlan) lines.push(`port default vlan ${this.vlanId(settings.access_vlan)}`);
|
|
295
|
+
if (settings.trunk_vlans) {
|
|
296
|
+
lines.push("port link-type trunk");
|
|
297
|
+
if (settings.native_vlan) lines.push(`port trunk pvid vlan ${this.vlanId(settings.native_vlan)}`);
|
|
298
|
+
// VLAN 1 is allowed by default and never wanted. The native VLAN has to be allowed too, or its untagged frames are dropped.
|
|
299
|
+
const allowed = [...settings.trunk_vlans, ...(settings.native_vlan ? [settings.native_vlan] : [])].map((name) =>
|
|
300
|
+
this.vlanId(name)
|
|
301
|
+
);
|
|
302
|
+
lines.push("undo port trunk allow-pass vlan 1");
|
|
303
|
+
lines.push(`port trunk allow-pass vlan ${VrpRender.vlanBatch(allowed)}`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (settings.stp === false) lines.push("stp disable");
|
|
307
|
+
else if (typeof settings.stp === "object") {
|
|
308
|
+
if (settings.stp.edge) lines.push("stp edged-port enable");
|
|
309
|
+
if (settings.stp.cost !== undefined) lines.push(`stp cost ${settings.stp.cost}`);
|
|
310
|
+
if (settings.stp.priority !== undefined) lines.push(`stp port priority ${settings.stp.priority}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const storm = settings.storm_control;
|
|
314
|
+
if (storm) {
|
|
315
|
+
const kinds = [
|
|
316
|
+
["broadcast", storm.broadcast],
|
|
317
|
+
["multicast", storm.multicast],
|
|
318
|
+
["unknown-unicast", storm.unknown_unicast],
|
|
319
|
+
] as const;
|
|
320
|
+
|
|
321
|
+
for (const [kind, threshold] of kinds) {
|
|
322
|
+
if (!threshold || !("percent" in threshold)) continue;
|
|
323
|
+
|
|
324
|
+
const percent = threshold.percent;
|
|
325
|
+
lines.push(`storm control ${kind} min-rate percent ${percent} max-rate percent ${percent}`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
lines.push("storm control action suppress", "storm control enable trap", "storm control enable log");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return lines;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private static vrrpLines(group: Vrrp): string[] {
|
|
335
|
+
const lines = [`vrrp vrid ${group.id} virtual-ip ${group.address}`];
|
|
336
|
+
if (group.priority !== undefined) lines.push(`vrrp vrid ${group.id} priority ${group.priority}`);
|
|
337
|
+
// The platform preempts by default; the config only does when asked.
|
|
338
|
+
if (!group.preempt) lines.push(`vrrp vrid ${group.id} preempt disable`);
|
|
339
|
+
if (group.interval !== undefined && group.interval !== 1) lines.push(`vrrp vrid ${group.id} timer advertise ${group.interval}`);
|
|
340
|
+
return lines;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private vlanInterface(name: string, iface: Extract<AnyInterface, { type: "vlan" }>): string[] {
|
|
344
|
+
const device = this.device;
|
|
345
|
+
const lines: string[] = [];
|
|
346
|
+
if (iface.description) lines.push(`description ${iface.description}`);
|
|
347
|
+
// Binding to a VRF clears the addresses, so it comes first.
|
|
348
|
+
if (iface.vrf) lines.push(`ip binding vpn-instance ${iface.vrf}`);
|
|
349
|
+
|
|
350
|
+
for (const entry of iface.addresses ?? []) {
|
|
351
|
+
const address = typeof entry === "string" ? entry : entry.address;
|
|
352
|
+
if (Address.family(address) === "ipv4")
|
|
353
|
+
lines.push(`ip address ${Address.host(address)} ${Address.netmask(Address.split(address).len)}`);
|
|
354
|
+
else lines.push(`ipv6 address ${address}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const group of iface.vrrp ?? []) lines.push(...VrpRender.vrrpLines(group));
|
|
358
|
+
|
|
359
|
+
const relays = Object.values(device.dhcp_relay ?? {}).filter((relay) => relay.interface === name);
|
|
360
|
+
if (relays.length) {
|
|
361
|
+
lines.push("dhcp select relay");
|
|
362
|
+
for (const server of relays.flatMap((relay) => relay.servers)) lines.push(`dhcp relay binding server ip ${server}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
for (const area of Object.values(device.routing?.ospf?.areas ?? {})) {
|
|
366
|
+
const settings = area.interfaces[name as keyof typeof area.interfaces];
|
|
367
|
+
if (!settings) continue;
|
|
368
|
+
|
|
369
|
+
if (settings.cost !== undefined) lines.push(`ospf cost ${settings.cost}`);
|
|
370
|
+
if (settings.network === "point-to-point") lines.push("ospf network-type p2p");
|
|
371
|
+
if (settings.bfd) lines.push("ospf bfd enable", ...this.bfdTimers("ospf bfd"));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return lines;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** A port the config does not declare is shut down. A LAG member holds only its membership. */
|
|
378
|
+
private physicalPort(port: string, settings: AnyPort | undefined): string[] {
|
|
379
|
+
if (!settings) return ["shutdown"];
|
|
380
|
+
|
|
381
|
+
// A port joins a LAG only while it holds no other setting, so membership comes before its description.
|
|
382
|
+
if (settings.lag) {
|
|
383
|
+
const membership = `eth-trunk ${this.lagNumbers.get(settings.lag)}`;
|
|
384
|
+
return settings.description ? [membership, `description ${settings.description}`] : [membership];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const lines: string[] = [];
|
|
388
|
+
if (settings.description) lines.push(`description ${settings.description}`);
|
|
389
|
+
lines.push(...this.switching(settings));
|
|
390
|
+
if (settings.lldp === false) lines.push("lldp disable");
|
|
391
|
+
if (settings.mac_limit !== undefined) lines.push(`mac-address limit maximum ${settings.mac_limit}`);
|
|
392
|
+
if (settings.ethertypes) lines.push(`traffic-filter acl ${this.ethertypeAcls.get(settings.ethertypes.join(" "))} inbound`);
|
|
393
|
+
|
|
394
|
+
const flow = this.device.flow_export;
|
|
395
|
+
const observed = flow?.protocol === "sflow" && (!flow.interfaces || flow.interfaces.includes(port));
|
|
396
|
+
if (observed) {
|
|
397
|
+
for (let index = 0; index < flow!.collectors.length; index++)
|
|
398
|
+
lines.push(`sflow sampling collector ${VrpRender.FIRST_COLLECTOR + index}`);
|
|
399
|
+
lines.push(`sflow sampling rate ${flow!.sampling}`);
|
|
400
|
+
lines.push("sflow sampling inbound");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return lines;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Static routes. A route watched by BFD needs a BFD template for its next hop first, from the device's own address on that network, with the device's timers.
|
|
408
|
+
*/
|
|
409
|
+
private staticRoutes(): void {
|
|
410
|
+
const routes = this.device.routing?.static ?? [];
|
|
411
|
+
const watched = [...new Set(routes.filter((route) => route.bfd && route.via).map((route) => route.via!))];
|
|
412
|
+
|
|
413
|
+
for (const nextHop of watched) {
|
|
414
|
+
const local = VrpRender.localAddressTo(this.device, nextHop);
|
|
415
|
+
const timers = this.bfdTimers("").map((line) => line.trim());
|
|
416
|
+
this.line([`ip route-static bfd ${nextHop} local-address ${local}`, ...timers].join(" "));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const route of routes) {
|
|
420
|
+
const { addr, len } = Address.split(route.prefix);
|
|
421
|
+
const nextHop = route.blackhole ? "NULL0" : (route.via ?? this.nameOf(route.interface!));
|
|
422
|
+
const vrf = route.vrf ? ` vpn-instance ${route.vrf}` : "";
|
|
423
|
+
const preference = route.distance ? ` preference ${route.distance}` : "";
|
|
424
|
+
const description = route.description ? ` description ${route.description}` : "";
|
|
425
|
+
const bfd = route.bfd ? " bfd enable" : "";
|
|
426
|
+
if (Address.family(addr) === "ipv4")
|
|
427
|
+
this.line(`ip route-static${vrf} ${addr} ${Address.netmask(len)} ${nextHop}${preference}${description}${bfd}`);
|
|
428
|
+
else this.line(`ipv6 route-static${vrf} ${addr} ${len} ${nextHop}${preference}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
this.separator();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** The device's own IPv4 address on the network that holds `nextHop`. `unsupported()` refuses a watched route without one. */
|
|
435
|
+
static localAddressTo(device: Device, nextHop: string): string | undefined {
|
|
436
|
+
for (const iface of Object.values(device.interfaces ?? {})) {
|
|
437
|
+
for (const entry of iface.addresses ?? []) {
|
|
438
|
+
const address = typeof entry === "string" ? entry : entry.address;
|
|
439
|
+
if (Address.contains(Address.prefixOf(address), nextHop)) return Address.host(address);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** The BFD timers of `routing.bfd`, after `prefix`, or nothing for the platform's own. The platform does not print its default multiplier, 3. */
|
|
447
|
+
private bfdTimers(prefix: string): string[] {
|
|
448
|
+
const bfd = this.device.routing?.bfd;
|
|
449
|
+
if (!bfd) return [];
|
|
450
|
+
|
|
451
|
+
const multiplier = bfd.multiplier === VrpRender.BFD_MULTIPLIER ? "" : ` detect-multiplier ${bfd.multiplier}`;
|
|
452
|
+
return [`${prefix} min-tx-interval ${bfd.interval} min-rx-interval ${bfd.interval}${multiplier}`.trim()];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* OSPF for IPv4. An area announces the networks of its interfaces; a passive interface is silent. `unsupported()` refuses IPv6.
|
|
457
|
+
*/
|
|
458
|
+
private ospf(): void {
|
|
459
|
+
const device = this.device;
|
|
460
|
+
const ospf = device.routing?.ospf;
|
|
461
|
+
if (!ospf) return;
|
|
462
|
+
|
|
463
|
+
const routerId = device.routing?.router_id;
|
|
464
|
+
const lines: string[] = [];
|
|
465
|
+
for (const source of ospf.redistribute ?? []) lines.push(`import-route ${source === "connected" ? "direct" : "static"}`);
|
|
466
|
+
|
|
467
|
+
const areaLines: string[] = [];
|
|
468
|
+
|
|
469
|
+
for (const [areaId, area] of Object.entries(ospf.areas)) {
|
|
470
|
+
areaLines.push(`area ${areaId}`);
|
|
471
|
+
|
|
472
|
+
for (const [iface, settings] of Object.entries(area.interfaces)) {
|
|
473
|
+
if (settings?.passive) lines.push(`silent-interface ${this.nameOf(iface)}`);
|
|
474
|
+
for (const prefix of Derive.ipv4Networks(device, iface))
|
|
475
|
+
areaLines.push(` network ${Address.host(prefix)} ${Address.wildcard(Address.split(prefix).len)}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
this.block(`ospf ${VrpRender.OSPF_PROCESS}${routerId ? ` router-id ${routerId}` : ""}`, [...lines, ...areaLines]);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private wantsBfd(): boolean {
|
|
483
|
+
const areas = Object.values(this.device.routing?.ospf?.areas ?? {});
|
|
484
|
+
const onOspf = areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
|
|
485
|
+
const onStatic = (this.device.routing?.static ?? []).some((route) => route.bfd);
|
|
486
|
+
|
|
487
|
+
return onOspf || onStatic;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** sFlow from the management address, to each collector in order. */
|
|
491
|
+
private flowExport(): void {
|
|
492
|
+
const device = this.device;
|
|
493
|
+
const flow = device.flow_export;
|
|
494
|
+
if (flow?.protocol !== "sflow") return;
|
|
495
|
+
|
|
496
|
+
if (device.connection.host) this.line(`sflow agent ip ${device.connection.host}`);
|
|
497
|
+
|
|
498
|
+
flow.collectors.forEach((collector, index) => {
|
|
499
|
+
const number = VrpRender.FIRST_COLLECTOR + index;
|
|
500
|
+
this.line(`sflow collector ${number} ip ${collector.address} udp-port ${collector.port ?? 6343}`);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
this.separator();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** The log files' level and count, then each syslog collector. A default the device does not print is not written: level `debugging`, 200 files, port 514. */
|
|
507
|
+
private logging(): void {
|
|
508
|
+
const logging = this.device.system?.logging;
|
|
509
|
+
if (!logging) return;
|
|
510
|
+
|
|
511
|
+
const local = logging.local;
|
|
512
|
+
if (local && local.level !== "debug") {
|
|
513
|
+
this.line(`info-center source default channel ${VrpRender.LOGFILE_CHANNEL} log level ${VrpRender.LOG_LEVEL[local.level]}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (local?.files !== undefined && local.files !== VrpRender.LOGFILES_DEFAULT)
|
|
517
|
+
this.line(`info-center max-logfile-number ${local.files}`);
|
|
518
|
+
|
|
519
|
+
for (const collector of logging.remote ?? []) {
|
|
520
|
+
const level = collector.level === "debug" ? "" : ` level ${VrpRender.LOG_LEVEL[collector.level]}`;
|
|
521
|
+
const port = collector.port !== undefined && collector.port !== VrpRender.SYSLOG_PORT ? ` port ${collector.port}` : "";
|
|
522
|
+
this.line(`info-center loghost ${collector.address}${level}${port}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
this.separator();
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Alarms raised at another severity than the platform's own, in the `alarm` view. The platform prints a severity with a capital, `Warning`. */
|
|
529
|
+
private alarms(): void {
|
|
530
|
+
const alarms = Object.entries(this.device.system?.alarms ?? {});
|
|
531
|
+
if (!alarms.length) return;
|
|
532
|
+
|
|
533
|
+
this.block(
|
|
534
|
+
"alarm",
|
|
535
|
+
alarms.map(([name, alarm]) => `alarm ${name} severity ${VrpRender.ALARM_SEVERITY[alarm.severity]}`)
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** SNMP version 2c with one read community, version 3 with users in one read-only group, or both. */
|
|
540
|
+
private snmp(): void {
|
|
541
|
+
const device = this.device;
|
|
542
|
+
const snmp = device.management?.snmp;
|
|
543
|
+
if (!snmp) return;
|
|
544
|
+
|
|
545
|
+
const versions = [snmp.community ? "v2c" : "", snmp.users ? "v3" : ""].filter(Boolean).join(" ");
|
|
546
|
+
const own = (snmp.allow?.length ?? 0) > 0;
|
|
547
|
+
const guarded = (device.management?.allow?.length ?? 0) > 0;
|
|
548
|
+
const acl = own ? ` acl ${VrpRender.SNMP_ACL}` : guarded ? ` acl ${VrpRender.MANAGEMENT_ACL}` : "";
|
|
549
|
+
|
|
550
|
+
this.line("snmp-agent");
|
|
551
|
+
this.line(`snmp-agent sys-info version ${versions}`);
|
|
552
|
+
if (snmp.contact) this.line(`snmp-agent sys-info contact ${snmp.contact}`);
|
|
553
|
+
if (snmp.location) this.line(`snmp-agent sys-info location ${snmp.location}`);
|
|
554
|
+
if (snmp.community) this.line(`snmp-agent community read cipher ${Secrets.marker(snmp.community)}${acl}`);
|
|
555
|
+
if (snmp.users) {
|
|
556
|
+
this.line("snmp-agent mib-view included all iso");
|
|
557
|
+
this.line(`snmp-agent group v3 ${VrpRender.SNMP_GROUP} privacy read-view all${acl}`);
|
|
558
|
+
|
|
559
|
+
for (const [name, user] of Object.entries(snmp.users)) {
|
|
560
|
+
// The switch prints a user's own line before its settings.
|
|
561
|
+
this.line(`snmp-agent usm-user v3 ${name}`);
|
|
562
|
+
this.line(`snmp-agent usm-user v3 ${name} group ${VrpRender.SNMP_GROUP}`);
|
|
563
|
+
this.line(
|
|
564
|
+
`snmp-agent usm-user v3 ${name} authentication-mode ${VrpRender.SNMP_AUTH[user.auth]} cipher ${Secrets.marker(user.auth_password)}`
|
|
565
|
+
);
|
|
566
|
+
this.line(
|
|
567
|
+
`snmp-agent usm-user v3 ${name} privacy-mode ${VrpRender.SNMP_PRIVACY[user.privacy]} cipher ${Secrets.marker(user.privacy_password)}`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
this.separator();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private ssh(): void {
|
|
576
|
+
const device = this.device;
|
|
577
|
+
const ssh = device.management?.ssh;
|
|
578
|
+
if (!ssh) return;
|
|
579
|
+
|
|
580
|
+
const guarded = (device.management?.allow?.length ?? 0) > 0;
|
|
581
|
+
|
|
582
|
+
this.line("stelnet server enable");
|
|
583
|
+
|
|
584
|
+
for (const user of Object.keys(device.users ?? {})) {
|
|
585
|
+
this.line(`ssh user ${user}`);
|
|
586
|
+
this.line(`ssh user ${user} authentication-type password`);
|
|
587
|
+
this.line(`ssh user ${user} service-type stelnet`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (guarded) this.line(`ssh server acl ${VrpRender.MANAGEMENT_ACL}`);
|
|
591
|
+
// Who decides whether a login is allowed: AAA, the same place for every service.
|
|
592
|
+
this.line("ssh authorization-type default aaa");
|
|
593
|
+
this.separator();
|
|
594
|
+
|
|
595
|
+
if (!ssh.weak_crypto) {
|
|
596
|
+
this.line("ssh server cipher aes256_ctr aes128_ctr");
|
|
597
|
+
this.line("ssh server hmac sha2_256_96 sha2_256");
|
|
598
|
+
this.line("ssh server key-exchange dh_group_exchange_sha256 ecdh_sha2_nistp256 ecdh_sha2_nistp384 ecdh_sha2_nistp521");
|
|
599
|
+
this.separator();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** The console logs in with the device's users unless the config gives it a password of its own. Remote logins always use the users. */
|
|
604
|
+
private userInterfaces(): void {
|
|
605
|
+
const device = this.device;
|
|
606
|
+
const console = device.management?.console;
|
|
607
|
+
|
|
608
|
+
const consoleLogin = console
|
|
609
|
+
? ["authentication-mode password", `set authentication password cipher ${Secrets.marker(console.password)}`]
|
|
610
|
+
: ["authentication-mode aaa"];
|
|
611
|
+
|
|
612
|
+
this.block("user-interface con 0", consoleLogin);
|
|
613
|
+
this.block("user-interface vty 0 4", ["authentication-mode aaa", `protocol inbound ${device.management?.telnet ? "all" : "ssh"}`]);
|
|
614
|
+
}
|
|
615
|
+
}
|
package/src/schema/common.ts
CHANGED
|
@@ -13,14 +13,23 @@ export type Secret =
|
|
|
13
13
|
readonly secret_file: string;
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
/** An address
|
|
17
|
-
export type
|
|
16
|
+
/** An IPv4 address, four numbers with dots between them, for example `10.0.0.1`. The type checks the shape; `circuit validate` checks each number is 0 to 255. */
|
|
17
|
+
export type IPv4 = `${number}.${number}.${number}.${number}`;
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
export type
|
|
19
|
+
/** An IPv6 address, groups with colons between them, for example `2001:db8::1`. The type only checks there is a colon; `circuit validate` checks the rest. */
|
|
20
|
+
export type IPv6 = `${string}:${string}`;
|
|
21
21
|
|
|
22
|
-
/** An
|
|
23
|
-
export type
|
|
22
|
+
/** An address without a length, IPv4 or IPv6, for example `10.0.0.1` or `2001:db8::1`. */
|
|
23
|
+
export type IP = IPv4 | IPv6;
|
|
24
|
+
|
|
25
|
+
/** A network with its length, for example `10.0.0.0/24` or `2001:db8::/32`. */
|
|
26
|
+
export type Prefix = `${IP}/${number}`;
|
|
27
|
+
|
|
28
|
+
/** An interface address with its length, for example `10.0.0.1/24` or `2001:db8::1/64`. */
|
|
29
|
+
export type InterfaceAddress = `${IP}/${number}`;
|
|
30
|
+
|
|
31
|
+
/** A MAC address, six pairs of hex digits with colons between them, for example `02:00:00:00:00:01`. */
|
|
32
|
+
export type MacAddress = `${string}:${string}:${string}:${string}:${string}:${string}`;
|
|
24
33
|
|
|
25
34
|
/** An address family. */
|
|
26
35
|
export type Family = "ipv4" | "ipv6";
|
package/src/schema/index.ts
CHANGED