---
name: "tailscale"
description: "Tailscale-Mesh-VPN für KI-Multi-Host-Setups: Setup, Troubleshooting, OpenClaw-Provider + Modell-Katalog-Refresh."
---

# Tailscale Skill

Mesh-VPN für Multi-Host KI-Inference-Setups. Ersetzt fragile Cloudflare-Quick-Tunnels, Auto-Push-Scripts und dynamische URLs durch persistente Tailscale-IPs.

## Wann nutzen

- Mehrere Hosts (VPS + Macs + Win11 + …) sollen sich gegenseitig erreichen, OHNE Public-Ports zu öffnen
- LM Studio / Ollama / andere lokale APIs vom VPS aus konsumieren
- Cloudflare-Quick-Tunnels sterben dauernd (wechselnde URLs, 100s-Timeout, asymmetrisches Routing)
- VPN-Setup ohne Router-Konfiguration / NAT-Forwarding nötig
- Kostenloser Tier reicht: bis 100 Geräte, 3 User pro persönlichem Tailnet

## Wann NICHT nutzen

- Öffentlich erreichbare Endpoints (dann besser Cloudflare-Tunnel mit Custom Domain oder Nginx Reverse Proxy)
- Wenn Latenz <10ms entscheidend ist (Tailscale fügt ~5-50ms hinzu je nach DERP-Relay)

---

## 📋 Standard-Setup-Workflow

### Phase 1: Account-Hygiene (kritisch!)

⚠️ **Häufigster Fehler:** Hosts landen in verschiedenen Tailnets weil unterschiedliche Accounts (Google/Microsoft/Apple/Magic-Link).

**Vor Setup festlegen:** EIN Account für ALLE Hosts. Empfehlung Google-Login, weil portierbar.

**Check welcher Account aktiv ist:**
```bash
# Linux/Mac
tailscale status --json | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('Self',{}).get('UserID'))"
# Oder einfach
tailscale status   # Spalte 3 zeigt user@
```
```powershell
# Windows
tailscale status
```

### Phase 2: Installation pro Host

#### Linux (VPS) — bevorzugt über Paketmanager

**Debian/Ubuntu:**
```bash
# Tailscale-APT-Repo hinzufügen (offizielle Doku: https://tailscale.com/download/linux)
sudo apt install -y curl apt-transport-https
sudo mkdir -p --mode=0755 /usr/share/keyrings
sudo curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.noarmor.gpg -o /usr/share/keyrings/tailscale-archive-keyring.gpg
sudo curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.tailscale-keyring.list -o /etc/apt/sources.list.d/tailscale.list
sudo apt update
sudo apt install -y tailscale
sudo tailscale up --hostname=<sinnvoller-name>
```

(Distribution/Version anpassen: bullseye, bookworm, jammy, noble — siehe `https://pkgs.tailscale.com/stable/`.)

**RHEL/Fedora:**
```bash
sudo dnf config-manager --add-repo https://pkgs.tailscale.com/stable/fedora/tailscale.repo
sudo dnf install -y tailscale
sudo systemctl enable --now tailscaled
sudo tailscale up --hostname=<sinnvoller-name>
```

**Alternative (wenn Paketmanager nicht passt):** Offiziellen Installer manuell laden, inspecten, dann ausführen:
```bash
curl -fsSL -o /tmp/tailscale-install.sh https://tailscale.com/install.sh
less /tmp/tailscale-install.sh   # vorher inspizieren!
bash /tmp/tailscale-install.sh
rm /tmp/tailscale-install.sh
```

#### macOS
```bash
brew install --cask tailscale-app
open -a Tailscale
# Menüleiste-Icon → "Log in..."
```

#### Windows
1. Installer: https://tailscale.com/download/windows
2. Tray-Icon → "Log in..." → Account wählen

### Phase 3: Login-Browser-Hygiene

**Pitfall:** Tailscale-Auth-URL öffnest du im Browser, aber dort bist du in EINEM ANDEREN Google-Account eingeloggt → Error 403 "user is not authorized to view this auth request".

**Sauberer Weg:** Inkognito-Tab nutzen → Login-URL einfügen → "Sign in with Google" → expliziter Account-Wahl.

### Phase 4: IPs verifizieren

