95 lines
3.2 KiB
TypeScript
95 lines
3.2 KiB
TypeScript
import { execFile } from 'child_process';
|
|
|
|
export interface TailscaleServeConfig {
|
|
/** The gateway's local port (e.g. 18800). */
|
|
localPort: number;
|
|
/** Tailscale Serve HTTPS port (default 443). */
|
|
servePort?: number;
|
|
/** Custom hostname (default: machine hostname). */
|
|
hostname?: string;
|
|
}
|
|
|
|
/** Execute a command with a timeout, returning stdout. */
|
|
function exec(cmd: string, args: string[], timeoutMs = 5000): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
execFile(cmd, args, { timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
if (error) {
|
|
reject(new Error(`${cmd} ${args.join(' ')} failed: ${stderr || error.message}`));
|
|
} else {
|
|
resolve(stdout.trim());
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if Tailscale CLI is available and the daemon is running.
|
|
*/
|
|
export async function isTailscaleAvailable(): Promise<{ available: boolean; version?: string; error?: string }> {
|
|
try {
|
|
const version = await exec('tailscale', ['version', '--short']);
|
|
// Check if daemon is running by getting status
|
|
await exec('tailscale', ['status', '--json']);
|
|
return { available: true, version };
|
|
} catch (err) {
|
|
return { available: false, error: err instanceof Error ? err.message : String(err) };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the MagicDNS hostname from Tailscale status.
|
|
*/
|
|
async function getTailscaleHostname(): Promise<string | undefined> {
|
|
try {
|
|
const statusJson = await exec('tailscale', ['status', '--json']);
|
|
const status = JSON.parse(statusJson) as { Self?: { DNSName?: string } };
|
|
const dnsName = status.Self?.DNSName;
|
|
// DNSName ends with a trailing dot — strip it
|
|
return dnsName?.replace(/\.$/, '');
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start Tailscale Serve to expose the local gateway port on the tailnet.
|
|
*
|
|
* Runs: `tailscale serve --bg --https=<servePort> http://127.0.0.1:<localPort>`
|
|
*/
|
|
export async function startTailscaleServe(config: TailscaleServeConfig): Promise<string | undefined> {
|
|
const servePort = config.servePort ?? 443;
|
|
const target = `http://127.0.0.1:${config.localPort}`;
|
|
|
|
try {
|
|
await exec('tailscale', ['serve', '--bg', `--https=${servePort}`, target]);
|
|
const hostname = config.hostname ?? await getTailscaleHostname();
|
|
if (hostname) {
|
|
const url = servePort === 443 ? `https://${hostname}` : `https://${hostname}:${servePort}`;
|
|
console.log(`Tailscale Serve: Flynn available at ${url}`);
|
|
return url;
|
|
}
|
|
console.log('Tailscale Serve: started (could not determine hostname)');
|
|
return undefined;
|
|
} catch (err) {
|
|
console.error('Failed to start Tailscale Serve:', err instanceof Error ? err.message : err);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop Tailscale Serve for the configured port.
|
|
*
|
|
* Runs: `tailscale serve --https=<servePort> off`
|
|
*/
|
|
export async function stopTailscaleServe(config: TailscaleServeConfig): Promise<void> {
|
|
const servePort = config.servePort ?? 443;
|
|
|
|
try {
|
|
await exec('tailscale', ['serve', `--https=${servePort}`, 'off']);
|
|
console.log('Tailscale Serve: stopped');
|
|
} catch (err) {
|
|
// Best effort — don't fail shutdown
|
|
console.error('Failed to stop Tailscale Serve:', err instanceof Error ? err.message : err);
|
|
}
|
|
}
|