tailscale-relay

star 22

Comprehensive Tailscale relay service management with health monitoring, WebSocket testing, device discovery, and deployment automation

CloudToLocalLLM-online By CloudToLocalLLM-online schedule Updated 2/23/2026

name: tailscale-relay description: Comprehensive Tailscale relay service management with health monitoring, WebSocket testing, device discovery, and deployment automation

Tailscale Relay Management

Streamline Tailscale tunnel relay development for CloudToLocalLLM with templates, diagnostic tools, and production checklists.

Context

The Tailscale Relay service (services/tailscale-relay/) provides secure WebSocket tunneling to local LLM providers (Ollama, OpenClaw Gateway) via Tailscale network. It handles:

  • JWT-authenticated WebSocket connections on /tailscale/ws
  • Request forwarding to target devices (default: port 11434 for Ollama)
  • Health monitoring on /health endpoint
  • Multi-user support with JWT token validation

Quick Start

Start Relay Service

cd services/tailscale-relay
npm install
npm run dev  # Development with nodemon
npm start    # Production

Health Check

curl http://localhost:3002/health
# Expected: {"status":"healthy","timestamp":"..."}

Test WebSocket Connection

# Using the test script
./scripts/test-connection.sh

Templates

Relay Server Template

Location: templates/relay-server.js

import express from 'express';
import http from 'http';
import { WebSocketServer } from 'ws';
import fetch from 'node-fetch';
import jwt from 'jsonwebtoken';

const app = express();
const server = http.createServer(app);

const PORT = process.env.PORT || 3002;
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';