Auf jedem Host:
```bash
tailscale ip -4   # 100.x.x.x
tailscale status  # muss alle Peers zeigen
```

Alle Hosts müssen sich gegenseitig in `tailscale status` sehen. Wenn nicht → Account-Mismatch (häufigste Ursache).

---

## 🔧 Windows-Spezifika (häufige Stolperfallen)

### Killer-Fix Nr. 1: Shields-Up

Windows-Tailscale-Client hat default oft `shields-up=true` → blockt ALLE eingehenden Pakete, auch wenn Peer online ist.

```powershell
tailscale set --shields-up=false
```

**Verifikation:** `tailscale ping <peer-ip>` vom Peer aus muss "pong" zeigen.

### Killer-Fix Nr. 2: Netzwerk-Profil

Tailscale-Adapter muss auf `Private` stehen, sonst blockt Windows-Firewall alles.

```powershell
Get-NetConnectionProfile | Format-Table InterfaceAlias, NetworkCategory, Name
Set-NetConnectionProfile -InterfaceAlias "Tailscale" -NetworkCategory Private
```

### Killer-Fix Nr. 3: Firewall-Regel für Tailscale-Range

**Admin-PowerShell:**
```powershell
New-NetFirewallRule -DisplayName "Tailscale Inbound" -Direction Inbound -Protocol Any -RemoteAddress 100.64.0.0/10 -Action Allow
```

Das erlaubt **alle** Pakete aus dem Tailscale-CGNAT-Range (100.64.0.0/10) — sicher, weil nur deine eigenen Geräte da drin sind.

⚠️ **Admin-PowerShell starten:** Windows-Taste → `powershell` tippen → Rechtsklick "Windows PowerShell" → "Als Administrator ausführen" → UAC bestätigen.

Bei "Zugriff verweigert" / "Windows System Error 5" → du bist nicht Admin.

### Killer-Fix Nr. 4: Service-Status

```powershell
Get-Service Tailscale | Format-Table Name, Status, StartType
# Erwartet: Running, Automatic
```

Bei Stopped:
```powershell
Start-Service Tailscale
```

---

## 🍎 macOS-Spezifika

### LM Studio auf Local Network binden

**LM Studio GUI:**
1. **Developer Tab** → Server starten
2. Settings (Zahnrad):
   - ✅ **"Serve on Local Network"** (sonst nur 127.0.0.1!)
   - ✅ **CORS** an
   - Port: **1234**
3. App-Settings:
   - ✅ **"Run LM Studio on system startup"**
   - ✅ **"Start server when app starts"**

### Ollama auf Local Network binden

```bash
launchctl setenv OLLAMA_HOST 0.0.0.0:11434
# Oder via plist für Persistenz:
plutil -insert EnvironmentVariables -xml '<dict><key>OLLAMA_HOST</key><string>0.0.0.0:11434</string></dict>' ~/Library/LaunchAgents/homebrew.mxcl.ollama.plist
brew services restart ollama
```

### Verifizieren was auf Port lauscht
```bash
lsof -iTCP:1234 -sTCP:LISTEN
# Erwartet: *:1234 (nicht 127.0.0.1:1234)
```

---

## 🐧 Linux/VPS-Spezifika

### Login wenn schon eingeloggt (anderes Tailnet)
```bash
tailscale logout
tailscale up --hostname=<name> --reset
# URL erscheint → Browser öffnen
```

### Hostname ändern
```bash
sudo tailscale up --hostname=<neuer-name>
```

### Service-Status
```bash
systemctl status tailscaled
journalctl -u tailscaled -f
```

---

## 🔬 Troubleshooting-Matrix

