Skip to content
Open
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
13 changes: 11 additions & 2 deletions REMOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,17 @@ For hosted web pairing over Tailscale HTTPS, opt in to Tailscale Serve:
npx t3 serve --tailscale-serve
```

By default this configures Tailscale Serve on HTTPS port 443 and advertises
`https://machine.tailnet.ts.net/`. Advanced users can choose a different HTTPS port:
By default this configures Tailscale Serve on HTTPS port 443, discovers the machine's MagicDNS
name, and advertises `https://machine.tailnet.ts.net/`. If MagicDNS discovery is unavailable, the
CLI falls back to the normal headless pairing URL.

Use `--tailscale-serve-host` when the advertised HTTPS host should be fixed instead of discovered:

```bash
npx t3 serve --tailscale-serve --tailscale-serve-host machine.tailnet.ts.net
```

Advanced users can also choose a different HTTPS port:

```bash
npx t3 serve --tailscale-serve --tailscale-serve-port 8443
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/cli/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
tailscaleServeHost: Option.none(),
},
Option.none(),
).pipe(
Expand All @@ -92,6 +93,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
T3CODE_NO_BROWSER: "true",
T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "false",
T3CODE_LOG_WS_EVENTS: "true",
T3CODE_TAILSCALE_SERVE_HOST: "env-tailnet.example.ts.net",
},
}),
),
Expand All @@ -118,6 +120,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: true,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
tailscaleServeHost: "env-tailnet.example.ts.net",
});
}),
);
Expand All @@ -141,6 +144,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: Option.some(true),
tailscaleServeEnabled: Option.some(true),
tailscaleServePort: Option.some(8443),
tailscaleServeHost: Option.some("cli-tailnet.example.ts.net"),
},
Option.some("Debug"),
).pipe(
Expand All @@ -158,6 +162,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
T3CODE_NO_BROWSER: "false",
T3CODE_AUTO_BOOTSTRAP_PROJECT_FROM_CWD: "false",
T3CODE_LOG_WS_EVENTS: "false",
T3CODE_TAILSCALE_SERVE_HOST: "ignored-env-tailnet.example.ts.net",
},
}),
),
Expand All @@ -184,6 +189,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: true,
tailscaleServeEnabled: true,
tailscaleServePort: 8443,
tailscaleServeHost: "cli-tailnet.example.ts.net",
});
}),
);
Expand Down Expand Up @@ -215,6 +221,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: Option.some(false),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
tailscaleServeHost: Option.none(),
},
Option.none(),
).pipe(
Expand Down Expand Up @@ -253,6 +260,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: false,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
tailscaleServeHost: undefined,
});
}),
);
Expand Down Expand Up @@ -290,6 +298,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
tailscaleServeHost: Option.none(),
},
Option.none(),
).pipe(
Expand Down Expand Up @@ -327,6 +336,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: false,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
tailscaleServeHost: undefined,
});
assert.equal(join(baseDir, "userdata"), resolved.stateDir);
}),
Expand All @@ -353,6 +363,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
tailscaleServeHost: Option.none(),
},
Option.none(),
).pipe(
Expand Down Expand Up @@ -412,6 +423,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
tailscaleServeHost: Option.none(),
},
Option.some("Debug"),
).pipe(
Expand Down Expand Up @@ -452,6 +464,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: true,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
tailscaleServeHost: undefined,
});
}),
);
Expand Down Expand Up @@ -488,6 +501,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
tailscaleServeHost: Option.none(),
},
Option.none(),
).pipe(
Expand Down Expand Up @@ -521,6 +535,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: false,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
tailscaleServeHost: undefined,
});
}),
);
Expand All @@ -545,6 +560,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
tailscaleServeHost: Option.none(),
},
Option.none(),
{
Expand Down Expand Up @@ -584,6 +600,7 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => {
logWebSocketEvents: false,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
tailscaleServeHost: undefined,
});
}),
);
Expand Down
20 changes: 20 additions & 0 deletions apps/server/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export const tailscaleServePortFlag = Flag.integer("tailscale-serve-port").pipe(
Flag.withDescription("HTTPS port for Tailscale Serve when --tailscale-serve is enabled."),
Flag.optional,
);
export const tailscaleServeHostFlag = Flag.string("tailscale-serve-host").pipe(
Flag.withDescription("Host name for Tailscale Serve when --tailscale-serve is enabled."),
Flag.optional,
);

const EnvServerConfig = Config.all({
logLevel: Config.logLevel("T3CODE_LOG_LEVEL").pipe(Config.withDefault("Info")),
Expand Down Expand Up @@ -136,6 +140,10 @@ const EnvServerConfig = Config.all({
Config.option,
Config.map(Option.getOrUndefined),
),
tailscaleServeHost: Config.string("T3CODE_TAILSCALE_SERVE_HOST").pipe(
Config.option,
Config.map(Option.getOrUndefined),
),
});

export interface CliServerFlags {
Expand All @@ -151,6 +159,7 @@ export interface CliServerFlags {
readonly logWebSocketEvents: Option.Option<boolean>;
readonly tailscaleServeEnabled: Option.Option<boolean>;
readonly tailscaleServePort: Option.Option<number>;
readonly tailscaleServeHost: Option.Option<string>;
}

export interface CliAuthLocationFlags {
Expand Down Expand Up @@ -185,6 +194,7 @@ export const sharedServerCommandFlags = {
logWebSocketEvents: logWebSocketEventsFlag,
tailscaleServeEnabled: tailscaleServeFlag,
tailscaleServePort: tailscaleServePortFlag,
tailscaleServeHost: tailscaleServeHostFlag,
} as const;

export const authLocationFlags = sharedServerLocationFlags;
Expand Down Expand Up @@ -230,6 +240,7 @@ export const resolveServerConfig = (
logWebSocketEvents: flags.logWebSocketEvents ?? Option.none(),
tailscaleServeEnabled: flags.tailscaleServeEnabled ?? Option.none(),
tailscaleServePort: flags.tailscaleServePort ?? Option.none(),
tailscaleServeHost: flags.tailscaleServeHost ?? Option.none(),
} satisfies CliServerFlags;
const bootstrapFd = Option.getOrUndefined(normalizedFlags.bootstrapFd) ?? env.bootstrapFd;
const bootstrapEnvelope =
Expand Down Expand Up @@ -330,6 +341,13 @@ export const resolveServerConfig = (
),
() => 443,
);
const tailscaleServeHost = Option.getOrElse(
resolveOptionPrecedence(
normalizedFlags.tailscaleServeHost,
Option.fromUndefinedOr(env.tailscaleServeHost),
),
() => undefined,
);
const staticDir = devUrl ? undefined : yield* resolveStaticDir();
const host = Option.getOrElse(
resolveOptionPrecedence(
Expand Down Expand Up @@ -374,6 +392,7 @@ export const resolveServerConfig = (
logWebSocketEvents,
tailscaleServeEnabled,
tailscaleServePort,
tailscaleServeHost,
};

return config;
Expand All @@ -397,6 +416,7 @@ export const resolveCliAuthConfig = (
logWebSocketEvents: Option.none(),
tailscaleServeEnabled: Option.none(),
tailscaleServePort: Option.none(),
tailscaleServeHost: Option.none(),
},
cliLogLevel,
);
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface ServerConfigShape extends ServerDerivedPaths {
readonly logWebSocketEvents: boolean;
readonly tailscaleServeEnabled: boolean;
readonly tailscaleServePort: number;
readonly tailscaleServeHost?: string | undefined;
}

export const deriveServerPaths = Effect.fn(function* (
Expand Down Expand Up @@ -168,6 +169,7 @@ export class ServerConfig extends Context.Service<ServerConfig, ServerConfigShap
logWebSocketEvents: false,
tailscaleServeEnabled: false,
tailscaleServePort: 443,
tailscaleServeHost: undefined,
port: 0,
host: undefined,
desktopBootstrapToken: undefined,
Expand Down
20 changes: 15 additions & 5 deletions apps/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { TerminalManagerLive } from "./terminal/Layers/Manager.ts";
import * as GitManager from "./git/GitManager.ts";
import { KeybindingsLive } from "./keybindings.ts";
import { ServerRuntimeStartup, ServerRuntimeStartupLive } from "./serverRuntimeStartup.ts";
import { TailscaleServeRuntime, TailscaleServeRuntimeLive } from "./tailscaleServeRuntime.ts";
import { OrchestrationReactorLive } from "./orchestration/Layers/OrchestrationReactor.ts";
import { RuntimeReceiptBusLive } from "./orchestration/Layers/RuntimeReceiptBus.ts";
import { ProviderRuntimeIngestionLive } from "./orchestration/Layers/ProviderRuntimeIngestion.ts";
Expand Down Expand Up @@ -352,9 +353,11 @@ export const makeServerLayer = Layer.unwrap(
? Layer.effectDiscard(
Effect.acquireRelease(
Effect.gen(function* () {
const tailscaleServeRuntime = yield* TailscaleServeRuntime;
const server = yield* HttpServer.HttpServer;
const address = server.address;
if (typeof address === "string" || !("port" in address)) {
yield* tailscaleServeRuntime.markUnavailable;
return null;
}

Expand All @@ -365,18 +368,24 @@ export const makeServerLayer = Layer.unwrap(
localHost: "127.0.0.1",
}).pipe(
Effect.as({ localPort, servePort: config.tailscaleServePort }),
Effect.tap(() => tailscaleServeRuntime.markConfigured),
Effect.tap(() =>
Effect.logInfo("Tailscale Serve configured", {
localPort,
servePort: config.tailscaleServePort,
}),
),
Effect.catch((cause) =>
Effect.logWarning("Failed to configure Tailscale Serve", {
cause,
localPort,
servePort: config.tailscaleServePort,
}).pipe(Effect.as(null)),
tailscaleServeRuntime.markUnavailable.pipe(
Effect.andThen(
Effect.logWarning("Failed to configure Tailscale Serve", {
cause,
localPort,
servePort: config.tailscaleServePort,
}),
),
Effect.as(null),
),
),
);
}),
Expand Down Expand Up @@ -411,6 +420,7 @@ export const makeServerLayer = Layer.unwrap(

return serverApplicationLayer.pipe(
Layer.provideMerge(RuntimeServicesLive),
Layer.provideMerge(TailscaleServeRuntimeLive),
Layer.provideMerge(HttpServerLive),
Layer.provide(ObservabilityLive),
Layer.provideMerge(FetchHttpClient.layer),
Expand Down
9 changes: 8 additions & 1 deletion apps/server/src/serverRuntimeStartup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
formatHostForUrl,
isWildcardHost,
issueHeadlessServeAccessInfo,
resolveAdvertisedStartupBaseUrl,
} from "./startupAccess.ts";

export class ServerRuntimeStartupError extends Data.TaggedError("ServerRuntimeStartupError")<{
Expand Down Expand Up @@ -247,7 +248,13 @@ const resolveStartupBrowserTarget = Effect.gen(function* () {
serverConfig.host && !isWildcardHost(serverConfig.host)
? `http://${formatHostForUrl(serverConfig.host)}:${serverConfig.port}`
: localUrl;
const baseTarget = serverConfig.devUrl?.toString() ?? bindUrl;
const httpBaseTarget = serverConfig.devUrl?.toString() ?? bindUrl;
const baseTarget = yield* resolveAdvertisedStartupBaseUrl({
httpBaseUrl: httpBaseTarget,
tailscaleServeEnabled: serverConfig.tailscaleServeEnabled,
tailscaleServePort: serverConfig.tailscaleServePort,
tailscaleServeHost: serverConfig.tailscaleServeHost,
});
return yield* Effect.succeed(serverConfig.mode === "desktop" ? baseTarget : undefined).pipe(
Effect.flatMap((target) =>
target ? Effect.succeed(target) : serverAuth.issueStartupPairingUrl(baseTarget),
Expand Down
Loading