Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions apps/server/src/environment/RemoteOpenTargets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,28 @@ const layerSpawner = (input: { readonly status: CommandResult; readonly prefs: C
}),
);

const layerNet = (input: { readonly ipv4: boolean; readonly ipv6: boolean }) =>
const layerNet = (input: {
readonly ipv4: boolean;
readonly ipv6: boolean;
readonly tailnet?: boolean;
}) =>
Layer.succeed(NetService.NetService, {
canListenOnHost: () => Effect.succeed(true),
isPortAvailableOnLoopback: () => Effect.succeed(true),
hasListenerOnHost: (_port, host) => Effect.succeed(host === "::1" ? input.ipv6 : input.ipv4),
hasListenerOnHost: (_port, host) =>
Effect.succeed(
host === "127.0.0.1"
? input.ipv4
: host === "::1"
? input.ipv6
: (host === "100.64.1.2" || host === "bb-1.tail1234.ts.net") && input.tailnet === true,
),
reserveLoopbackPort: () => Effect.succeed(40_000),
findAvailablePort: (preferred) => Effect.succeed(preferred),
});

const resolveTargets = (input: {
readonly sshd: { readonly ipv4: boolean; readonly ipv6: boolean };
readonly sshd: { readonly ipv4: boolean; readonly ipv6: boolean; readonly tailnet?: boolean };
readonly tailscale: { readonly status: CommandResult; readonly prefs: CommandResult };
readonly hostname: string;
readonly username?: string | null;
Expand Down Expand Up @@ -125,6 +136,20 @@ describe("RemoteOpenTargets", () => {
}),
);

it.effect("advertises the tailnet name when sshd listens only on the tailnet address", () =>
Effect.gen(function* () {
const targets = yield* resolveTargets({
sshd: { ipv4: false, ipv6: false, tailnet: true },
tailscale: TAILSCALE_UP,
hostname: "bb-1",
username: "t3-user",
});
expect(targets).toEqual([
{ kind: "tailscale", host: "bb-1.tail1234.ts.net", username: "t3-user" },
]);
}),
);

it.effect("ignores a stored RunSSH after `tailscale down`", () =>
Effect.gen(function* () {
const targets = yield* resolveTargets({
Expand Down
20 changes: 13 additions & 7 deletions apps/server/src/environment/RemoteOpenTargets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,29 @@ export const make = Effect.gen(function* () {
);
const username = yield* HostProcess.Username;

const magicDnsName = yield* readTailscaleStatus.pipe(
Effect.map((status) => status.magicDnsName),
const tailscaleStatus = yield* readTailscaleStatus.pipe(
Effect.orElseSucceed(() => null),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
);
const magicDnsName = tailscaleStatus?.magicDnsName ?? null;

const targets: Array<RemoteOpenTarget> = [];

if (!sshdListening) {
// Tailscale SSH needs no local sshd; tailscaled alone does not prove an SSH route.
if (
magicDnsName !== null &&
// sshd may listen only on the tailnet address (ListenAddress), and Tailscale SSH
// needs no local sshd; tailscaled alone does not prove an SSH route.
if (magicDnsName === null) return targets;
const tailnetSshdListening = yield* Effect.forEach(
tailscaleStatus?.tailnetIpv4Addresses ?? [],
(address) => net.hasListenerOnHost(SSH_PORT, address),
).pipe(Effect.map((listening) => listening.includes(true)));
const sshRoute =
tailnetSshdListening ||
(yield* readTailscaleSshEnabled.pipe(
Effect.orElseSucceed(() => false),
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
))
) {
));
if (sshRoute) {
targets.push({
kind: "tailscale",
host: magicDnsName,
Expand Down
Loading