| Symptom | Wahrscheinliche Ursache | Fix |
|---|---|---|
| `tailscale status` zeigt nur eigene IP, keine Peers | Falscher Account | `tailscale logout && tailscale up --reset` |
| Peer steht in `status` aber nicht erreichbar | Win11 Shields-Up | `tailscale set --shields-up=false` |
| `tailscale ping` Timeout | Firewall blockt | Firewall-Regel für 100.64.0.0/10 |
| TCP-Connect Timeout, Ping OK | Service nicht auf Local Network | LM Studio "Serve on Local Network" / `OLLAMA_HOST=0.0.0.0` |
| `Error 403` beim Login | Falscher Google-Account im Browser | Inkognito-Tab nutzen |
| `tailscale netcheck` zeigt `UDP: false` | NAT/Firewall blockt UDP 41641 | Tailscale fällt auf DERP-Relay zurück (langsamer, aber funktioniert) |
| Inferenz funktioniert, dann plötzlich Timeout | Tailscale-Reconnect | Kein Eingriff nötig, kommt wieder |
| Control UI / Task-Liste zeigt veraltete Modelle | Modell-Katalog-Cache | Gateway-Restart + Browser-F5 (siehe „Modell-Katalog Refresh") |

### Diagnose-Commands (immer parallel laufen lassen)

**Vom Server (Linux):**
```bash
tailscale status
tailscale ping --timeout=5s <peer-ip>
tailscale netcheck
timeout 5 bash -c "</dev/tcp/<peer-ip>/<port>" && echo OK || echo TIMEOUT
curl -s -o /dev/null -w "HTTP %{http_code} (%{time_total}s)\n" --max-time 10 http://<peer-ip>:<port>/<path>
```

**Vom Peer (Windows):**
```powershell
tailscale status
tailscale ping <vps-ip>
tailscale netcheck
Get-NetConnectionProfile | Format-Table InterfaceAlias, NetworkCategory
Get-Service Tailscale
netstat -an | findstr :<port>   # muss 0.0.0.0:<port> LISTENING zeigen
```

**Vom Peer (macOS):**
```bash
tailscale status
tailscale ping <vps-ip>
lsof -iTCP:<port> -sTCP:LISTEN
```

---

## 🚀 OpenClaw-Provider-Integration

### LM Studio (OpenAI-kompatibel)

```json
{
  "models": {
    "providers": {
      "lmstudio-mac": {
        "api": "openai-completions",
        "baseUrl": "http://100.93.235.101:1234/v1",
        "timeoutSeconds": 600
      },
      "lmstudio-win": {
        "api": "openai-completions",
        "baseUrl": "http://100.64.45.99:1234/v1",
        "timeoutSeconds": 600
      }
    }
  }
}
```

### Ollama

```json
{
  "macstudio": {
    "api": "ollama",
    "baseUrl": "http://100.82.59.70:11434",
    "timeoutSeconds": 600
  }
}
```

### Patch-Command (Python, weil `config.patch` baseUrl/api blockt)

```bash
python3 << 'EOF'
import json, shutil, time
src = '/root/.openclaw/openclaw.json'
shutil.copy(src, f'{src}.backup-tailscale-{int(time.time())}')
c = json.load(open(src))
provs = c.setdefault('models',{}).setdefault('providers',{})
provs['lmstudio-mac'] = {
    'api': 'openai-completions',
    'baseUrl': 'http://100.93.235.101:1234/v1',
    'timeoutSeconds': 600,
}
json.dump(c, open(src,'w'), indent=2)
EOF
```

Dann `gateway restart` (nicht `config.patch` für baseUrl/api Felder!).

### Live-Verifikation

```bash
curl -s --max-time 30 http://<tailscale-ip>:1234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{"model":"<model-id>","messages":[{"role":"user","content":"Say PONG"}],"max_tokens":10}' \
  | python3 -m json.tool
```

---

## 🔄 Modell-Katalog Refresh (PFLICHT nach Provider-Änderungen)

**Problem:** Nach Provider-Änderungen (URL-Update, neue Provider, gelöschte Provider) zeigt die Control UI / Task-Liste im Browser oft **veraltete Modell-Listen**:
- Alte (tote) Modelle erscheinen noch in der Dropdown
- Neue Modelle fehlen in der Auswahl
- Cron-Jobs / Spawn-Tasks scheitern mit „model not found"

**Ursache:** Zwei Cache-Layer:
1. **Gateway-seitig:** Provider-Modell-Listen werden read-only gecached (`models.list` default `view: "default"`)
2. **Browser-seitig:** Control UI cached die Antwort lokal

**Der vollständige Refresh-Move:**

### Schritt 1: Gateway-seitig — Vollständige Discovery erzwingen

Zwei Optionen, je nach Lage:

**A) Sanft — nur Modell-Katalog refreshen** (für laufende Sessions schonend)

