feat: convert ssh tunnels into a plugin

This commit is contained in:
LukeGus committed 2026-09-23 17:27:28 -05:00
1 parent c85c1fdb86
commit 1682861853
220 files changed
+36928 -11374

No files matched your search

@@ -521,6 +521,30 @@ describe("connectHost", () => {
).toEqual({ proxied: true });
});
it("connects over a stream it is given and skips the transport", async () => {
const connection = await connectHost(
host({
jumpHosts: [{ hostId: 2 }],
useSocks5: true,
socks5Host: "proxy",
portKnockSequence: [{ port: 7000 }],
}),
{
userId: "user-1",
purpose: "tunnel",
sock: { throughSource: true } as never,
},
);
const config = (connection.client as unknown as FakeSshClient)
.connectConfig!;
expect(config.sock).toEqual({ throughSource: true });
expect(config.hostVerifier).toBe(mocks.verifier);
expect(mocks.createJumpHostChain).not.toHaveBeenCalled();
expect(mocks.createSocks5Connection).not.toHaveBeenCalled();
expect(mocks.performPortKnocking).not.toHaveBeenCalled();
expect(connection.jumpClient).toBeNull();
});
it("knocks before connecting when the host has a sequence", async () => {
await connectHost(
host({ portKnockSequence: [{ port: 7000 }, { port: 8000 }] }),
@@ -1,81 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const canAccessHost = vi.hoisted(() => vi.fn());
vi.mock("../../utils/permission-manager.js", () => ({
PermissionManager: { getInstance: () => ({ canAccessHost }) },
}));
/**
* /ssh/tunnel/disconnect and /ssh/tunnel/cancel are addressed by tunnel NAME.
* Their ownership check used to run only inside
* `if (config && config.sourceHostId)`, and web endpoint tunnels are
* deliberately kept out of tunnelConfigs -- so `config` was always undefined
* for them and the check never ran.
*
* Host ids are small sequential integers and endpoint ids are client-supplied
* with no server-side randomness, so `web:{hostId}:{endpointId}` is guessable.
* That let any authenticated user force-close another user's live tunnel, and
* manualDisconnects then made the victim's own re-open fail for several
* seconds.
*/
describe("authorizeTunnelAction", () => {
beforeEach(() => {
canAccessHost.mockReset();
});
it("checks the host encoded in a reserved name when no config exists", async () => {
canAccessHost.mockResolvedValue({ hasAccess: true });
const { authorizeTunnelAction } =
await import("../../hosts/tunnel/routes.js");
const result = await authorizeTunnelAction("u1", "web:7:e1", undefined);
expect(result.allowed).toBe(true);
expect(canAccessHost).toHaveBeenCalledWith("u1", 7, "connect");
});
it("denies a reserved name whose host the user cannot access", async () => {
canAccessHost.mockResolvedValue({ hasAccess: false });
const { authorizeTunnelAction } =
await import("../../hosts/tunnel/routes.js");
expect(
(await authorizeTunnelAction("u1", "web:7:e1", undefined)).allowed,
).toBe(false);
});
it("fails closed on a reserved name it cannot parse", async () => {
// Never fall through to the unchecked path: an unparseable reserved name
// means "cannot verify ownership", which must deny.
const { authorizeTunnelAction } =
await import("../../hosts/tunnel/routes.js");
for (const name of ["web:", "web:abc:e1", "web:0:e1"]) {
expect((await authorizeTunnelAction("u1", name, undefined)).allowed).toBe(
false,
);
}
expect(canAccessHost).not.toHaveBeenCalled();
});
it("still checks a registered config's host id", async () => {
canAccessHost.mockResolvedValue({ hasAccess: true });
const { authorizeTunnelAction } =
await import("../../hosts/tunnel/routes.js");
await authorizeTunnelAction("u1", "my-tunnel", { sourceHostId: 42 });
expect(canAccessHost).toHaveBeenCalledWith("u1", 42, "connect");
});
it("leaves an ordinary unregistered name allowed, as before", async () => {
// Pre-existing behaviour for non-reserved names, preserved deliberately:
// tightening it is a separate change with its own blast radius.
const { authorizeTunnelAction } =
await import("../../hosts/tunnel/routes.js");
expect(
(await authorizeTunnelAction("u1", "my-tunnel", undefined)).allowed,
).toBe(true);
expect(canAccessHost).not.toHaveBeenCalled();
});
});
@@ -1,59 +0,0 @@
import { describe, expect, it } from "vitest";
import {
RESERVED_TUNNEL_NAME_PREFIX,
buildWebEndpointTunnelName,
isReservedTunnelName,
parseReservedTunnelName,
} from "../../hosts/tunnel/utils.js";
describe("reserved tunnel names", () => {
it("builds a name from a host id and endpoint id", () => {
expect(buildWebEndpointTunnelName(7, "e1")).toBe("web:7:e1");
});
it("recognises its own names and nothing else", () => {
expect(isReservedTunnelName(buildWebEndpointTunnelName(7, "e1"))).toBe(
true,
);
expect(isReservedTunnelName("my-tunnel")).toBe(false);
expect(isReservedTunnelName("website")).toBe(false);
expect(isReservedTunnelName(RESERVED_TUNNEL_NAME_PREFIX)).toBe(true);
});
it("round-trips through parse", () => {
expect(
parseReservedTunnelName(buildWebEndpointTunnelName(7, "e1")),
).toEqual({
hostId: 7,
endpointId: "e1",
});
});
it("keeps an endpoint id containing colons intact", () => {
// Endpoint ids are client-supplied strings with no character restriction,
// so splitting on the LAST colon would corrupt them. The host id segment
// is digits only, so splitting on the first colon after it is safe.
expect(parseReservedTunnelName("web:7:a:b:c")).toEqual({
hostId: 7,
endpointId: "a:b:c",
});
});
it("returns null for anything it cannot verify", () => {
// Callers use this for an ownership check, so null must mean "cannot
// verify" and they must fail closed. Never return a partial guess.
for (const name of [
"my-tunnel",
"web:",
"web:7",
"web::e1",
"web:abc:e1",
"web:-1:e1",
"web:0:e1",
"web:7:",
"web:7.5:e1",
]) {
expect(parseReservedTunnelName(name)).toBeNull();
}
});
});
@@ -1,31 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { resolveC2SSourceHostId } from "../../../hosts/tunnel/c2s-relay.js";
describe("resolveC2SSourceHostId", () => {
it("uses the remote row matching the stable sync id", async () => {
const findBySyncId = vi.fn().mockResolvedValue(42);
await expect(
resolveC2SSourceHostId(
{ sourceHostId: 7, sourceHostSyncId: "host-sync-id" },
findBySyncId,
),
).resolves.toBe(42);
expect(findBySyncId).toHaveBeenCalledWith("host-sync-id");
});
it("keeps legacy local ids when no sync id is available", async () => {
await expect(
resolveC2SSourceHostId({ sourceHostId: 7 }, vi.fn()),
).resolves.toBe(7);
});
it("does not silently fall back to a mismatched id", async () => {
await expect(
resolveC2SSourceHostId(
{ sourceHostId: 7, sourceHostSyncId: "missing" },
vi.fn().mockResolvedValue(null),
),
).rejects.toThrow("not found on the remote server");
});
});