1import * as NodeDnsPromises from "node:dns/promises";
2import * as NodeNet from "node:net";
3import type { SshDeviceHostConfig } from "@t3tools/contracts";
4import * as NodeOS from "node:os";
5import * as Context from "effect/Context";
6import { runSshCommand } from "@t3tools/ssh/command";
7import * as Effect from "effect/Effect";
8
9export const LocalDeviceHostAddresses = Context.Reference<ReadonlySet<string>>(
10 "LocalDeviceHostAddresses",
11 {
12 defaultValue: () =>
13 new Set(
14 Object.values(NodeOS.networkInterfaces()).flatMap(
15 (entries) => entries?.map((entry) => entry.address) ?? [],
16 ),
17 ),
18 },
19);
20
21/** Resolve aliases on the owning environment without opening an SSH connection. */
22export const isLocalSshDeviceHost = Effect.fn("isLocalSshDeviceHost")(function* (
23 host: SshDeviceHostConfig,
24) {
25 const result = yield* runSshCommand(
26 { alias: host.target, hostname: host.target, username: null, port: host.port ?? null },
27 {
28 preHostArgs: ["-G", ...(host.identityFile ? ["-i", host.identityFile] : [])],
29 timeoutMs: 5000,
30 },
31 ).pipe(Effect.result);
32 if (result._tag === "Failure") return false;
33 const config = new Map(
34 result.success.stdout.split("\n").map((line) => {
35 const separator = line.indexOf(" ");
36 return [line.slice(0, separator), line.slice(separator + 1).trim()];
37 }),
38 );
39 // A local forwarded port or a proxy can lead to a different machine.
40 if (
41 config.get("port") !== "22" ||
42 ["proxycommand", "proxyjump"].some((key) => config.has(key) && config.get(key) !== "none")
43 )
44 return false;
45 const hostname = config.get("hostname")?.replace(/^\[|\]$/g, "");
46 if (!hostname) return false;
47 const addresses = NodeNet.isIP(hostname)
48 ? [hostname]
49 : yield* Effect.tryPromise(() => NodeDnsPromises.lookup(hostname, { all: true })).pipe(
50 Effect.map((entries) => entries.map((entry) => entry.address)),
51 Effect.timeout("2 seconds"),
52 Effect.orElseSucceed(() => [] as string[]),
53 );
54 const localAddresses = yield* LocalDeviceHostAddresses;
55 return (
56 addresses.length > 0 &&
57 addresses.every(
58 (address) => localAddresses.has(address) || address === "::1" || address.startsWith("127."),
59 )
60 );
61});
62
63export const remoteSshDeviceHosts = Effect.fn("remoteSshDeviceHosts")(function* (
64 hosts: ReadonlyArray<SshDeviceHostConfig>,
65) {
66 return yield* Effect.filter(
67 hosts,
68 (host) => isLocalSshDeviceHost(host).pipe(Effect.map((local) => !local)),
69 { concurrency: 4 },
70 );
71});