In der **Control UI** im **Debug Tab** → Section „Models" → „Refresh" Button drücken.
(Ruft intern `models.list` mit `view: "all"` auf, was vollständige Discovery triggert.)

**B) Hart — Gateway-Restart** (immer wirksam, auch bei Provider-URL-Änderungen)

```bash
# Über OpenClaw-Gateway-Tool (im Mastermind-Tooling)
gateway restart "Modell-Katalog refresh nach Provider-Änderung"

# Oder CLI:
openclaw gateway restart
```

⚠️ Restart unterbricht laufende Chats/Cron-Jobs kurz (~2-5s). Bei `reloadKind: "hot"` Provider-Änderungen NICHT nötig — aber bei URL-Wechsel oder neuen Providern doch.

### Schritt 2: Browser-seitig — UI-Cache leeren

**Pflicht, sonst sieht User weiterhin die alte Liste:**

1. In der Control UI **F5** drücken (oder Cmd+R / Strg+R)
2. Bei hartnäckigem Cache: **Strg+Shift+R** (Hard-Reload, ignoriert HTTP-Cache)
3. Im Task-Liste-Dialog: Modell-Dropdown **schließen und wieder öffnen** (triggert oft separaten Fetch)

### Schritt 3: Verifikation

Im Task-Liste-Dialog (oder Cron-Job-Editor) **Modell-Dropdown** öffnen → muss neue Provider/Modelle zeigen:
- `lmstudio-mac/qwen/qwen3.6-35b-a3b`
- `lmstudio-win/qwen/qwen3.6-35b-a3b`
- (etc.)

**Falls immer noch alt:**
- Console im Browser öffnen (F12) → Network-Tab → `models.list` Request inspecten → Response zeigt was Gateway liefert
- Wenn Gateway-Response neue Modelle hat, aber UI alte zeigt → harter Browser-Cache → Strg+Shift+R oder Browser-DevTools → „Disable cache" während DevTools offen
- Wenn Gateway-Response auch alte Modelle hat → Gateway-Restart fehlt oder Provider liefert nicht (LM Studio Server down/wechselte Modelle?)

### Schritt 4: Direkter API-Check (Debug)

Was Gateway gerade als Modelle kennt:
```bash
# Frontend: Debug Tab → Manual RPC → method: models.list, params: {"view":"all"}
```

Was der Provider direkt liefert (Bypass des Gateways):
```bash
curl -s --max-time 5 http://<tailscale-ip>:1234/v1/models | python3 -m json.tool
```

Wenn die zwei Listen divergieren → Gateway hat alten Cache → Restart.

### Standard-Refresh-Workflow nach Provider-Änderung

```
1. openclaw.json patchen
2. gateway restart                          # Server-Side Refresh
3. Browser: Control UI -> F5                # Client-Side Refresh
4. Task-Liste-Dialog oeffnen -> Modell-Dropdown checken
5. Falls Luecke: Strg+Shift+R + Dropdown schliessen/oeffnen
```

---

## 🧹 Cleanup-Checkliste nach Migration von Cloudflare-Tunnels