app.get('/health', (req, res) => {
  res.send({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

// WebSocket server for Tailscale connections
const wss = new WebSocketServer({
  server,
  path: '/tailscale/ws'
});

wss.on('connection', async (ws, req) => {
  const url = new URL(req.url, `http://${req.headers.host}`);
  const token = url.searchParams.get('token');
  const targetIp = url.searchParams.get('targetIp');

  if (!token || !targetIp) {
    ws.close(1008, 'Missing token or targetIp');
    return;
  }

  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    const userId = decoded.sub || decoded.userId;
    console.log(`[${new Date().toISOString()}] Relay: User ${userId} → ${targetIp}`);

    ws.on('message', async (message) => {
      try {
        // Forward to target device (default: Ollama on 11434)
        const targetPort = process.env.TARGET_PORT || 11434;
        const response = await fetch(`http://${targetIp}:${targetPort}/api/generate`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: message
        });

        const data = await response.buffer();
        ws.send(data);
      } catch (error) {
        console.error(`Relay error to ${targetIp}:`, error.message);
        ws.send(JSON.stringify({
          error: 'Forward failed',
          details: error.message
        }));
      }
    });

    ws.on('close', () => {
      console.log(`[${new Date().toISOString()}] Relay: Disconnected ${userId}`);
    });

  } catch (error) {
    console.error('Relay auth failed:', error.message);
    ws.close(1008, 'Authentication failed');
  }
});

server.listen(PORT, () => {
  console.log(`Tailscale Relay listening on port ${PORT}`);
  console.log(`WebSocket path: /tailscale/ws?token=<jwt>&targetIp=<ip>`);
});

Docker Compose Template

Location: templates/docker-compose.yml

version: '3.8'
services:
  tailscale-relay:
    build: .
    ports:
      - "3002:3002"
    environment:
      - PORT=3002
      - JWT_SECRET=${JWT_SECRET}
      - TARGET_PORT=11434
      - NODE_ENV=production
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3002/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Scripts

Health Check Script

Location: scripts/health-check.sh

#!/bin/bash
# Tailscale Relay Health Check

RELAY_URL="${RELAY_URL:-http://localhost:3002}"
ENDPOINT="/health"

echo "Checking Tailscale Relay health at $RELAY_URL$ENDPOINT..."

response=$(curl -s -w "\n%{http_code}" "$RELAY_URL$ENDPOINT")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')

if [ "$http_code" -eq 200 ]; then
  status=$(echo "$body" | jq -r '.status')
  if [ "$status" = "healthy" ]; then
    echo "✓ Relay is healthy"
    echo "$body" | jq '.'
    exit 0
  fi
fi

echo "✗ Relay health check failed (HTTP $http_code)"
echo "$body"
exit 1

WebSocket Connection Test

Location: scripts/test-connection.sh

#!/bin/bash
# Test WebSocket connection to Tailscale Relay

RELAY_URL="${RELAY_URL:-ws://localhost:3002/tailscale/ws}"
JWT_TOKEN="${JWT_TOKEN}"
TARGET_IP="${TARGET_IP:-100.100.100.100}"

if [ -z "$JWT_TOKEN" ]; then
  echo "Error: JWT_TOKEN environment variable required"
  echo "Usage: JWT_TOKEN=xxx TARGET_IP=xxx ./test-connection.sh"
  exit 1
fi

echo "Testing WebSocket connection to $RELAY_URL"
echo "Target IP: $TARGET_IP"

# Using websocat for testing (install: apt install websocat)
echo '{"model":"llama2","prompt":"Hello"}' | websocat "$RELAY_URL?token=$JWT_TOKEN&targetIp=$TARGET_IP"

# Alternative: Use wscat
# echo '{"model":"llama2","prompt":"Hello"}' | wscat -c "$RELAY_URL?token=$JWT_TOKEN&targetIp=$TARGET_IP"

Tailscale Device Discovery

Location: scripts/list-devices.sh

#!/bin/bash
# List Tailscale devices on the tailnet

if ! command -v tailscale &> /dev/null; then
  echo "Error: tailscale CLI not installed"
  echo "Install: curl -fsSL https://tailscale.com/install.sh | sh"
  exit 1
fi

echo "Tailscale devices on your tailnet:"
echo "=================================="
tailscale status --json | jq -r '.Peer[] | select(.Online == true) | "\(.HostName)\t\(.TailscaleIPs[0])\t\(.OS)"' | column -t

Flutter Client Integration Example

Location: examples/client-implementation.dart

import 'package:web_socket_channel/web_socket_channel.dart';

class TailscaleRelayClient {
  final String relayUrl;
  final String jwtToken;
  final String targetIp;

  TailscaleRelayClient({
    required this.relayUrl,
    required this.jwtToken,
    required this.targetIp,
  });

  WebSocketChannel connect() {
    final wsUrl = Uri.parse('$relayUrl/tailscale/ws?token=$jwtToken&targetIp=$targetIp');
    return WebSocketChannel.connect(wsUrl);
  }

  Future<void> sendRequest(Map<String, dynamic> request) async {
    final channel = connect();

    await channel.ready;

    channel.sink.add(jsonEncode(request));

    channel.stream.listen(
      (response) {
        print('Response: $response');
      },
      onError: (error) {
        print('WebSocket error: $error');
      },
      onDone: () {
        channel.sink.close();
      },
    );
  }
}

Deployment Checklist

Pre-Deployment

  • JWT_SECRET configured in environment (strong random key)
  • TARGET_PORT set correctly (11434 for Ollama, 18789 for OpenClaw)
  • Firewall allows inbound traffic on PORT (default: 3002)
  • Tailscale installed and authenticated on relay server
  • TLS/SSL termination configured (reverse proxy)

Security Verification

  • JWT validation tested with expired/invalid tokens
  • WebSocket connection timeout configured
  • Rate limiting implemented (consider express-rate-limit)
  • CORS configured if accessed from browser
  • Input sanitization on targetIp parameter

Monitoring Setup

  • Health check endpoint accessible
  • Logging configured (Winston or similar)
  • Metrics collection (Prometheus format optional)
  • Alert rules for connection failures
  • Uptime monitoring configured

Production Rollout

  • Docker image built and pushed to registry
  • docker-compose.yml deployed to server
  • Health check passing: curl http://localhost:3002/health
  • WebSocket connection tested with real client
  • Log rotation configured
  • Restart policy set (unless-stopped)

Troubleshooting

Relay Not Starting

# Check port availability
lsof -i :3002

# Verify Tailscale status
tailscale status

# Check JWT_SECRET is set
echo $JWT_SECRET

WebSocket Connection Failures

# Test WebSocket directly
websocat ws://localhost:3002/tailscale/ws?token=xxx&targetIp=yyy

# Check relay logs
docker logs tailscale-relay -f

# Verify JWT token validity
echo "eyJ..." | jq -R 'split(".") | .[0],.[1] | @base64d | fromjson'

Target Device Unreachable

# Ping target via Tailscale
ping <target-tailscale-ip>

# Check target service is running
curl http://<target-ip>:11434/api/tags

# Verify Tailscale network connectivity
tailscale ping <target-hostname>

Related Files

  • Relay service: services/tailscale-relay/src/server.js
  • Flutter integration: lib/screens/onboarding/steps/tailscale_discovery_step.dart
  • Onboarding service: lib/services/onboarding/setup_wizard_service.dart
  • Documentation: docs/user-guide/SETUP_GUIDE.md
Install via CLI
npx skills add https://github.com/CloudToLocalLLM-online/CloudToLocalLLM --skill tailscale-relay
Repository Details
star Stars 22
call_split Forks 4
navigation Branch main
article Path SKILL.md
More from Creator
CloudToLocalLLM-online
CloudToLocalLLM-online Explore all skills →