Tailscale VPN sharing, Serve, and Funnel for remote access
63
52%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./src/skills/bundled/tailscale/SKILL.mdShare local services via Tailscale Serve (private) and Funnel (public internet access).
/tailscale serve 3000 Share port on tailnet
/tailscale serve 3000 --path /api Share at specific path
/tailscale serve stop 3000 Stop sharing port
/tailscale serve status View active shares/tailscale funnel 3000 Expose to internet
/tailscale funnel 3000 --https Force HTTPS
/tailscale funnel stop 3000 Stop public access
/tailscale funnel status View funnels/tailscale status Network status
/tailscale ip Show Tailscale IP
/tailscale peers List connected peers
/tailscale ping <peer> Ping a peer/tailscale send <file> <peer> Send file to peer
/tailscale receive Receive incoming filesimport { createTailscaleClient } from 'clodds/tailscale';
const tailscale = createTailscaleClient({
// Auth (optional if already logged in)
authKey: process.env.TAILSCALE_AUTHKEY,
// Socket path
socketPath: '/var/run/tailscale/tailscaled.sock',
});// Share local port on tailnet
await tailscale.serve({
port: 3000,
protocol: 'https', // 'http' | 'https'
});
console.log(`Shared at: https://${tailscale.hostname}:3000`);
// Share at specific path
await tailscale.serve({
port: 8080,
path: '/api',
protocol: 'https',
});
// Share with custom hostname
await tailscale.serve({
port: 3000,
hostname: 'clodds', // clodds.tailnet-name.ts.net
});
// Stop sharing
await tailscale.serveStop(3000);
// Get serve status
const serves = await tailscale.serveStatus();
for (const serve of serves) {
console.log(`Port ${serve.port} → ${serve.url}`);
}// Expose to public internet
await tailscale.funnel({
port: 3000,
protocol: 'https',
});
console.log(`Public URL: https://${tailscale.hostname}.ts.net`);
// With custom domain (if configured)
await tailscale.funnel({
port: 3000,
hostname: 'api.example.com',
});
// Stop funnel
await tailscale.funnelStop(3000);
// Get funnel status
const funnels = await tailscale.funnelStatus();
for (const funnel of funnels) {
console.log(`Port ${funnel.port} → ${funnel.publicUrl}`);
}// Get status
const status = await tailscale.status();
console.log(`Hostname: ${status.hostname}`);
console.log(`IP: ${status.ip}`);
console.log(`Tailnet: ${status.tailnet}`);
console.log(`Online: ${status.online}`);
// List peers
const peers = await tailscale.peers();
for (const peer of peers) {
console.log(`${peer.hostname} (${peer.ip})`);
console.log(` OS: ${peer.os}`);
console.log(` Online: ${peer.online}`);
console.log(` Last seen: ${peer.lastSeen}`);
}
// Ping peer
const ping = await tailscale.ping('other-machine');
console.log(`Latency: ${ping.latencyMs}ms`);// Send file to peer
await tailscale.sendFile({
file: '/path/to/file.zip',
peer: 'other-machine',
});
// Receive files (returns when file received)
const received = await tailscale.receiveFile({
savePath: '/downloads',
timeout: 60000,
});
console.log(`Received: ${received.filename}`);
console.log(`From: ${received.sender}`);
console.log(`Size: ${received.size} bytes`);const ip = await tailscale.getIP();
console.log(`Tailscale IP: ${ip}`); // 100.x.x.x| Feature | Serve | Funnel |
|---|---|---|
| Access | Tailnet only | Public internet |
| Auth | Tailscale identity | None (public) |
| URL | machine.tailnet.ts.net | machine.ts.net |
| Use case | Internal tools | Public APIs |
| Type | Format |
|---|---|
| Serve | https://machine.tailnet-name.ts.net:port |
| Funnel | https://machine.ts.net |
| Custom domain | https://your-domain.com |
// Share local dev server with team
await tailscale.serve({ port: 3000 });
// Team can access at https://your-machine.tailnet.ts.net:3000// Make webhook publicly accessible
await tailscale.funnel({ port: 3000, path: '/webhooks' });
// External services can POST to https://your-machine.ts.net/webhooks// Access bot from phone while away from desk
await tailscale.serve({ port: 18789 });
// Open https://your-machine.tailnet.ts.net:18789/webchat on phone2a8c94e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.