@takodotid/circuit 0.32.0 → 0.33.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/dist/cli/index.js
CHANGED
|
@@ -545,6 +545,20 @@ function bridgesOf(device) {
|
|
|
545
545
|
return Object.entries(device.interfaces ?? {}).flatMap(([name, iface]) => iface.type === "bridge" ? [{ name, members: iface.members }] : []);
|
|
546
546
|
}
|
|
547
547
|
var isTransparent = (device) => bridgesOf(device).length > 0;
|
|
548
|
+
function transparentManagement(device) {
|
|
549
|
+
if (!isTransparent(device))
|
|
550
|
+
return;
|
|
551
|
+
const members = new Set(bridgesOf(device).flatMap((bridge) => bridge.members));
|
|
552
|
+
const holders = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})];
|
|
553
|
+
for (const [name, settings] of holders) {
|
|
554
|
+
if (members.has(name))
|
|
555
|
+
continue;
|
|
556
|
+
const address = addressesOf2(settings).find((each) => familyOf2(host2(each)) === "ipv4");
|
|
557
|
+
if (address)
|
|
558
|
+
return { name, address };
|
|
559
|
+
}
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
548
562
|
function forwardDomainOf(device, name) {
|
|
549
563
|
const index = bridgesOf(device).findIndex((bridge) => bridge.members.includes(name));
|
|
550
564
|
return index < 0 ? undefined : index + 1;
|
|
@@ -2232,8 +2246,11 @@ function render(device) {
|
|
|
2232
2246
|
global(section("system global"), device);
|
|
2233
2247
|
if (device.system?.banner)
|
|
2234
2248
|
section(BANNER).set("buffer", quote(device.system.banner));
|
|
2235
|
-
if (isTransparent(device))
|
|
2236
|
-
|
|
2249
|
+
if (isTransparent(device)) {
|
|
2250
|
+
const management = transparentManagement(device);
|
|
2251
|
+
const manageip = management ? `${host2(management.address)}/${netmask(split(management.address).len)}` : TRANSPARENT_MANAGEIP;
|
|
2252
|
+
section("system settings").set("opmode", "transparent").set("manageip", manageip);
|
|
2253
|
+
}
|
|
2237
2254
|
if (natRules(device).length)
|
|
2238
2255
|
section("system settings").set("central-nat", "enable");
|
|
2239
2256
|
section("system settings").set("default-voip-alg-mode", "kernel-helper-based");
|
|
@@ -2428,6 +2445,11 @@ function vlanParent(device, vlan) {
|
|
|
2428
2445
|
return carriers.length === 1 ? carriers[0][0] : undefined;
|
|
2429
2446
|
}
|
|
2430
2447
|
function routed(entry, device, name, settings, access) {
|
|
2448
|
+
if (isTransparent(device)) {
|
|
2449
|
+
if (transparentManagement(device)?.name === name)
|
|
2450
|
+
entry.set("allowaccess", access);
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2431
2453
|
const addresses = addressesOf2(settings);
|
|
2432
2454
|
const ipv4 = addresses.filter((address) => familyOf2(host2(address)) === "ipv4");
|
|
2433
2455
|
const ipv6 = addresses.filter((address) => familyOf2(host2(address)) === "ipv6");
|
|
@@ -2634,7 +2656,7 @@ function staticRoutes(ipv4, ipv6, device) {
|
|
|
2634
2656
|
if (route.via)
|
|
2635
2657
|
entry.set("gateway", route.via);
|
|
2636
2658
|
const outgoing = route.interface ?? (route.via ? interfaceFor(device, route.via, route.vrf) : undefined);
|
|
2637
|
-
if (outgoing && !route.blackhole)
|
|
2659
|
+
if (outgoing && !route.blackhole && !isTransparent(device))
|
|
2638
2660
|
entry.set("device", quote(nameOf(device, outgoing)));
|
|
2639
2661
|
if (route.distance !== undefined && route.distance !== 10)
|
|
2640
2662
|
entry.set("distance", route.distance);
|
|
@@ -3027,27 +3049,38 @@ function transparentGaps(device) {
|
|
|
3027
3049
|
return [];
|
|
3028
3050
|
const found = [];
|
|
3029
3051
|
const where = "in transparent mode, which a bridge needs";
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3052
|
+
const management = transparentManagement(device);
|
|
3053
|
+
if (device.connection.host && device.connection.host !== (management && host2(management.address)))
|
|
3054
|
+
found.push(`connection.host, ${where}: only the management address, one IPv4 address outside the bridges`);
|
|
3055
|
+
const holders = [
|
|
3056
|
+
...Object.entries(device.ports ?? {}).map(([name, port]) => [`ports.${name}`, name, port?.addresses]),
|
|
3057
|
+
...Object.entries(device.interfaces ?? {}).map(([name, iface]) => [`interfaces.${name}`, name, iface.addresses])
|
|
3058
|
+
];
|
|
3059
|
+
for (const [path, name, addresses] of holders) {
|
|
3060
|
+
const extra = (addresses ?? []).filter((entry) => !(typeof entry === "string" && management && name === management.name && entry === management.address));
|
|
3061
|
+
if (extra.length)
|
|
3062
|
+
found.push(`${path}.addresses, ${where}: one IPv4 address in all, outside the bridges, for management`);
|
|
3039
3063
|
}
|
|
3040
3064
|
const routing = device.routing;
|
|
3041
|
-
if (routing?.
|
|
3042
|
-
found.push(`routing, ${where}`);
|
|
3065
|
+
if (routing?.bgp || routing?.ospf)
|
|
3066
|
+
found.push(`routing, ${where}: static routes only`);
|
|
3067
|
+
for (const [index, route] of (routing?.static ?? []).entries()) {
|
|
3068
|
+
const at = `routing.static[${index}]`;
|
|
3069
|
+
if (!management)
|
|
3070
|
+
found.push(`${at}, ${where}: a route needs the management address`);
|
|
3071
|
+
if (!route.via || familyOf2(route.via) !== "ipv4" || route.interface || route.blackhole || route.vrf)
|
|
3072
|
+
found.push(`${at}, ${where}: only an IPv4 next hop, for the management address`);
|
|
3073
|
+
}
|
|
3043
3074
|
if (device.firewall?.nat)
|
|
3044
3075
|
found.push(`firewall.nat, ${where}`);
|
|
3045
3076
|
if (device.dhcp && Object.keys(device.dhcp).length)
|
|
3046
3077
|
found.push(`dhcp, ${where}`);
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3078
|
+
if (!management) {
|
|
3079
|
+
const services = device.management ?? {};
|
|
3080
|
+
for (const service of ["ssh", "telnet", "http", "https", "api", "api_tls", "snmp", "native"]) {
|
|
3081
|
+
if (services[service])
|
|
3082
|
+
found.push(`management.${service}, ${where}: the device has no management address to answer on`);
|
|
3083
|
+
}
|
|
3051
3084
|
}
|
|
3052
3085
|
return found;
|
|
3053
3086
|
}
|
|
@@ -14,6 +14,13 @@ export declare function bridgesOf(device: Device): {
|
|
|
14
14
|
}[];
|
|
15
15
|
/** Whether the device runs in transparent mode: it has a bridge. */
|
|
16
16
|
export declare const isTransparent: (device: Device) => boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The management address of a device in transparent mode: the one IPv4 address on a port or an interface outside every bridge. The device answers on it, the platform's `manageip`, through that interface alone. Undefined when there is none, and the device is reached only through its console.
|
|
19
|
+
*/
|
|
20
|
+
export declare function transparentManagement(device: Device): {
|
|
21
|
+
name: string;
|
|
22
|
+
address: string;
|
|
23
|
+
} | undefined;
|
|
17
24
|
/** The forward domain of a VLAN interface in a bridge, or undefined. */
|
|
18
25
|
export declare function forwardDomainOf(device: Device, name: string): number | undefined;
|
|
19
26
|
/** The name the platform gives a port or an interface: a port by its spelling, any other interface by its key. */
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ import { connect, managementHost, shell } from "../../../transport/ssh";
|
|
|
11
11
|
import { portsOf, spell } from "../catalog";
|
|
12
12
|
import type { ApplyOptions, DeviceAdapter, Plan, ReadOptions } from "../types";
|
|
13
13
|
import { acceptsBeforeDrop, familiesOf, isImplied, serviceOf } from "./firewall";
|
|
14
|
-
import { holderOf, interfaceFor, isTransparent, network, settingsOf, subnet } from "./names";
|
|
14
|
+
import { holderOf, interfaceFor, isTransparent, network, settingsOf, subnet, transparentManagement } from "./names";
|
|
15
15
|
import { clean, plan } from "./plan";
|
|
16
16
|
import { COLLECTORS, FULL, HELPERS, RESOLVERS, render, SECTIONS, SPEEDS, SSH_KEYS, vlanParent, VRFS, zoneOf } from "./render";
|
|
17
17
|
import { BITS, PREFIX_LIST, routingGaps } from "./routing";
|
|
@@ -482,30 +482,49 @@ export function unsupported(device: Device): string[] {
|
|
|
482
482
|
return found;
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
-
/**
|
|
485
|
+
/**
|
|
486
|
+
* What transparent mode, which a bridge needs, leaves out: every address but one IPv4 management address outside the bridges, routing beyond static routes for it, NAT and DHCP. Without a management address the device is reached through its console and answers no service.
|
|
487
|
+
*/
|
|
486
488
|
function transparentGaps(device: Device): string[] {
|
|
487
489
|
if (!isTransparent(device)) return [];
|
|
488
490
|
|
|
489
491
|
const found: string[] = [];
|
|
490
492
|
const where = "in transparent mode, which a bridge needs";
|
|
493
|
+
const management = transparentManagement(device);
|
|
491
494
|
|
|
492
|
-
if (device.connection.host
|
|
495
|
+
if (device.connection.host && device.connection.host !== (management && host(management.address)))
|
|
496
|
+
found.push(`connection.host, ${where}: only the management address, one IPv4 address outside the bridges`);
|
|
493
497
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
498
|
+
const holders = [
|
|
499
|
+
...Object.entries(device.ports ?? {}).map(([name, port]) => [`ports.${name}`, name, port?.addresses] as const),
|
|
500
|
+
...Object.entries(device.interfaces ?? {}).map(([name, iface]) => [`interfaces.${name}`, name, iface.addresses] as const),
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
for (const [path, name, addresses] of holders) {
|
|
504
|
+
const extra = (addresses ?? []).filter(
|
|
505
|
+
(entry) => !(typeof entry === "string" && management && name === management.name && entry === management.address)
|
|
506
|
+
);
|
|
507
|
+
if (extra.length) found.push(`${path}.addresses, ${where}: one IPv4 address in all, outside the bridges, for management`);
|
|
499
508
|
}
|
|
500
509
|
|
|
501
510
|
const routing = device.routing;
|
|
502
|
-
if (routing?.
|
|
511
|
+
if (routing?.bgp || routing?.ospf) found.push(`routing, ${where}: static routes only`);
|
|
512
|
+
|
|
513
|
+
for (const [index, route] of (routing?.static ?? []).entries()) {
|
|
514
|
+
const at = `routing.static[${index}]`;
|
|
515
|
+
if (!management) found.push(`${at}, ${where}: a route needs the management address`);
|
|
516
|
+
if (!route.via || familyOf(route.via) !== "ipv4" || route.interface || route.blackhole || route.vrf)
|
|
517
|
+
found.push(`${at}, ${where}: only an IPv4 next hop, for the management address`);
|
|
518
|
+
}
|
|
519
|
+
|
|
503
520
|
if (device.firewall?.nat) found.push(`firewall.nat, ${where}`);
|
|
504
521
|
if (device.dhcp && Object.keys(device.dhcp).length) found.push(`dhcp, ${where}`);
|
|
505
522
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
523
|
+
if (!management) {
|
|
524
|
+
const services = device.management ?? {};
|
|
525
|
+
for (const service of ["ssh", "telnet", "http", "https", "api", "api_tls", "snmp", "native"] as const) {
|
|
526
|
+
if (services[service]) found.push(`management.${service}, ${where}: the device has no management address to answer on`);
|
|
527
|
+
}
|
|
509
528
|
}
|
|
510
529
|
|
|
511
530
|
return found;
|
|
@@ -16,6 +16,27 @@ export function bridgesOf(device: Device): { name: string; members: readonly str
|
|
|
16
16
|
/** Whether the device runs in transparent mode: it has a bridge. */
|
|
17
17
|
export const isTransparent = (device: Device) => bridgesOf(device).length > 0;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The management address of a device in transparent mode: the one IPv4 address on a port or an interface outside every bridge. The device answers on it, the platform's `manageip`, through that interface alone. Undefined when there is none, and the device is reached only through its console.
|
|
21
|
+
*/
|
|
22
|
+
export function transparentManagement(device: Device): { name: string; address: string } | undefined {
|
|
23
|
+
if (!isTransparent(device)) return undefined;
|
|
24
|
+
|
|
25
|
+
const members = new Set(bridgesOf(device).flatMap((bridge) => bridge.members));
|
|
26
|
+
const holders = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})] as [
|
|
27
|
+
string,
|
|
28
|
+
Addressed | undefined,
|
|
29
|
+
][];
|
|
30
|
+
|
|
31
|
+
for (const [name, settings] of holders) {
|
|
32
|
+
if (members.has(name)) continue;
|
|
33
|
+
const address = addressesOf(settings).find((each) => familyOf(host(each)) === "ipv4");
|
|
34
|
+
if (address) return { name, address };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
/** The forward domain of a VLAN interface in a bridge, or undefined. */
|
|
20
41
|
export function forwardDomainOf(device: Device, name: string): number | undefined {
|
|
21
42
|
const index = bridgesOf(device).findIndex((bridge) => bridge.members.includes(name));
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
nameOf,
|
|
16
16
|
network,
|
|
17
17
|
subnet,
|
|
18
|
+
transparentManagement,
|
|
18
19
|
vrfNumber,
|
|
19
20
|
} from "./names";
|
|
20
21
|
import { routing, usesBfd } from "./routing";
|
|
@@ -269,8 +270,12 @@ export function render(device: Device): string {
|
|
|
269
270
|
certificates(section("vpn certificate local"), section("vpn certificate ca"), device);
|
|
270
271
|
global(section("system global"), device);
|
|
271
272
|
if (device.system?.banner) section(BANNER).set("buffer", quote(device.system.banner));
|
|
272
|
-
// A bridge needs transparent mode, and transparent mode a management address
|
|
273
|
-
if (isTransparent(device))
|
|
273
|
+
// A bridge needs transparent mode, and transparent mode a management address: the config's, or one on no network the device can reach, which keeps it reached only through its console.
|
|
274
|
+
if (isTransparent(device)) {
|
|
275
|
+
const management = transparentManagement(device);
|
|
276
|
+
const manageip = management ? `${host(management.address)}/${netmask(split(management.address).len)}` : TRANSPARENT_MANAGEIP;
|
|
277
|
+
section("system settings").set("opmode", "transparent").set("manageip", manageip);
|
|
278
|
+
}
|
|
274
279
|
if (natRules(device).length) section("system settings").set("central-nat", "enable");
|
|
275
280
|
// SIP is otherwise rewritten by its proxy whatever the helpers say; this way only the `sip` helper does.
|
|
276
281
|
section("system settings").set("default-voip-alg-mode", "kernel-helper-based");
|
|
@@ -484,6 +489,12 @@ type RoutedSettings = {
|
|
|
484
489
|
|
|
485
490
|
/** 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. */
|
|
486
491
|
function routed(entry: Block, device: Device, name: string, settings: RoutedSettings, access: string): void {
|
|
492
|
+
// In transparent mode no interface holds an address: the management address is the device's, and answers on its interface alone.
|
|
493
|
+
if (isTransparent(device)) {
|
|
494
|
+
if (transparentManagement(device)?.name === name) entry.set("allowaccess", access);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
487
498
|
const addresses = addressesOf(settings);
|
|
488
499
|
const ipv4 = addresses.filter((address) => familyOf(host(address)) === "ipv4");
|
|
489
500
|
const ipv6 = addresses.filter((address) => familyOf(host(address)) === "ipv6");
|
|
@@ -732,8 +743,9 @@ function staticRoutes(ipv4: Block, ipv6: Block, device: Device): void {
|
|
|
732
743
|
|
|
733
744
|
if (route.via) entry.set("gateway", route.via);
|
|
734
745
|
|
|
746
|
+
// In transparent mode a route names no interface: it carries only the management address's own traffic.
|
|
735
747
|
const outgoing = route.interface ?? (route.via ? interfaceFor(device, route.via, route.vrf) : undefined);
|
|
736
|
-
if (outgoing && !route.blackhole) entry.set("device", quote(nameOf(device, outgoing)));
|
|
748
|
+
if (outgoing && !route.blackhole && !isTransparent(device)) entry.set("device", quote(nameOf(device, outgoing)));
|
|
737
749
|
|
|
738
750
|
if (route.distance !== undefined && route.distance !== 10) entry.set("distance", route.distance);
|
|
739
751
|
if (route.description) entry.set("comment", quote(route.description));
|