@takodotid/circuit 0.31.0 → 0.32.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 +39 -23
- package/dist/types/adapters/devices/vrp/index.d.ts +1 -1
- package/dist/types/schema/device.d.ts +2 -0
- package/dist/types/schema/policy.d.ts +2 -0
- package/dist/types/transport/console.d.ts +4 -2
- package/package.json +1 -1
- package/src/adapters/devices/fortios/index.ts +2 -2
- package/src/adapters/devices/fortios/routing.ts +1 -1
- package/src/adapters/devices/junos/bgp.ts +6 -0
- package/src/adapters/devices/junos/index.ts +1 -1
- package/src/adapters/devices/raisecom-ros/index.ts +2 -2
- package/src/adapters/devices/routeros/index.ts +2 -2
- package/src/adapters/devices/routeros/render.ts +2 -0
- package/src/adapters/devices/vrp/TRAPS.md +2 -0
- package/src/adapters/devices/vrp/index.ts +5 -3
- package/src/schema/device.ts +2 -0
- package/src/schema/policy.ts +2 -0
- package/src/transport/console.ts +30 -15
package/dist/cli/index.js
CHANGED
|
@@ -425,7 +425,7 @@ function expect(channel, write, patterns, timeout = 30000) {
|
|
|
425
425
|
channel.write(write);
|
|
426
426
|
});
|
|
427
427
|
}
|
|
428
|
-
async function consoleLogin(channel, dialect, user, password) {
|
|
428
|
+
async function consoleLogin(channel, dialect, user, password, passwordOnly = false) {
|
|
429
429
|
const others = dialect.others ?? [];
|
|
430
430
|
const states = [dialect.login, dialect.password, ...others.map(([pattern]) => pattern), dialect.prompt];
|
|
431
431
|
const toward = (index) => {
|
|
@@ -438,32 +438,38 @@ async function consoleLogin(channel, dialect, user, password) {
|
|
|
438
438
|
`;
|
|
439
439
|
};
|
|
440
440
|
await quiet(channel, 1000);
|
|
441
|
+
const start = passwordOnly ? 1 : 0;
|
|
441
442
|
let state = (await expect(channel, `
|
|
442
443
|
`, states)).index;
|
|
443
|
-
for (let step = 0;state !==
|
|
444
|
+
for (let step = 0;state !== start; step++) {
|
|
445
|
+
if (passwordOnly && state === 0)
|
|
446
|
+
throw new Error("the console asks for a name, but its console is set to ask only for a password");
|
|
444
447
|
if (step >= 10)
|
|
445
|
-
throw new Error(
|
|
448
|
+
throw new Error(`the console never reached a ${passwordOnly ? "password" : "login"} prompt`);
|
|
446
449
|
state = (await expect(channel, toward(state), states)).index;
|
|
447
450
|
}
|
|
448
451
|
await quiet(channel, 1000);
|
|
449
|
-
|
|
450
|
-
|
|
452
|
+
if (!passwordOnly) {
|
|
453
|
+
const name = dialect.user ? dialect.user(user) : user;
|
|
454
|
+
if ((await expect(channel, `${name}
|
|
451
455
|
`, [dialect.password, dialect.login, dialect.prompt])).index !== 0) {
|
|
452
|
-
|
|
456
|
+
throw new Error("the console did not ask for a password");
|
|
457
|
+
}
|
|
453
458
|
}
|
|
459
|
+
const refusal = dialect.refused ? [dialect.refused] : [];
|
|
460
|
+
const first = 2 + refusal.length;
|
|
461
|
+
const answers = [dialect.prompt, dialect.login, ...refusal, ...others.map(([pattern]) => pattern)];
|
|
454
462
|
let answer = await expect(channel, `${password}
|
|
455
|
-
`,
|
|
456
|
-
for (let step = 0;answer.index >=
|
|
463
|
+
`, answers, 60000);
|
|
464
|
+
for (let step = 0;answer.index >= first; step++) {
|
|
457
465
|
if (step >= 5)
|
|
458
466
|
throw new Error("the console kept asking after the login");
|
|
459
|
-
answer = await expect(channel, others[answer.index -
|
|
460
|
-
dialect.prompt,
|
|
461
|
-
dialect.login,
|
|
462
|
-
...others.map(([pattern]) => pattern)
|
|
463
|
-
]);
|
|
467
|
+
answer = await expect(channel, others[answer.index - first][1], answers);
|
|
464
468
|
}
|
|
465
469
|
if (answer.index === 1)
|
|
466
470
|
throw new Error(`the console refused the login of ${user}`);
|
|
471
|
+
if (answer.index === 2 && dialect.refused)
|
|
472
|
+
throw new Error("the console refused the password");
|
|
467
473
|
await quiet(channel, 800);
|
|
468
474
|
}
|
|
469
475
|
function quiet(channel, silence) {
|
|
@@ -1312,7 +1318,7 @@ function ruleGaps(device, where, rule) {
|
|
|
1312
1318
|
const set = rule.set ?? {};
|
|
1313
1319
|
if ((rule.match || rule.set) && !rule.action)
|
|
1314
1320
|
found.push(`${where}, a rule without action: the platform stops at the first rule that matches`);
|
|
1315
|
-
for (const field of ["as_path_length", "rpki", "large_community"]) {
|
|
1321
|
+
for (const field of ["as_path_length", "through_as", "rpki", "large_community"]) {
|
|
1316
1322
|
if (match[field] !== undefined)
|
|
1317
1323
|
found.push(`${where}.match.${field}`);
|
|
1318
1324
|
}
|
|
@@ -2761,9 +2767,9 @@ async function consoleSession(device, work) {
|
|
|
2761
2767
|
const { console: console2, user, password } = consoleOf(device);
|
|
2762
2768
|
const opened = await openConsole(console2);
|
|
2763
2769
|
try {
|
|
2764
|
-
await consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password);
|
|
2770
|
+
await consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password, console2.passwordOnly);
|
|
2765
2771
|
const cli = new Cli(opened.channel);
|
|
2766
|
-
const relogin = () => consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password);
|
|
2772
|
+
const relogin = () => consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password, console2.passwordOnly);
|
|
2767
2773
|
const result = await work(cli, relogin);
|
|
2768
2774
|
await consoleLogout(cli);
|
|
2769
2775
|
return result;
|
|
@@ -4345,6 +4351,11 @@ function term(out, device, term, rule, family, splitFamily) {
|
|
|
4345
4351
|
out.set(`policy-options as-path ${name}`, quote2(`.{${min},${max ?? ""}}`));
|
|
4346
4352
|
from.push(["as-path", name]);
|
|
4347
4353
|
}
|
|
4354
|
+
if (match.through_as !== undefined) {
|
|
4355
|
+
const name = `circuit-through-${match.through_as}`;
|
|
4356
|
+
out.set(`policy-options as-path ${name}`, quote2(`.* ${match.through_as} .*`));
|
|
4357
|
+
from.push(["as-path", name]);
|
|
4358
|
+
}
|
|
4348
4359
|
if (match.rpki)
|
|
4349
4360
|
from.push(["validation-database", VALIDATION[match.rpki]]);
|
|
4350
4361
|
const wanted = [
|
|
@@ -4657,7 +4668,7 @@ async function onConsole(device, work) {
|
|
|
4657
4668
|
const { console: console2, user, password } = consoleOf(device);
|
|
4658
4669
|
const opened = await openConsole(console2);
|
|
4659
4670
|
const login = async () => {
|
|
4660
|
-
await consoleLogin(opened.channel, JUNOS_CONSOLE, user, password);
|
|
4671
|
+
await consoleLogin(opened.channel, JUNOS_CONSOLE, user, password, console2.passwordOnly);
|
|
4661
4672
|
const ready = openCli(opened.channel);
|
|
4662
4673
|
opened.channel.write(`
|
|
4663
4674
|
`);
|
|
@@ -5529,7 +5540,7 @@ async function consoleShell(device) {
|
|
|
5529
5540
|
const { console: console2, user, password } = consoleOf(device);
|
|
5530
5541
|
const opened = await openConsole(console2);
|
|
5531
5542
|
try {
|
|
5532
|
-
await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password);
|
|
5543
|
+
await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password, console2.passwordOnly);
|
|
5533
5544
|
} catch (error) {
|
|
5534
5545
|
opened.close();
|
|
5535
5546
|
throw error;
|
|
@@ -5542,7 +5553,7 @@ async function consoleShell(device) {
|
|
|
5542
5553
|
opened.close();
|
|
5543
5554
|
},
|
|
5544
5555
|
fresh: async () => {
|
|
5545
|
-
await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password);
|
|
5556
|
+
await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password, console2.passwordOnly);
|
|
5546
5557
|
await send(opened.channel, "terminal page-break disable", 15000, 1000);
|
|
5547
5558
|
return session;
|
|
5548
5559
|
}
|
|
@@ -7080,6 +7091,8 @@ function conditions(rule) {
|
|
|
7080
7091
|
tests.push(`bgp-path-len>${match.as_path_length.min - 1}`);
|
|
7081
7092
|
if (match.as_path_length?.max !== undefined)
|
|
7082
7093
|
tests.push(`bgp-path-len<${match.as_path_length.max + 1}`);
|
|
7094
|
+
if (match.through_as !== undefined)
|
|
7095
|
+
tests.push(`bgp-as-path ${match.through_as}`);
|
|
7083
7096
|
if (match.rpki)
|
|
7084
7097
|
tests.push(`rpki ${match.rpki}`);
|
|
7085
7098
|
if (match.community)
|
|
@@ -7478,7 +7491,7 @@ async function consoleSession2(device) {
|
|
|
7478
7491
|
const { console: console2, user, password } = consoleOf(device);
|
|
7479
7492
|
const opened = await openConsole(console2);
|
|
7480
7493
|
try {
|
|
7481
|
-
await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password);
|
|
7494
|
+
await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password, console2.passwordOnly);
|
|
7482
7495
|
} catch (error) {
|
|
7483
7496
|
opened.close();
|
|
7484
7497
|
throw error;
|
|
@@ -7487,7 +7500,7 @@ async function consoleSession2(device) {
|
|
|
7487
7500
|
const session = {
|
|
7488
7501
|
run: (command, timeout) => shell.run(command, timeout),
|
|
7489
7502
|
fresh: async () => {
|
|
7490
|
-
await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password);
|
|
7503
|
+
await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password, console2.passwordOnly);
|
|
7491
7504
|
return session;
|
|
7492
7505
|
},
|
|
7493
7506
|
end: () => {
|
|
@@ -7726,7 +7739,7 @@ var DEVICE_FACTS3 = [
|
|
|
7726
7739
|
"domain default",
|
|
7727
7740
|
"Info:"
|
|
7728
7741
|
];
|
|
7729
|
-
var UNDO_BY_KEYWORD2 = /^(description|name|port default vlan|port link-type|port trunk pvid vlan|set authentication password|eth-trunk|ip binding vpn-instance)\b/;
|
|
7742
|
+
var UNDO_BY_KEYWORD2 = /^(description|name|port default vlan|port link-type|port trunk pvid vlan|set authentication password|eth-trunk|ip binding vpn-instance|ssh server cipher|ssh server hmac|ssh server key-exchange)\b/;
|
|
7730
7743
|
function undo2(line) {
|
|
7731
7744
|
if (line.startsWith("undo "))
|
|
7732
7745
|
return line.slice("undo ".length);
|
|
@@ -8176,7 +8189,10 @@ var VRP_CONSOLE = {
|
|
|
8176
8189
|
password: /Password:\s*$/i,
|
|
8177
8190
|
prompt: /<[^>\n]+>\s*$/,
|
|
8178
8191
|
logout: "quit",
|
|
8192
|
+
refused: /The password is invalid/i,
|
|
8179
8193
|
others: [
|
|
8194
|
+
[/Please Press ENTER\.?\s*$/i, `
|
|
8195
|
+
`],
|
|
8180
8196
|
[/Change now\? ?\[Y\/N\]:?\s*$/i, `N
|
|
8181
8197
|
`],
|
|
8182
8198
|
[/\[~?\*?[^\]\n]+\]\s*$/, `return
|
|
@@ -8187,7 +8203,7 @@ async function onConsole2(device, work) {
|
|
|
8187
8203
|
const { console: console2, user, password } = consoleOf(device);
|
|
8188
8204
|
const opened = await openConsole(console2);
|
|
8189
8205
|
const login = async () => {
|
|
8190
|
-
await consoleLogin(opened.channel, VRP_CONSOLE, user, password);
|
|
8206
|
+
await consoleLogin(opened.channel, VRP_CONSOLE, user, password, console2.passwordOnly);
|
|
8191
8207
|
await send(opened.channel, "screen-length 0 temporary", 1e4, 1500);
|
|
8192
8208
|
};
|
|
8193
8209
|
try {
|
|
@@ -18,7 +18,7 @@ export declare function login(device: Device): Promise<Client>;
|
|
|
18
18
|
* Every login asks whether to change the password. Answering yes replaces it at once and every stored credential with it, so the answer is always no. Anything sent before the question is answered is taken as the answer.
|
|
19
19
|
*/
|
|
20
20
|
export declare function openShell(client: Client): Promise<ClientChannel>;
|
|
21
|
-
/** How the console asks for a login: the same question to change the password, always answered no,
|
|
21
|
+
/** How the console asks for a login: the same question to change the password, always answered no, a system view left with `return`, and the line's banner after a logout or a refused password answered with Enter. Not yet proved on a device. */
|
|
22
22
|
export declare const VRP_CONSOLE: ConsoleDialect;
|
|
23
23
|
/** What the device runs, credentials scrubbed. */
|
|
24
24
|
export declare function read(device: Device, _network?: readonly Device[], options?: ReadOptions): Promise<string>;
|
|
@@ -36,6 +36,8 @@ export type Console = {
|
|
|
36
36
|
port: string;
|
|
37
37
|
/** Line speed in bits per second, such as 9600 or 115200. */
|
|
38
38
|
baud: number;
|
|
39
|
+
/** The console asks only for a password, with no name first. Circuit then types no name; `connection.user` still picks the password. */
|
|
40
|
+
passwordOnly?: true;
|
|
39
41
|
};
|
|
40
42
|
/** A separate routing table, with the interfaces that route in it. */
|
|
41
43
|
export type Vrf = {
|
|
@@ -33,6 +33,8 @@ export type Match<S extends string> = {
|
|
|
33
33
|
min?: number;
|
|
34
34
|
max?: number;
|
|
35
35
|
};
|
|
36
|
+
/** The AS path holds this AS anywhere: the route came from it or through it. */
|
|
37
|
+
through_as?: number;
|
|
36
38
|
/** The route's origin validation state. */
|
|
37
39
|
rpki?: "valid" | "invalid" | "not-found";
|
|
38
40
|
/** Has this standard community, `asn:value`. */
|
|
@@ -27,6 +27,8 @@ export type ConsoleDialect = {
|
|
|
27
27
|
others?: readonly (readonly [RegExp, string])[];
|
|
28
28
|
/** The name typed at the login prompt, when the platform adds to it. */
|
|
29
29
|
user?: (user: string) => string;
|
|
30
|
+
/** What the console prints when it refuses a password, for a console that then shows no login prompt. */
|
|
31
|
+
refused?: RegExp;
|
|
30
32
|
};
|
|
31
33
|
/** The channel a console speaks on: an SSH stream from the relay, or anything shaped like one. */
|
|
32
34
|
type Channel = Pick<ClientChannel, "write"> & {
|
|
@@ -39,9 +41,9 @@ export declare function expect(channel: Channel, write: string, patterns: readon
|
|
|
39
41
|
text: string;
|
|
40
42
|
}>;
|
|
41
43
|
/**
|
|
42
|
-
* Log in on a console, whatever state the last session left it in: what `others` names is moved on, a session left open is logged out, so every session starts from a fresh login. Returns once the CLI's prompt shows.
|
|
44
|
+
* Log in on a console, whatever state the last session left it in: what `others` names is moved on, a session left open is logged out, so every session starts from a fresh login. Returns once the CLI's prompt shows. A console that asks only for a password (`passwordOnly`) is logged in at its password prompt, with no name typed.
|
|
43
45
|
*/
|
|
44
|
-
export declare function consoleLogin(channel: Channel, dialect: ConsoleDialect, user: string, password: string): Promise<void>;
|
|
46
|
+
export declare function consoleLogin(channel: Channel, dialect: ConsoleDialect, user: string, password: string, passwordOnly?: boolean): Promise<void>;
|
|
45
47
|
/** The text after a command's echo. A terminal wraps a long echo where it likes, so the echo is matched character by character, line breaks it added skipped. */
|
|
46
48
|
export declare function afterEcho(text: string, command: string): string;
|
|
47
49
|
/** A CLI on a console that runs one command at a time and returns what it printed, as an SSH exec would: no echo, no prompt. */
|
package/package.json
CHANGED
|
@@ -173,9 +173,9 @@ async function consoleSession<T>(device: Device, work: (cli: Cli, relogin: () =>
|
|
|
173
173
|
const opened = await openConsole(console);
|
|
174
174
|
|
|
175
175
|
try {
|
|
176
|
-
await consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password);
|
|
176
|
+
await consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password, console.passwordOnly);
|
|
177
177
|
const cli = new Cli(opened.channel);
|
|
178
|
-
const relogin = () => consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password);
|
|
178
|
+
const relogin = () => consoleLogin(opened.channel, FORTIOS_CONSOLE, user, password, console.passwordOnly);
|
|
179
179
|
|
|
180
180
|
const result = await work(cli, relogin);
|
|
181
181
|
await consoleLogout(cli);
|
|
@@ -418,7 +418,7 @@ function ruleGaps(device: Device, where: string, rule: AnyRule): string[] {
|
|
|
418
418
|
if ((rule.match || rule.set) && !rule.action)
|
|
419
419
|
found.push(`${where}, a rule without action: the platform stops at the first rule that matches`);
|
|
420
420
|
|
|
421
|
-
for (const field of ["as_path_length", "rpki", "large_community"] as const) {
|
|
421
|
+
for (const field of ["as_path_length", "through_as", "rpki", "large_community"] as const) {
|
|
422
422
|
if (match[field] !== undefined) found.push(`${where}.match.${field}`);
|
|
423
423
|
}
|
|
424
424
|
|
|
@@ -210,6 +210,12 @@ function term(out: Statements, device: Device, term: string, rule: AnyRule, fami
|
|
|
210
210
|
from.push(["as-path", name]);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
if (match.through_as !== undefined) {
|
|
214
|
+
const name = `circuit-through-${match.through_as}`;
|
|
215
|
+
out.set(`policy-options as-path ${name}`, quote(`.* ${match.through_as} .*`));
|
|
216
|
+
from.push(["as-path", name]);
|
|
217
|
+
}
|
|
218
|
+
|
|
213
219
|
if (match.rpki) from.push(["validation-database", VALIDATION[match.rpki]]);
|
|
214
220
|
|
|
215
221
|
// Members of one named community must all be present, so a rule matching both kinds names them together.
|
|
@@ -375,7 +375,7 @@ async function onConsole<T>(device: Device, work: (channel: ClientChannel, relog
|
|
|
375
375
|
|
|
376
376
|
// The CLI is opened on a prompt asked for after the login, since the login's own prompt is already read.
|
|
377
377
|
const login = async () => {
|
|
378
|
-
await consoleLogin(opened.channel, JUNOS_CONSOLE, user, password);
|
|
378
|
+
await consoleLogin(opened.channel, JUNOS_CONSOLE, user, password, console.passwordOnly);
|
|
379
379
|
const ready = openCli(opened.channel);
|
|
380
380
|
opened.channel.write("\n");
|
|
381
381
|
await ready;
|
|
@@ -371,7 +371,7 @@ async function consoleShell(device: Device): Promise<Session> {
|
|
|
371
371
|
const opened = await openConsole(console);
|
|
372
372
|
|
|
373
373
|
try {
|
|
374
|
-
await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password);
|
|
374
|
+
await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password, console.passwordOnly);
|
|
375
375
|
} catch (error) {
|
|
376
376
|
opened.close();
|
|
377
377
|
throw error;
|
|
@@ -384,7 +384,7 @@ async function consoleShell(device: Device): Promise<Session> {
|
|
|
384
384
|
opened.close();
|
|
385
385
|
},
|
|
386
386
|
fresh: async () => {
|
|
387
|
-
await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password);
|
|
387
|
+
await consoleLogin(opened.channel, RAISECOM_CONSOLE, user, password, console.passwordOnly);
|
|
388
388
|
await send(opened.channel, "terminal page-break disable", 15_000, 1000);
|
|
389
389
|
return session;
|
|
390
390
|
},
|
|
@@ -68,7 +68,7 @@ async function consoleSession(device: Device): Promise<Session> {
|
|
|
68
68
|
const opened = await openConsole(console);
|
|
69
69
|
|
|
70
70
|
try {
|
|
71
|
-
await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password);
|
|
71
|
+
await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password, console.passwordOnly);
|
|
72
72
|
} catch (error) {
|
|
73
73
|
opened.close();
|
|
74
74
|
throw error;
|
|
@@ -79,7 +79,7 @@ async function consoleSession(device: Device): Promise<Session> {
|
|
|
79
79
|
const session: Session = {
|
|
80
80
|
run: (command, timeout) => shell.run(command, timeout),
|
|
81
81
|
fresh: async () => {
|
|
82
|
-
await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password);
|
|
82
|
+
await consoleLogin(opened.channel, ROUTEROS_CONSOLE, user, password, console.passwordOnly);
|
|
83
83
|
return session;
|
|
84
84
|
},
|
|
85
85
|
end: () => {
|
|
@@ -1033,6 +1033,8 @@ function conditions(rule: AnyRule): string[] {
|
|
|
1033
1033
|
if (match.prefix_length?.max !== undefined) tests.push(`dst-len<${match.prefix_length.max + 1}`);
|
|
1034
1034
|
if (match.as_path_length?.min !== undefined) tests.push(`bgp-path-len>${match.as_path_length.min - 1}`);
|
|
1035
1035
|
if (match.as_path_length?.max !== undefined) tests.push(`bgp-path-len<${match.as_path_length.max + 1}`);
|
|
1036
|
+
// The platform's AS path regexp matches each AS as a whole number, so a bare number is that AS anywhere in the path.
|
|
1037
|
+
if (match.through_as !== undefined) tests.push(`bgp-as-path ${match.through_as}`);
|
|
1036
1038
|
if (match.rpki) tests.push(`rpki ${match.rpki}`);
|
|
1037
1039
|
if (match.community) tests.push(`bgp-communities includes ${match.community}`);
|
|
1038
1040
|
if (match.large_community) tests.push(`bgp-large-communities includes ${match.large_community}`);
|
|
@@ -21,3 +21,5 @@ Behaviour that looks like something else. Each one has been hit on a CE6855 runn
|
|
|
21
21
|
- sFlow samples per port, `sflow sampling rate N`, N from 4096; the default is 8192 and an explicit 8192 is kept as written. A port can point to a collector only once the collector exists globally; until then it is refused with "collector does not exist".
|
|
22
22
|
- `vlan batch` has every VLAN on one line, and undoing a list that includes a VLAN with an L3 interface is refused. A change sends only the VLANs added and `undo vlan batch` for the ones removed.
|
|
23
23
|
- A VLAN name is unique across VLANs: a new VLAN cannot take a name another still has. VLANs no longer used, and their VLAN interfaces, are removed first, in the same candidate.
|
|
24
|
+
- `undo ssh server key-exchange <algorithms>` is refused with "Too many parameters"; `undo ssh server key-exchange` takes none. `hmac` was proved the same way. `cipher` is undone the same way but was not proved on its own.
|
|
25
|
+
- A console on a password only (`passwordOnly`) shows `Please Press ENTER.` after a logout or a refused password, and `Error: The password is invalid.` for a wrong one.
|
|
@@ -37,7 +37,7 @@ const DEVICE_FACTS = [
|
|
|
37
37
|
|
|
38
38
|
/** Lines whose removal takes only their keyword, not their value. */
|
|
39
39
|
const UNDO_BY_KEYWORD =
|
|
40
|
-
/^(description|name|port default vlan|port link-type|port trunk pvid vlan|set authentication password|eth-trunk|ip binding vpn-instance)\b/;
|
|
40
|
+
/^(description|name|port default vlan|port link-type|port trunk pvid vlan|set authentication password|eth-trunk|ip binding vpn-instance|ssh server cipher|ssh server hmac|ssh server key-exchange)\b/;
|
|
41
41
|
|
|
42
42
|
function undo(line: string): string {
|
|
43
43
|
// A negated line is undone by stating it.
|
|
@@ -571,13 +571,15 @@ export async function openShell(client: Client): Promise<ClientChannel> {
|
|
|
571
571
|
return channel;
|
|
572
572
|
}
|
|
573
573
|
|
|
574
|
-
/** How the console asks for a login: the same question to change the password, always answered no,
|
|
574
|
+
/** How the console asks for a login: the same question to change the password, always answered no, a system view left with `return`, and the line's banner after a logout or a refused password answered with Enter. Not yet proved on a device. */
|
|
575
575
|
export const VRP_CONSOLE: ConsoleDialect = {
|
|
576
576
|
login: /Username:\s*$/i,
|
|
577
577
|
password: /Password:\s*$/i,
|
|
578
578
|
prompt: /<[^>\n]+>\s*$/,
|
|
579
579
|
logout: "quit",
|
|
580
|
+
refused: /The password is invalid/i,
|
|
580
581
|
others: [
|
|
582
|
+
[/Please Press ENTER\.?\s*$/i, "\n"],
|
|
581
583
|
[/Change now\? ?\[Y\/N\]:?\s*$/i, "N\n"],
|
|
582
584
|
[/\[~?\*?[^\]\n]+\]\s*$/, "return\n"],
|
|
583
585
|
],
|
|
@@ -589,7 +591,7 @@ async function onConsole<T>(device: Device, work: (channel: ClientChannel, relog
|
|
|
589
591
|
const opened = await openConsole(console);
|
|
590
592
|
|
|
591
593
|
const login = async () => {
|
|
592
|
-
await consoleLogin(opened.channel, VRP_CONSOLE, user, password);
|
|
594
|
+
await consoleLogin(opened.channel, VRP_CONSOLE, user, password, console.passwordOnly);
|
|
593
595
|
await send(opened.channel, "screen-length 0 temporary", 10_000, 1500);
|
|
594
596
|
};
|
|
595
597
|
|
package/src/schema/device.ts
CHANGED
|
@@ -39,6 +39,8 @@ export type Console = {
|
|
|
39
39
|
port: string;
|
|
40
40
|
/** Line speed in bits per second, such as 9600 or 115200. */
|
|
41
41
|
baud: number;
|
|
42
|
+
/** The console asks only for a password, with no name first. Circuit then types no name; `connection.user` still picks the password. */
|
|
43
|
+
passwordOnly?: true;
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
/** A separate routing table, with the interfaces that route in it. */
|
package/src/schema/policy.ts
CHANGED
|
@@ -32,6 +32,8 @@ export type Match<S extends string> = {
|
|
|
32
32
|
prefix_length?: { min?: number; max?: number };
|
|
33
33
|
/** AS path length within this range, inclusive. Every AS counts, prepends included. */
|
|
34
34
|
as_path_length?: { min?: number; max?: number };
|
|
35
|
+
/** The AS path holds this AS anywhere: the route came from it or through it. */
|
|
36
|
+
through_as?: number;
|
|
35
37
|
/** The route's origin validation state. */
|
|
36
38
|
rpki?: "valid" | "invalid" | "not-found";
|
|
37
39
|
/** Has this standard community, `asn:value`. */
|
package/src/transport/console.ts
CHANGED
|
@@ -250,6 +250,8 @@ export type ConsoleDialect = {
|
|
|
250
250
|
others?: readonly (readonly [RegExp, string])[];
|
|
251
251
|
/** The name typed at the login prompt, when the platform adds to it. */
|
|
252
252
|
user?: (user: string) => string;
|
|
253
|
+
/** What the console prints when it refuses a password, for a console that then shows no login prompt. */
|
|
254
|
+
refused?: RegExp;
|
|
253
255
|
};
|
|
254
256
|
|
|
255
257
|
/** Terminal escape sequences: cursor moves, screen clears, character sets and modes. */
|
|
@@ -293,9 +295,15 @@ export function expect(channel: Channel, write: string, patterns: readonly RegEx
|
|
|
293
295
|
}
|
|
294
296
|
|
|
295
297
|
/**
|
|
296
|
-
* Log in on a console, whatever state the last session left it in: what `others` names is moved on, a session left open is logged out, so every session starts from a fresh login. Returns once the CLI's prompt shows.
|
|
298
|
+
* Log in on a console, whatever state the last session left it in: what `others` names is moved on, a session left open is logged out, so every session starts from a fresh login. Returns once the CLI's prompt shows. A console that asks only for a password (`passwordOnly`) is logged in at its password prompt, with no name typed.
|
|
297
299
|
*/
|
|
298
|
-
export async function consoleLogin(
|
|
300
|
+
export async function consoleLogin(
|
|
301
|
+
channel: Channel,
|
|
302
|
+
dialect: ConsoleDialect,
|
|
303
|
+
user: string,
|
|
304
|
+
password: string,
|
|
305
|
+
passwordOnly = false
|
|
306
|
+
): Promise<void> {
|
|
299
307
|
const others = dialect.others ?? [];
|
|
300
308
|
const states = [dialect.login, dialect.password, ...others.map(([pattern]) => pattern), dialect.prompt];
|
|
301
309
|
|
|
@@ -309,32 +317,39 @@ export async function consoleLogin(channel: Channel, dialect: ConsoleDialect, us
|
|
|
309
317
|
// What the last session printed as it left, such as a login prompt after its logout, arrives late; it must not be taken for an answer.
|
|
310
318
|
await quiet(channel, 1_000);
|
|
311
319
|
|
|
320
|
+
// The prompt a fresh login starts at: the login prompt, or the password prompt when there is no name to type.
|
|
321
|
+
const start = passwordOnly ? 1 : 0;
|
|
322
|
+
|
|
312
323
|
let state = (await expect(channel, "\n", states)).index;
|
|
313
|
-
for (let step = 0; state !==
|
|
314
|
-
if (
|
|
324
|
+
for (let step = 0; state !== start; step++) {
|
|
325
|
+
if (passwordOnly && state === 0) throw new Error("the console asks for a name, but its console is set to ask only for a password");
|
|
326
|
+
if (step >= 10) throw new Error(`the console never reached a ${passwordOnly ? "password" : "login"} prompt`);
|
|
315
327
|
state = (await expect(channel, toward(state), states)).index;
|
|
316
328
|
}
|
|
317
329
|
|
|
318
330
|
// A console can print the login prompt more than once; a name typed while it does is lost.
|
|
319
331
|
await quiet(channel, 1_000);
|
|
320
332
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
333
|
+
if (!passwordOnly) {
|
|
334
|
+
const name = dialect.user ? dialect.user(user) : user;
|
|
335
|
+
if ((await expect(channel, `${name}\n`, [dialect.password, dialect.login, dialect.prompt])).index !== 0) {
|
|
336
|
+
throw new Error("the console did not ask for a password");
|
|
337
|
+
}
|
|
324
338
|
}
|
|
325
339
|
|
|
326
|
-
// A platform may ask something once logged in, such as whether to read a licence; `others` answers it.
|
|
327
|
-
|
|
328
|
-
|
|
340
|
+
// A platform may ask something once logged in, such as whether to read a licence; `others` answers it. A refusal is checked before `others`, since the banner that follows it matches one of them.
|
|
341
|
+
const refusal = dialect.refused ? [dialect.refused] : [];
|
|
342
|
+
const first = 2 + refusal.length;
|
|
343
|
+
const answers = [dialect.prompt, dialect.login, ...refusal, ...others.map(([pattern]) => pattern)];
|
|
344
|
+
|
|
345
|
+
let answer = await expect(channel, `${password}\n`, answers, 60_000);
|
|
346
|
+
for (let step = 0; answer.index >= first; step++) {
|
|
329
347
|
if (step >= 5) throw new Error("the console kept asking after the login");
|
|
330
|
-
answer = await expect(channel, others[answer.index -
|
|
331
|
-
dialect.prompt,
|
|
332
|
-
dialect.login,
|
|
333
|
-
...others.map(([pattern]) => pattern),
|
|
334
|
-
]);
|
|
348
|
+
answer = await expect(channel, others[answer.index - first]![1], answers);
|
|
335
349
|
}
|
|
336
350
|
|
|
337
351
|
if (answer.index === 1) throw new Error(`the console refused the login of ${user}`);
|
|
352
|
+
if (answer.index === 2 && dialect.refused) throw new Error("the console refused the password");
|
|
338
353
|
|
|
339
354
|
// Some consoles print the prompt twice after a login; what arrives late must not reach the first command.
|
|
340
355
|
await quiet(channel, 800);
|