mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix: prefer ipv4 when resolving ssh host names (#1365)
This commit is contained in:
1 parent
4076eb66ab
commit
e88df7827b
4 files changed
+72
-6
No files matched your search
@@ -154,6 +154,17 @@ export function isRetriableDnsError(error: unknown): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LookupResult = { address: string; family: number };
|
||||||
|
|
||||||
|
// Prefer IPv4: ssh2 gets one address and has no fallback, and a name that
|
||||||
|
// also has an AAAA record often points at a v6 address sshd is not bound to.
|
||||||
|
export function pickSshAddress(result: LookupResult | LookupResult[]): string {
|
||||||
|
const list = Array.isArray(result) ? result : [result];
|
||||||
|
if (list.length === 0)
|
||||||
|
throw Object.assign(new Error("No address found"), { code: "ENOTFOUND" });
|
||||||
|
return (list.find((entry) => entry.family === 4) ?? list[0]).address;
|
||||||
|
}
|
||||||
|
|
||||||
/** Resolves a hostname up front, retrying transient container DNS failures. */
|
/** Resolves a hostname up front, retrying transient container DNS failures. */
|
||||||
export async function resolveHostForSshConnect(
|
export async function resolveHostForSshConnect(
|
||||||
host: string,
|
host: string,
|
||||||
@@ -168,10 +179,10 @@ export async function resolveHostForSshConnect(
|
|||||||
|
|
||||||
for (let attempt = 0; ; attempt += 1) {
|
for (let attempt = 0; ; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const result = await lookup(normalized);
|
const address = pickSshAddress(await lookup(normalized, { all: true }));
|
||||||
return {
|
return {
|
||||||
host: result.address,
|
host: address,
|
||||||
resolvedAddress: result.address,
|
resolvedAddress: address,
|
||||||
attempts: attempt + 1,
|
attempts: attempt + 1,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
pickSshAddress,
|
||||||
|
resolveHostForSshConnect,
|
||||||
|
} from "../../src/backend/helpers.js";
|
||||||
|
|
||||||
|
describe("resolveHostForSshConnect", () => {
|
||||||
|
it("prefers IPv4 over IPv6", async () => {
|
||||||
|
const lookup = vi.fn().mockResolvedValue([
|
||||||
|
{ address: "fe80::1", family: 6 },
|
||||||
|
{ address: "192.168.1.20", family: 4 },
|
||||||
|
]);
|
||||||
|
const result = await resolveHostForSshConnect("nas", lookup);
|
||||||
|
expect(result.host).toBe("192.168.1.20");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps an IPv6-only answer", () => {
|
||||||
|
expect(pickSshAddress({ address: "2001:db8::5", family: 6 })).toBe(
|
||||||
|
"2001:db8::5",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -25,6 +25,17 @@ export function shouldResolveBeforeSshConnect(host: string): boolean {
|
|||||||
return net.isIP(normalized) === 0;
|
return net.isIP(normalized) === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LookupResult = { address: string; family: number };
|
||||||
|
|
||||||
|
// Prefer IPv4: ssh2 gets one address and has no fallback, and a name that
|
||||||
|
// also has an AAAA record often points at a v6 address sshd is not bound to.
|
||||||
|
export function pickSshAddress(result: LookupResult | LookupResult[]): string {
|
||||||
|
const list = Array.isArray(result) ? result : [result];
|
||||||
|
if (list.length === 0)
|
||||||
|
throw Object.assign(new Error("No address found"), { code: "ENOTFOUND" });
|
||||||
|
return (list.find((entry) => entry.family === 4) ?? list[0]).address;
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveHostForSshConnect(
|
export async function resolveHostForSshConnect(
|
||||||
host: string,
|
host: string,
|
||||||
lookup: Lookup = dns.lookup,
|
lookup: Lookup = dns.lookup,
|
||||||
@@ -38,10 +49,10 @@ export async function resolveHostForSshConnect(
|
|||||||
|
|
||||||
for (let attempt = 0; ; attempt += 1) {
|
for (let attempt = 0; ; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const result = await lookup(normalized);
|
const address = pickSshAddress(await lookup(normalized, { all: true }));
|
||||||
return {
|
return {
|
||||||
host: result.address,
|
host: address,
|
||||||
resolvedAddress: result.address,
|
resolvedAddress: address,
|
||||||
attempts: attempt + 1,
|
attempts: attempt + 1,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
isRetriableDnsError,
|
isRetriableDnsError,
|
||||||
|
pickSshAddress,
|
||||||
resolveHostForSshConnect,
|
resolveHostForSshConnect,
|
||||||
resolveSshConnectConfigHost,
|
resolveSshConnectConfigHost,
|
||||||
shouldResolveBeforeSshConnect,
|
shouldResolveBeforeSshConnect,
|
||||||
@@ -76,4 +77,25 @@ describe("SSH DNS resolution", () => {
|
|||||||
resolvedHost: "10.0.0.6",
|
resolvedHost: "10.0.0.6",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("prefers an IPv4 address when a name has both", async () => {
|
||||||
|
const lookup = vi.fn().mockResolvedValue([
|
||||||
|
{ address: "::1", family: 6 },
|
||||||
|
{ address: "10.0.0.7", family: 4 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(resolveHostForSshConnect("box", lookup)).resolves.toEqual({
|
||||||
|
host: "10.0.0.7",
|
||||||
|
resolvedAddress: "10.0.0.7",
|
||||||
|
attempts: 1,
|
||||||
|
});
|
||||||
|
expect(lookup).toHaveBeenCalledWith("box", { all: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to IPv6 when that is all there is", () => {
|
||||||
|
expect(pickSshAddress([{ address: "2001:db8::2", family: 6 }])).toBe(
|
||||||
|
"2001:db8::2",
|
||||||
|
);
|
||||||
|
expect(() => pickSshAddress([])).toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user