- [ ] LaunchAgents am Mac killen: `launchctl bootout gui/$(id -u)/com.<name>.mactunnel`
- [ ] plist löschen: `rm ~/Library/LaunchAgents/com.<name>.mactunnel.plist`
- [ ] Win11 Scheduled Task: `Disable-ScheduledTask -TaskName <task-name>` oder `Unregister-ScheduledTask`
- [ ] cloudflared deinstallieren falls nicht mehr gebraucht: `brew uninstall cloudflared`
- [ ] Auto-Push-Scripts archivieren (nicht löschen, Audit-Trail)
- [ ] Nginx-Vhost für Tunnel-Subdomain raus
- [ ] stats-server / Proxy-Endpoints aus Source-Code entfernen
- [ ] Cron-Jobs für Tunnel-Healthcheck löschen
- [ ] Healthcheck-Script auf Tailscale-IPs umstellen
- [ ] OpenClaw-Provider mit alten Tunnel-URLs entfernen oder umbenennen
- [ ] ALARM-Files aufräumen (`memory/healthcheck-ALARM.md`)
- [ ] MEMORY.md updaten: alte Tunnel-Sektionen als "überholt" markieren, neue Tailscale-Sektion hinzufügen
- [ ] **Modell-Katalog refreshen (siehe Sektion „Modell-Katalog Refresh")**

---

## 🎁 Optional: MagicDNS

Statt 100.x.x.x-IPs Hostnames nutzen:

1. https://login.tailscale.com/admin/dns → MagicDNS aktivieren
2. Dann erreichbar via `http://mac-mini:1234/v1` (statt 100.93.235.101)
3. **Vorteil:** Bei IP-Wechsel (selten, aber möglich nach Reinstall) bleiben Configs gültig

⚠️ Achtung bei Hostnamen-Konflikten: Tailscale lowercased + ersetzt Sonderzeichen. `mac-studio-von-raumkommander-2` bleibt so.

---

## 📊 Performance-Erwartungen

Aus 2026-06-20 Migration (VPS = Hetzner Wien, Mac mini = Heimnetz Österreich, Win11 = Heimnetz Österreich):

| Metric | Wert |
|---|---|
| Tailscale-Handshake-Latenz | 21ms (DERP Nuremberg) |
| TCP-Connect VPS → Mac mini | 74ms |
| TCP-Connect VPS → Win11 | 48ms |
| HTTP /v1/models | 46-51ms |
| Inferenz qwen3.6-35b first-token | <2s |
| Inferenz llama-3.2-1b PONG | <1s |

DERP-Relay (wenn direkte UDP-Verbindung fehlschlägt) fügt 20-50ms hinzu, ist aber transparent.

---

## 📚 Referenzen

- **Tailscale Docs:** https://tailscale.com/kb
- **Tailscale Linux-Install (offiziell):** https://tailscale.com/download/linux
- **Tailscale APT-Pakete:** https://pkgs.tailscale.com/stable/
- **Admin-Konsole:** https://login.tailscale.com/admin/machines
- **ACL-Editor:** https://login.tailscale.com/admin/acls
- **CGNAT-Range:** 100.64.0.0/10 (Tailscale-IP-Pool)
- **Standard-Ports:** WireGuard UDP 41641 (peer-to-peer), HTTPS 443 (DERP-Relay-Fallback)

---

## 🔑 Lessons Learned (aus 2026-06-20 Migration)

1. **Cloudflare-Quick-Tunnels sind für 24/7-Inference das falsche Tool** — sie sind für temporäre Demos gedacht. Wechselnde URLs, 100s-Timeout, häufige Reconnects.
2. **Drei verschiedene Accounts gleichzeitig** ist der häufigste Setup-Fehler. Immer ZUERST `tailscale status` auf allen Hosts vergleichen.
3. **Shields-Up am Windows-Client** ist Default-aktiv und blockt alles eingehende — Killer-Fix Nummer 1.
4. **Tailscale-Adapter braucht `Private` NetworkCategory** auf Windows, sonst greift die Public-Firewall-Policy.
5. **Inkognito-Tab beim Login** vermeidet Account-Konflikte im Browser (Fehler 403).
6. **LM Studio + Ollama default auf localhost** — Server-Settings prüfen ("Serve on Local Network" / `OLLAMA_HOST=0.0.0.0`).
7. **OpenClaw `config.patch` blockt `baseUrl` und `api`** — direkter Python-Edit + `gateway restart` nötig.
8. **Backup vor jedem Config-Change** mit timestamp-Suffix.
9. **Tailscale-IPs sind persistent** — keine Auto-Push-Scripts mehr nötig (vs. Cloudflare-Quick-Tunnels).
10. **MagicDNS optional** für sprechende Hostnamen, aber 100.x.x.x funktioniert genauso gut.
11. **Modell-Katalog refresh nach Provider-Änderung** ist Pflicht — Gateway-Restart + Browser-F5, sonst zeigen Task-Liste/Cron-Editor veraltete Modelle.
12. **Auf Linux IMMER Paketmanager** (apt/dnf) für Tailscale nutzen statt eines One-Liner-Installers. Wenn Installer doch nötig: erst nach `/tmp/` herunterladen, mit `less` inspizieren, dann `bash <file>` ausführen — niemals direkt durch eine Shell-Pipe.
